--- /dev/null
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | CiviCRM version 4.4 |
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC (c) 2004-2013 |
+ +--------------------------------------------------------------------+
+ | This file is a part of CiviCRM. |
+ | |
+ | CiviCRM is free software; you can copy, modify, and distribute it |
+ | under the terms of the GNU Affero General Public License |
+ | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
+ | |
+ | CiviCRM is distributed in the hope that it will be useful, but |
+ | WITHOUT ANY WARRANTY; without even the implied warranty of |
+ | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
+ | See the GNU Affero General Public License for more details. |
+ | |
+ | You should have received a copy of the GNU Affero General Public |
+ | License and the CiviCRM Licensing Exception along |
+ | with this program; if not, contact CiviCRM LLC |
+ | at info[AT]civicrm[DOT]org. If you have questions about the |
+ | GNU Affero General Public License or the licensing of CiviCRM, |
+ | see the CiviCRM license FAQ at http://civicrm.org/licensing |
+ +--------------------------------------------------------------------+
+*/
+
+/**
+ * Implement the "match" and "match-mandatory" options. If the submitted record doesn't have an ID
+ * but a "match" key is specified, then we will automatically search for pre-existing record and
+ * update it.
+ *
+ * Note that "match" and "match-mandatory" behave the same in the case where one matching record
+ * exists (ie they update the record). They also behave the same if there are multiple matching
+ * records (ie they throw an error). However, if there is no matching record, they differ:
+ * - "match-mandatory" will generate an error
+ * - "match" will allow action to proceed -- thus inserting a new record
+ *
+ * @code
+ * $result = civicrm_api('contact', 'create', array(
+ * 'options' => array(
+ * 'match' => array('last_name', 'first_name')
+ * ),
+ * 'first_name' => 'Jeffrey',
+ * 'last_name' => 'Lebowski',
+ * 'nick_name' => 'The Dude',
+ * ));
+ * @endcode
+ *
+ * @package CRM
+ * @copyright CiviCRM LLC (c) 2004-2013
+ * $Id$
+ */
+
+require_once 'api/Wrapper.php';
+class CRM_Utils_API_MatchOption implements API_Wrapper {
+
+ /**
+ * @var CRM_Utils_API_MatchOption
+ */
+ private static $_singleton = NULL;
+
+ /**
+ * @return CRM_Utils_API_MatchOption
+ */
+ public static function singleton() {
+ if (self::$_singleton === NULL) {
+ self::$_singleton = new CRM_Utils_API_MatchOption();
+ }
+ return self::$_singleton;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function fromApiInput($apiRequest) {
+ if ($apiRequest['action'] === 'create' && empty($apiRequest['params']['id']) && isset($apiRequest['params'], $apiRequest['params']['options'])) {
+ $keys = NULL;
+ if (isset($apiRequest['params']['options']['match-mandatory'])) {
+ $isMandatory = TRUE;
+ $keys = $apiRequest['params']['options']['match-mandatory'];
+ }
+ elseif ($apiRequest['params']['options']['match']) {
+ $isMandatory = FALSE;
+ $keys = $apiRequest['params']['options']['match'];
+ }
+
+ if (!empty($keys)) {
+ $getParams = $this->createGetParams($apiRequest, $keys);
+ $getResult = civicrm_api3($apiRequest['entity'], 'get', $getParams);
+ if ($getResult['count'] == 0) {
+ if ($isMandatory) throw new API_Exception("Failed to match existing record");
+ // OK, don't care
+ } elseif ($getResult['count'] == 1) {
+ $item = array_shift($getResult['values']);
+ $apiRequest['params']['id'] = $item['id'];
+ } else {
+ throw new API_Exception("Ambiguous match criteria");
+ }
+ }
+ }
+ return $apiRequest;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function toApiOutput($apiRequest, $result) {
+ return $result;
+ }
+
+ /**
+ * Create APIv3 "get" parameters to lookup an existing record using $keys
+ *
+ * @param array $apiRequest
+ * @param array $keys list of keys to match against
+ * @return array APIv3 $params
+ */
+ function createGetParams($apiRequest, $keys) {
+ $params = array('version' => 3);
+ foreach ($keys as $key) {
+ $params[$key] = CRM_Utils_Array::value($key, $apiRequest['params'], '');
+ }
+ return $params;
+ }
+}
--- /dev/null
+<?php
+
+require_once 'CiviTest/CiviUnitTestCase.php';
+/**
+ * Test that the API accepts the 'match' and 'match-mandatory' options.
+ */
+class CRM_Utils_API_MatchOptionTest extends CiviUnitTestCase {
+
+ function setUp() {
+ parent::setUp();
+ $this->assertDBQuery(0, "SELECT count(*) FROM civicrm_contact WHERE first_name='Jeffrey' and last_name='Lebowski'");
+
+ // Create noise to ensure we don't accidentally/coincidentally match the first record
+ $this->individualCreate(array('email' => 'ignore1@example.com'));
+ }
+
+ /**
+ * If there's no pre-existing record, then insert a new one.
+ */
+ function testMatch_none() {
+ $result = $this->callAPISuccess('contact', 'create', array(
+ 'options' => array(
+ 'match' => array('first_name', 'last_name'),
+ ),
+ 'contact_type' => 'Individual',
+ 'first_name' => 'Jeffrey',
+ 'last_name' => 'Lebowski',
+ 'nick_name' => '',
+ 'external_identifier' => '1',
+ ));
+ $this->assertEquals('Jeffrey', $result['values'][$result['id']]['first_name']);
+ $this->assertEquals('Lebowski', $result['values'][$result['id']]['last_name']);
+ }
+
+ /**
+ * If there's no pre-existing record, then throw an error.
+ */
+ function testMatchMandatory_none() {
+ $this->callAPIFailure('contact', 'create', array(
+ 'options' => array(
+ 'match-mandatory' => array('first_name', 'last_name'),
+ ),
+ 'contact_type' => 'Individual',
+ 'first_name' => 'Jeffrey',
+ 'last_name' => 'Lebowski',
+ 'nick_name' => '',
+ 'external_identifier' => '1',
+ ), 'Failed to match existing record');
+ }
+
+ function apiOptionNames() {
+ return array(
+ array('match'),
+ array('match-mandatory'),
+ );
+ }
+
+ /**
+ * If there's one pre-existing record, then update it.
+ *
+ * @dataProvider apiOptionNames
+ * @param string $apiOptionName e.g. "match" or "match-mandatory"
+ */
+ function testMatch_one($apiOptionName) {
+ // create basic record
+ $result1 = $this->callAPISuccess('contact', 'create', array(
+ 'contact_type' => 'Individual',
+ 'first_name' => 'Jeffrey',
+ 'last_name' => 'Lebowski',
+ 'nick_name' => '',
+ 'external_identifier' => '1',
+ ));
+
+ $this->individualCreate(array('email' => 'ignore2@example.com')); // more noise!
+
+ // update the record by matching first/last name
+ $result2 = $this->callAPISuccess('contact', 'create', array(
+ 'options' => array(
+ $apiOptionName => array('first_name', 'last_name'),
+ ),
+ 'contact_type' => 'Individual',
+ 'first_name' => 'Jeffrey',
+ 'last_name' => 'Lebowski',
+ 'nick_name' => 'The Dude',
+ 'external_identifier' => '2',
+ ));
+
+ $this->assertEquals($result1['id'], $result2['id']);
+ $this->assertEquals('Jeffrey', $result2['values'][$result2['id']]['first_name']);
+ $this->assertEquals('Lebowski', $result2['values'][$result2['id']]['last_name']);
+ $this->assertEquals('The Dude', $result2['values'][$result2['id']]['nick_name']);
+ // Make sure it was a real update
+ $this->assertDBQuery(1, "SELECT count(*) FROM civicrm_contact WHERE first_name='Jeffrey' and last_name='Lebowski' AND nick_name = 'The Dude'");
+ }
+
+ /**
+ * If there's more than one pre-existing record, throw an error.
+ *
+ * @dataProvider apiOptionNames
+ * @param string $apiOptionName e.g. "match" or "match-mandatory"
+ */
+ function testMatch_many($apiOptionName) {
+ // create the first Lebowski
+ $result1 = $this->callAPISuccess('contact', 'create', array(
+ 'contact_type' => 'Individual',
+ 'first_name' => 'Jeffrey',
+ 'last_name' => 'Lebowski',
+ 'nick_name' => 'The Dude',
+ 'external_identifier' => '1',
+ ));
+
+ // create the second Lebowski
+ $result2 = $this->callAPISuccess('contact', 'create', array(
+ 'contact_type' => 'Individual',
+ 'first_name' => 'Jeffrey',
+ 'last_name' => 'Lebowski',
+ 'nick_name' => 'The Big Lebowski',
+ 'external_identifier' => '2',
+ ));
+
+ $this->individualCreate(array('email' => 'ignore2@example.com')); // more noise!
+
+ // Try to update - but fail due to ambiguity
+ $result3 = $this->callAPIFailure('contact', 'create', array(
+ 'options' => array(
+ $apiOptionName => array('first_name', 'last_name'),
+ ),
+ 'contact_type' => 'Individual',
+ 'first_name' => 'Jeffrey',
+ 'last_name' => 'Lebowski',
+ 'nick_name' => '',
+ 'external_identifier' => 'new',
+ ), 'Ambiguous match criteria');
+ }
+
+}