CRM-13312 - CRM_Utils_API_MatchOption - Add option for updating based on user-specifi...
authorTim Otten <totten@civicrm.org>
Fri, 30 Aug 2013 23:41:35 +0000 (16:41 -0700)
committerTim Otten <totten@civicrm.org>
Sat, 31 Aug 2013 01:57:02 +0000 (18:57 -0700)
----------------------------------------
* CRM-13312: Implement API support for options.match
  http://issues.civicrm.org/jira/browse/CRM-13312

CRM/Utils/API/MatchOption.php [new file with mode: 0644]
api/api.php
tests/phpunit/CRM/Utils/API/MatchOptionTest.php [new file with mode: 0644]

diff --git a/CRM/Utils/API/MatchOption.php b/CRM/Utils/API/MatchOption.php
new file mode 100644 (file)
index 0000000..494e2d1
--- /dev/null
@@ -0,0 +1,126 @@
+<?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;
+  }
+}
index f42d5ac8da50991408164b80109849b1f177ad77..e2a366c06b7f4102632b6fd595a131179d85ca85 100644 (file)
@@ -30,6 +30,7 @@ function civicrm_api($entity, $action, $params, $extra = NULL) {
     CRM_Utils_API_HTMLInputCoder::singleton(),
     CRM_Utils_API_NullOutputCoder::singleton(),
     CRM_Utils_API_ReloadOption::singleton(),
+    CRM_Utils_API_MatchOption::singleton(),
   );
   CRM_Utils_Hook::apiWrappers($apiWrappers,$apiRequest);
 
diff --git a/tests/phpunit/CRM/Utils/API/MatchOptionTest.php b/tests/phpunit/CRM/Utils/API/MatchOptionTest.php
new file mode 100644 (file)
index 0000000..60fcae9
--- /dev/null
@@ -0,0 +1,136 @@
+<?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');
+  }
+
+}