From 70be69e231b5fc67e2af0530ea016ade54d25e0f Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Thu, 27 Mar 2014 16:04:45 -0700 Subject: [PATCH] CRM-14370 - API Kernel - Parse options/data/chains for v4 --- CRM/Utils/OptionBag.php | 129 ++++++++++++++++++++++++++ Civi/API/Kernel.php | 94 ++++++++++++++++++- tests/phpunit/Civi/API/KernelTest.php | 103 ++++++++++++++++++++ 3 files changed, 322 insertions(+), 4 deletions(-) create mode 100644 CRM/Utils/OptionBag.php create mode 100644 tests/phpunit/Civi/API/KernelTest.php diff --git a/CRM/Utils/OptionBag.php b/CRM/Utils/OptionBag.php new file mode 100644 index 0000000000..ebcf9d597c --- /dev/null +++ b/CRM/Utils/OptionBag.php @@ -0,0 +1,129 @@ +data = $data; + } + + /** + * @return array + */ + public function getArray() { + return $this->data; + } + + /** + * Retrieve a value from the bag + * + * @param string $key + * @param string|null $type + * @param mixed $default + * @return mixed + * @throws API_Exception + */ + public function get($key, $type = NULL, $default = NULL) { + if (!array_key_exists($key, $this->data)) { + return $default; + } + if (!$type) { + return $this->data[$key]; + } + $r = CRM_Utils_Type::validate($this->data[$key], $type); + if ($r !== NULL) { + return $r; + } + else { + throw new \API_Exception(ts("Could not find valid value for %1 (%2)", array(1 => $key, 2 => $type))); + } + } + + public function has($key) { + return isset($this->data[$key]); + } + + /** + * (PHP 5 >= 5.0.0)
+ * Whether a offset exists + * @link http://php.net/manual/en/arrayaccess.offsetexists.php + * @param mixed $offset

+ * An offset to check for. + *

+ * @return boolean true on success or false on failure. + *

+ *

+ * The return value will be casted to boolean if non-boolean was returned. + */ + public function offsetExists($offset) { + return array_key_exists($offset, $this->data); + } + + /** + * (PHP 5 >= 5.0.0)
+ * Offset to retrieve + * @link http://php.net/manual/en/arrayaccess.offsetget.php + * @param mixed $offset

+ * The offset to retrieve. + *

+ * @return mixed Can return all value types. + */ + public function offsetGet($offset) { + return $this->data[$offset]; + } + + /** + * (PHP 5 >= 5.0.0)
+ * Offset to set + * @link http://php.net/manual/en/arrayaccess.offsetset.php + * @param mixed $offset

+ * The offset to assign the value to. + *

+ * @param mixed $value

+ * The value to set. + *

+ * @return void + */ + public function offsetSet($offset, $value) { + $this->data[$offset] = $value; + } + + /** + * (PHP 5 >= 5.0.0)
+ * Offset to unset + * @link http://php.net/manual/en/arrayaccess.offsetunset.php + * @param mixed $offset

+ * The offset to unset. + *

+ * @return void + */ + public function offsetUnset($offset) { + unset($this->data[$offset]); + } + + /** + * (PHP 5 >= 5.0.0)
+ * Retrieve an external iterator + * @link http://php.net/manual/en/iteratoraggregate.getiterator.php + * @return Traversable An instance of an object implementing Iterator or + * Traversable + */ + public function getIterator() { + return new ArrayIterator($this->data); + } + + /** + * (PHP 5 >= 5.1.0)
+ * Count elements of an object + * @link http://php.net/manual/en/countable.count.php + * @return int The custom count as an integer. + *

+ *

+ * The return value is cast to an integer. + */ + public function count() { + return count($this->data); + } + + +} \ No newline at end of file diff --git a/Civi/API/Kernel.php b/Civi/API/Kernel.php index 91e55881cc..9740466574 100644 --- a/Civi/API/Kernel.php +++ b/Civi/API/Kernel.php @@ -72,6 +72,8 @@ class Kernel { */ $apiProvider = NULL; + // TODO Define alternative calling convention makes it easier to construct $apiRequest + // without the ambiguity of "data" vs "options" $apiRequest = $this->createRequest($entity, $action, $params, $extra); try { @@ -126,16 +128,100 @@ class Kernel { * @param string $action * @param array $params * @param mixed $extra - * @return array the request descriptor + * @return array the request descriptor; keys: + * - version: int + * - entity: string + * - action: string + * - params: array (string $key => mixed $value) [deprecated in v4] + * - extra: unspecified + * - fields: NULL|array (string $key => array $fieldSpec) + * - options: \CRM_Utils_OptionBag derived from params [v4-only] + * - data: \CRM_Utils_OptionBag derived from params [v4-only] + * - chains: unspecified derived from params [v4-only] */ public function createRequest($entity, $action, $params, $extra) { - $apiRequest = array(); - $apiRequest['entity'] = \CRM_Utils_String::munge($entity); - $apiRequest['action'] = \CRM_Utils_String::munge($action); + $apiRequest = array(); // new \Civi\API\Request(); $apiRequest['version'] = civicrm_get_api_version($params); $apiRequest['params'] = $params; $apiRequest['extra'] = $extra; $apiRequest['fields'] = NULL; + + if ($apiRequest['version'] <= 3) { + // APIv1-v3 munges entity/action names, which means that the same name can be written + // multiple ways. That makes it harder to work with. + $apiRequest['entity'] = \CRM_Utils_String::munge($entity); + $apiRequest['action'] = \CRM_Utils_String::munge($action); + } + else { + // APIv4 requires exact entity/action name; deviations should cause errors + if (!preg_match('/^[a-zA-Z][a-zA-Z0-9]*$/', $entity)) { + throw new \API_Exception("Malformed entity"); + } + if (!preg_match('/^[a-zA-Z][a-zA-Z0-9]*$/', $action)) { + throw new \API_Exception("Malformed action"); + } + $apiRequest['entity'] = $entity; + $apiRequest['action'] = $action; + } + + // APIv1-v3 mix data+options in $params which means that each API callback is responsible + // for splitting the two. In APIv4, the split is done systematically so that we don't + // so much parsing logic spread around. + if ($apiRequest['version'] >= 4) { + $options = array(); + $data = array(); + $chains = array(); + foreach ($params as $key => $value) { + if ($key == 'options') { + $options = array_merge($options, $value); + } + elseif ($key == 'return') { + if (!isset($options['return'])) { + $options['return'] = array(); + } + $options['return'] = array_merge($options['return'], $value); + } + elseif (preg_match('/^option\.(.*)$/', $key, $matches)) { + $options[$matches[1]] = $value; + } + elseif (preg_match('/^return\.(.*)$/', $key, $matches)) { + if ($value) { + if (!isset($options['return'])) { + $options['return'] = array(); + } + $options['return'][] = $matches[1]; + } + } + elseif (preg_match('/^format\.(.*)$/', $key, $matches)) { + if ($value) { + if (!isset($options['format'])) { + $options['format'] = $matches[1]; + } + else { + throw new \API_Exception("Too many API formats specified"); + } + } + } + elseif (preg_match('/^api\./', $key)) { + // FIXME: represent subrequests as instances of "Request" + $chains[$key] = $value; + } + elseif ($key == 'debug') { + $options['debug'] = $value; + } + elseif ($key == 'version') { + // ignore + } + else { + $data[$key] = $value; + + } + } + $apiRequest['options'] = new \CRM_Utils_OptionBag($options); + $apiRequest['data'] = new \CRM_Utils_OptionBag($data); + $apiRequest['chains'] = $chains; + } + return $apiRequest; } diff --git a/tests/phpunit/Civi/API/KernelTest.php b/tests/phpunit/Civi/API/KernelTest.php new file mode 100644 index 0000000000..1240aaabbd --- /dev/null +++ b/tests/phpunit/Civi/API/KernelTest.php @@ -0,0 +1,103 @@ + $requestParams, 1 => $expectedOptions, 2 => $expectedData, 3 => $expectedChains) + $cases[] = array( + array('version' => 4), // requestParams + array(), // expectedOptions + array(), // expectedData + array(), // expectedChains + ); + $cases[] = array( + array('version' => 4, 'debug' => TRUE), // requestParams + array('debug' => TRUE), // expectedOptions + array(), // expectedData + array(), // expectedChains + ); + $cases[] = array( + array('version' => 4, 'format.is_success' => TRUE), // requestParams + array('format' => 'is_success'), // expectedOptions + array(), // expectedData + array(), // expectedChains + ); + $cases[] = array( + array('version' => 4, 'option.limit' => 15, 'option.foo' => array('bar'), 'options' => array('whiz' => 'bang'), 'optionnotreally' => 'data'), // requestParams + array('limit' => 15, 'foo' => array('bar'), 'whiz' => 'bang'), // expectedOptions + array('optionnotreally' => 'data'), // expectedData + array(), // expectedChains + ); + $cases[] = array( + array('version' => 4, 'return' => array('field1', 'field2'), 'return.field3' => 1, 'return.field4' => 0, 'returnontreally' => 'data'), // requestParams + array('return' => array('field1', 'field2', 'field3')), // expectedOptions + array('returnontreally' => 'data'), // expectedData + array(), // expectedChains + ); + $cases[] = array( + array('version' => 4, 'foo' => array('bar'), 'whiz' => 'bang'), // requestParams + array(), // expectedOptions + array('foo' => array('bar'), 'whiz' => 'bang'), // expectedData + array(), // expectedChains + ); + $cases[] = array( + array('version' => 4, 'api.foo.bar' => array('whiz' => 'bang')), // requestParams + array(), // expectedOptions + array(), // expectedData + array('api.foo.bar' => array('whiz' => 'bang')), // expectedChains + ); + $cases[] = array( + array( + 'version' => 4, + 'option.limit' => 15, + 'options' => array('whiz' => 'bang'), + 'somedata' => 'data', + 'moredata' => array('woosh'), + 'return.field1' => 1, + 'return' => array('field2'), + 'api.first' => array('the first call'), + 'api.second' => array('the second call'), + ), // requestParams + array('limit' => 15, 'whiz' => 'bang', 'return' => array('field1', 'field2')), // expectedOptions + array('somedata' => 'data', 'moredata' => array('woosh')), // expectedData + array('api.first' => array('the first call'), 'api.second' => array('the second call')), // expectedChains + ); + return $cases; + } + + /** + * @param $inputParams + * @param $expectedOptions + * @param $expectedData + * @param $expectedChains + * @dataProvider v4options + */ + function testCreateRequest_v4Options($inputParams, $expectedOptions, $expectedData, $expectedChains) { + $kernel = new Kernel(NULL); + $apiRequest = $kernel->createRequest('MyEntity', 'MyAction', $inputParams, NULL); + $this->assertEquals($expectedOptions, $apiRequest['options']->getArray()); + $this->assertEquals($expectedData, $apiRequest['data']->getArray()); + $this->assertEquals($expectedChains, $apiRequest['chains']); + } + + /** + * @expectedException \API_Exception + */ + function testCreateRequest_v4BadEntity() { + $kernel = new Kernel(NULL); + $kernel->createRequest('Not!Valid', 'create', array('version' => 4), NULL); + } + + /** + * @expectedException \API_Exception + */ + function testCreateRequest_v4BadAction() { + $kernel = new Kernel(NULL); + $kernel->createRequest('MyEntity', 'bad!action', array('version' => 4), NULL); + } +} \ No newline at end of file -- 2.25.1