From c8463688c6993d1e59d6bca13e619944149056e5 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Fri, 30 Aug 2013 16:41:35 -0700 Subject: [PATCH] CRM-13312 - CRM_Utils_API_MatchOption - Add option for updating based on user-specified keys ---------------------------------------- * CRM-13312: Implement API support for options.match http://issues.civicrm.org/jira/browse/CRM-13312 --- CRM/Utils/API/MatchOption.php | 126 ++++++++++++++++ api/api.php | 1 + .../phpunit/CRM/Utils/API/MatchOptionTest.php | 136 ++++++++++++++++++ 3 files changed, 263 insertions(+) create mode 100644 CRM/Utils/API/MatchOption.php create mode 100644 tests/phpunit/CRM/Utils/API/MatchOptionTest.php diff --git a/CRM/Utils/API/MatchOption.php b/CRM/Utils/API/MatchOption.php new file mode 100644 index 0000000000..494e2d1705 --- /dev/null +++ b/CRM/Utils/API/MatchOption.php @@ -0,0 +1,126 @@ + 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; + } +} diff --git a/api/api.php b/api/api.php index f42d5ac8da..e2a366c06b 100644 --- a/api/api.php +++ b/api/api.php @@ -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 index 0000000000..60fcae9d7b --- /dev/null +++ b/tests/phpunit/CRM/Utils/API/MatchOptionTest.php @@ -0,0 +1,136 @@ +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'); + } + +} -- 2.25.1