From c65db512e9564b7ae4312d023306b407e20f7cf7 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Thu, 27 Mar 2014 23:38:19 -0700 Subject: [PATCH] CRM-14370 - Move helper functions into classes In particular: * civicrm_get_api_version => Kernel::parseVersion * _civicrm_api_resolve => MagicFunctionProvider::resolve * _civicrm_api_loadEntity => MagicFunctionProvider::loadEntity * civicrm_api_get_function_name => MagicFunctionProvider::getFunctionName * civicrm_api3_generic_getActions (partial) => MagicFunctionProvider::getActionNames Conflicts: Civi/Core/Container.php --- CRM/Admin/Form/Job.php | 13 +- Civi/API/Kernel.php | 19 ++- Civi/API/Provider/MagicFunctionProvider.php | 158 +++++++++++++++++- Civi/API/Subscriber/APIv3SchemaAdapter.php | 3 + Civi/Core/Container.php | 11 +- api/api.php | 169 -------------------- api/v3/Generic.php | 8 +- api/v3/Generic/Getactions.php | 25 +-- 8 files changed, 202 insertions(+), 204 deletions(-) diff --git a/CRM/Admin/Form/Job.php b/CRM/Admin/Form/Job.php index 4a147c31f8..c9ec3be713 100644 --- a/CRM/Admin/Form/Job.php +++ b/CRM/Admin/Form/Job.php @@ -112,13 +112,12 @@ class CRM_Admin_Form_Job extends CRM_Admin_Form { require_once 'api/api.php'; - $apiRequest = array(); - $apiRequest['entity'] = CRM_Utils_String::munge($fields['api_entity']); - $apiRequest['action'] = CRM_Utils_String::munge($fields['api_action']); - $apiRequest['version'] = 3; - $apiRequest += _civicrm_api_resolve($apiRequest); // look up function, file, is_generic - - if( !$apiRequest['function'] ) { + /** @var \Civi\API\Kernel $apiKernel */ + $apiKernel = \Civi\Core\Container::singleton()->get('civi_api_kernel'); + $apiRequest = $apiKernel->createRequest($fields['api_entity'], $fields['api_action'], array('version' => 3), NULL); + try { + $apiKernel->resolve($apiRequest); + } catch (\Civi\API\Exception\NotImplementedException $e) { $errors['api_action'] = ts('Given API command is not defined.'); } diff --git a/Civi/API/Kernel.php b/Civi/API/Kernel.php index 1189bde7f4..c9943a8a88 100644 --- a/Civi/API/Kernel.php +++ b/Civi/API/Kernel.php @@ -133,7 +133,7 @@ class Kernel { */ public function createRequest($entity, $action, $params, $extra) { $apiRequest = array(); // new \Civi\API\Request(); - $apiRequest['version'] = civicrm_get_api_version($params); + $apiRequest['version'] = $this->parseVersion($params); $apiRequest['params'] = $params; $apiRequest['extra'] = $extra; $apiRequest['fields'] = NULL; @@ -217,6 +217,23 @@ class Kernel { return $apiRequest; } + /** + * We must be sure that every request uses only one version of the API. + * + * @param array $params + * @return int + */ + protected function parseVersion($params) { + $desired_version = empty($params['version']) ? NULL : (int) $params['version']; + if (isset($desired_version) && is_integer($desired_version)) { + return $desired_version; + } + else { + // we will set the default to version 3 as soon as we find that it works. + return 3; + } + } + public function boot() { require_once ('api/v3/utils.php'); require_once 'api/Exception.php'; diff --git a/Civi/API/Provider/MagicFunctionProvider.php b/Civi/API/Provider/MagicFunctionProvider.php index 614d28833b..537b1987d0 100644 --- a/Civi/API/Provider/MagicFunctionProvider.php +++ b/Civi/API/Provider/MagicFunctionProvider.php @@ -42,9 +42,18 @@ class MagicFunctionProvider implements EventSubscriberInterface, ProviderInterfa ); } + /** + * @var array (string $cachekey => array('function' => string, 'is_generic' => bool)) + */ + private $cache; + + function __construct() { + $this->cache = array(); + } + public function onApiResolve(\Civi\API\Event\ResolveEvent $event) { $apiRequest = $event->getApiRequest(); - $resolved = _civicrm_api_resolve($apiRequest); + $resolved = $this->resolve($apiRequest); if ($resolved['function']) { $apiRequest += $resolved; $event->setApiRequest($apiRequest); @@ -67,4 +76,151 @@ class MagicFunctionProvider implements EventSubscriberInterface, ProviderInterfa return $result; } + public function getActionNames($entity, $version) { + // TODO don't recurse into civicrm_api + $r = civicrm_api('Entity', 'Get', array('version' => $version)); + if (!in_array($entity, $r['values'])) { + throw new \Civi\API\Exception\NotImplementedException("Entity " . $entity . " invalid. Use api.entity.get to have the list", array('entity' => $r['values'])); + } + $this->loadEntity($entity, $version); + + $functions = get_defined_functions(); + $actions = array(); + $prefix = 'civicrm_api' . $version . '_' . strtolower($entity) . '_'; + $prefixGeneric = 'civicrm_api' . $version . '_generic_'; + foreach ($functions['user'] as $fct) { + if (strpos($fct, $prefix) === 0) { + $actions[] = substr($fct, strlen($prefix)); + } + elseif (strpos($fct, $prefixGeneric) === 0) { + $actions[] = substr($fct, strlen($prefixGeneric)); + } + } + return $actions; + } + + /** + * Look up the implementation for a given API request + * + * @param $apiRequest array with keys: + * - entity: string, required + * - action: string, required + * - params: array + * - version: scalar, required + * + * @return array with keys + * - function: callback (mixed) + * - is_generic: boolean + */ + protected function resolve($apiRequest) { + $cachekey = strtolower($apiRequest['entity']) . ':' . strtolower($apiRequest['action']) . ':' . $apiRequest['version']; + if (isset($this->cache[$cachekey])) { + return $this->cache[$cachekey]; + } + + $camelName = _civicrm_api_get_camel_name($apiRequest['entity'], $apiRequest['version']); + $actionCamelName = _civicrm_api_get_camel_name($apiRequest['action']); + + // Determine if there is an entity-specific implementation of the action + $stdFunction = $this->getFunctionName($apiRequest['entity'], $apiRequest['action'], $apiRequest['version']); + if (function_exists($stdFunction)) { + // someone already loaded the appropriate file + // FIXME: This has the affect of masking bugs in load order; this is included to provide bug-compatibility + $this->cache[$cachekey] = array('function' => $stdFunction, 'is_generic' => FALSE); + return $this->cache[$cachekey]; + } + + $stdFiles = array( + // By convention, the $camelName.php is more likely to contain the function, so test it first + 'api/v' . $apiRequest['version'] . '/' . $camelName . '.php', + 'api/v' . $apiRequest['version'] . '/' . $camelName . '/' . $actionCamelName . '.php', + ); + foreach ($stdFiles as $stdFile) { + if (\CRM_Utils_File::isIncludable($stdFile)) { + require_once $stdFile; + if (function_exists($stdFunction)) { + $this->cache[$cachekey] = array('function' => $stdFunction, 'is_generic' => FALSE); + return $this->cache[$cachekey]; + } + } + } + + // Determine if there is a generic implementation of the action + require_once 'api/v3/Generic.php'; + # $genericFunction = 'civicrm_api3_generic_' . $apiRequest['action']; + $genericFunction = $this->getFunctionName('generic', $apiRequest['action'], $apiRequest['version']); + $genericFiles = array( + // By convention, the Generic.php is more likely to contain the function, so test it first + 'api/v' . $apiRequest['version'] . '/Generic.php', + 'api/v' . $apiRequest['version'] . '/Generic/' . $actionCamelName . '.php', + ); + foreach ($genericFiles as $genericFile) { + if (\CRM_Utils_File::isIncludable($genericFile)) { + require_once $genericFile; + if (function_exists($genericFunction)) { + $this->cache[$cachekey] = array('function' => $genericFunction, 'is_generic' => TRUE); + return $this->cache[$cachekey]; + } + } + } + + $this->cache[$cachekey] = array('function' => FALSE, 'is_generic' => FALSE); + return $this->cache[$cachekey]; + } + + /** + * @param string $entity + * @param string $action + * @return string + */ + protected function getFunctionName($entity, $action, $version) { + $entity = _civicrm_api_get_entity_name_from_camel($entity); + return 'civicrm_api' . $version . '_' . $entity . '_' . $action; + } + + /** + * Load/require all files related to an entity. + * + * This should not normally be called because it's does a file-system scan; it's + * only appropriate when introspection is really required (eg for "getActions"). + * + * @param string $entity + * @param int $version + * + * @return void + */ + protected function loadEntity($entity, $version) { + $camelName = _civicrm_api_get_camel_name($entity, $version); + + // Check for master entity file; to match _civicrm_api_resolve(), only load the first one + $stdFile = 'api/v' . $version . '/' . $camelName . '.php'; + if (\CRM_Utils_File::isIncludable($stdFile)) { + require_once $stdFile; + } + + // Check for standalone action files; to match _civicrm_api_resolve(), only load the first one + $loaded_files = array(); // array($relativeFilePath => TRUE) + $include_dirs = array_unique(explode(PATH_SEPARATOR, get_include_path())); + foreach ($include_dirs as $include_dir) { + $action_dir = implode(DIRECTORY_SEPARATOR, array($include_dir, 'api', "v${version}", $camelName)); + if (!is_dir($action_dir)) { + continue; + } + + $iterator = new \DirectoryIterator($action_dir); + foreach ($iterator as $fileinfo) { + $file = $fileinfo->getFilename(); + if (array_key_exists($file, $loaded_files)) { + continue; // action provided by an earlier item on include_path + } + + $parts = explode(".", $file); + if (end($parts) == "php" && !preg_match('/Tests?\.php$/', $file)) { + require_once $action_dir . DIRECTORY_SEPARATOR . $file; + $loaded_files[$file] = TRUE; + } + } + } + } + } \ No newline at end of file diff --git a/Civi/API/Subscriber/APIv3SchemaAdapter.php b/Civi/API/Subscriber/APIv3SchemaAdapter.php index a534d20836..54d5550737 100644 --- a/Civi/API/Subscriber/APIv3SchemaAdapter.php +++ b/Civi/API/Subscriber/APIv3SchemaAdapter.php @@ -45,6 +45,9 @@ class APIv3SchemaAdapter implements EventSubscriberInterface { public function onApiPrepare(\Civi\API\Event\PrepareEvent $event) { $apiRequest = $event->getApiRequest(); + if ($apiRequest['version'] > 3) { + return; + } $apiRequest['fields'] = _civicrm_api3_api_getfields($apiRequest); diff --git a/Civi/Core/Container.php b/Civi/Core/Container.php index c5ba5c7cd5..37616734c8 100644 --- a/Civi/Core/Container.php +++ b/Civi/Core/Container.php @@ -64,9 +64,14 @@ class Container { )) ->setFactoryService(self::SELF)->setFactoryMethod('createEventDispatcher'); + $container->setDefinition('magic_function_provider', new Definition( + '\Civi\API\Provider\MagicFunctionProvider', + array() + )); + $container->setDefinition('civi_api_kernel', new Definition( '\Civi\API\Kernel', - array(new Reference('dispatcher')) + array(new Reference('dispatcher'), new Reference('magic_function_provider')) )) ->setFactoryService(self::SELF)->setFactoryMethod('createApiKernel'); @@ -85,10 +90,10 @@ class Container { * @param \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher * @return \Civi\API\Kernel */ - public function createApiKernel($dispatcher) { + public function createApiKernel($dispatcher, $magicFunctionProvider) { $dispatcher->addSubscriber(new \Civi\API\Subscriber\TransactionSubscriber()); $dispatcher->addSubscriber(new \Civi\API\Subscriber\I18nSubscriber()); - $dispatcher->addSubscriber(new \Civi\API\Provider\MagicFunctionProvider()); + $dispatcher->addSubscriber($magicFunctionProvider); $dispatcher->addSubscriber(new \Civi\API\Subscriber\PermissionCheck()); $dispatcher->addSubscriber(new \Civi\API\Subscriber\APIv3SchemaAdapter()); $dispatcher->addSubscriber(new \Civi\API\Subscriber\WrapperAdapter(array( diff --git a/api/api.php b/api/api.php index 2548d072f5..f3b2be9d37 100644 --- a/api/api.php +++ b/api/api.php @@ -25,76 +25,6 @@ function civicrm_api($entity, $action, $params, $extra = NULL) { return \Civi\Core\Container::singleton()->get('civi_api_kernel')->run($entity, $action, $params, $extra); } -/** - * Look up the implementation for a given API request - * - * @param $apiRequest array with keys: - * - entity: string, required - * - action: string, required - * - params: array - * - version: scalar, required - * - * @return array with keys - * - function: callback (mixed) - * - is_generic: boolean - */ -function _civicrm_api_resolve($apiRequest) { - static $cache; - $cachekey = strtolower($apiRequest['entity']) . ':' . strtolower($apiRequest['action']) . ':' . $apiRequest['version']; - if (isset($cache[$cachekey])) { - return $cache[$cachekey]; - } - - $camelName = _civicrm_api_get_camel_name($apiRequest['entity'], $apiRequest['version']); - $actionCamelName = _civicrm_api_get_camel_name($apiRequest['action']); - - // Determine if there is an entity-specific implementation of the action - $stdFunction = civicrm_api_get_function_name($apiRequest['entity'], $apiRequest['action'], $apiRequest['version']); - if (function_exists($stdFunction)) { - // someone already loaded the appropriate file - // FIXME: This has the affect of masking bugs in load order; this is included to provide bug-compatibility - $cache[$cachekey] = array('function' => $stdFunction, 'is_generic' => FALSE); - return $cache[$cachekey]; - } - - $stdFiles = array( - // By convention, the $camelName.php is more likely to contain the function, so test it first - 'api/v' . $apiRequest['version'] . '/' . $camelName . '.php', - 'api/v' . $apiRequest['version'] . '/' . $camelName . '/' . $actionCamelName . '.php', - ); - foreach ($stdFiles as $stdFile) { - if (CRM_Utils_File::isIncludable($stdFile)) { - require_once $stdFile; - if (function_exists($stdFunction)) { - $cache[$cachekey] = array('function' => $stdFunction, 'is_generic' => FALSE); - return $cache[$cachekey]; - } - } - } - - // Determine if there is a generic implementation of the action - require_once 'api/v3/Generic.php'; - # $genericFunction = 'civicrm_api3_generic_' . $apiRequest['action']; - $genericFunction = civicrm_api_get_function_name('generic', $apiRequest['action'], $apiRequest['version']); - $genericFiles = array( - // By convention, the Generic.php is more likely to contain the function, so test it first - 'api/v' . $apiRequest['version'] . '/Generic.php', - 'api/v' . $apiRequest['version'] . '/Generic/' . $actionCamelName . '.php', - ); - foreach ($genericFiles as $genericFile) { - if (CRM_Utils_File::isIncludable($genericFile)) { - require_once $genericFile; - if (function_exists($genericFunction)) { - $cache[$cachekey] = array('function' => $genericFunction, 'is_generic' => TRUE); - return $cache[$cachekey]; - } - } - } - - $cache[$cachekey] = array('function' => FALSE, 'is_generic' => FALSE); - return $cache[$cachekey]; -} - /** * Version 3 wrapper for civicrm_api. Throws exception * @@ -145,101 +75,6 @@ function _civicrm_api3_api_getfields(&$apiRequest) { return $fields['values']; } -/** - * Load/require all files related to an entity. - * - * This should not normally be called because it's does a file-system scan; it's - * only appropriate when introspection is really required (eg for "getActions"). - * - * @param string $entity - * @param int $version - * - * @return void - */ -function _civicrm_api_loadEntity($entity, $version = 3) { - /* - $apiRequest = array(); - $apiRequest['entity'] = $entity; - $apiRequest['action'] = 'pretty sure it will never exist. Trick to [try to] force resolve to scan everywhere'; - $apiRequest['version'] = $version; - // look up function, file, is_generic - $apiRequest = _civicrm_api_resolve($apiRequest); - */ - - $camelName = _civicrm_api_get_camel_name($entity, $version); - - // Check for master entity file; to match _civicrm_api_resolve(), only load the first one - $stdFile = 'api/v' . $version . '/' . $camelName . '.php'; - if (CRM_Utils_File::isIncludable($stdFile)) { - require_once $stdFile; - } - - // Check for standalone action files; to match _civicrm_api_resolve(), only load the first one - $loaded_files = array(); // array($relativeFilePath => TRUE) - $include_dirs = array_unique(explode(PATH_SEPARATOR, get_include_path())); - foreach ($include_dirs as $include_dir) { - $action_dir = implode(DIRECTORY_SEPARATOR, array($include_dir, 'api', "v${version}", $camelName)); - if (! is_dir($action_dir)) { - continue; - } - - $iterator = new DirectoryIterator($action_dir); - foreach ($iterator as $fileinfo) { - $file = $fileinfo->getFilename(); - if (array_key_exists($file, $loaded_files)) { - continue; // action provided by an earlier item on include_path - } - - $parts = explode(".", $file); - if (end($parts) == "php" && !preg_match('/Tests?\.php$/', $file) ) { - require_once $action_dir . DIRECTORY_SEPARATOR . $file; - $loaded_files[$file] = TRUE; - } - } - } -} - -/** - * - * @deprecated - */ -function civicrm_api_get_function_name($entity, $action, $version = NULL) { - - if (empty($version)) { - $version = civicrm_get_api_version(); - } - - $entity = _civicrm_api_get_entity_name_from_camel($entity); - return 'civicrm_api3' . '_' . $entity . '_' . $action; -} - -/** - * We must be sure that every request uses only one version of the API. - * - * @param $desired_version : array or integer - * One chance to set the version number. - * After that, this version number will be used for the remaining request. - * This can either be a number, or an array(.., 'version' => $version, ..). - * This allows to directly pass the $params array. - * @return int - */ -function civicrm_get_api_version($desired_version = NULL) { - - if (is_array($desired_version)) { - // someone gave the full $params array. - $params = $desired_version; - $desired_version = empty($params['version']) ? NULL : (int) $params['version']; - } - if (isset($desired_version) && is_integer($desired_version)) { - $_version = $desired_version; - } - else { - // we will set the default to version 3 as soon as we find that it works. - $_version = 3; - } - return $_version; -} - /** * Check if the result is an error. Note that this function has been retained from * api v2 for convenience but the result is more standardised in v3 and param @@ -264,10 +99,6 @@ function civicrm_error($result) { } function _civicrm_api_get_camel_name($entity, $version = NULL) { - if (empty($version)) { - $version = civicrm_get_api_version(); - } - $fragments = explode('_', $entity); foreach ($fragments as & $fragment) { $fragment = ucfirst($fragment); diff --git a/api/v3/Generic.php b/api/v3/Generic.php index 7327d90750..cd92adbec2 100644 --- a/api/v3/Generic.php +++ b/api/v3/Generic.php @@ -105,8 +105,12 @@ function civicrm_api3_generic_getfields($apiRequest) { // find any supplemental information $hypApiRequest = array('entity' => $apiRequest['entity'], 'action' => $action, 'version' => $apiRequest['version']); - $hypApiRequest += _civicrm_api_resolve($hypApiRequest); - $helper = '_' . $hypApiRequest['function'] . '_spec'; + try { + list ($apiProvider, $hypApiRequest) = \Civi\Core\Container::singleton()->get('civi_api_kernel')->resolve($hypApiRequest); + $helper = '_' . $hypApiRequest['function'] . '_spec'; + } catch (\Civi\API\Exception\NotImplementedException $e) { + $helper = NULL; + } if (function_exists($helper)) { // alter $helper($metadata, $apiRequest); diff --git a/api/v3/Generic/Getactions.php b/api/v3/Generic/Getactions.php index 71b722c43c..a10990dc8d 100644 --- a/api/v3/Generic/Getactions.php +++ b/api/v3/Generic/Getactions.php @@ -26,26 +26,9 @@ +--------------------------------------------------------------------+ */ -function civicrm_api3_generic_getActions($params) { - civicrm_api3_verify_mandatory($params, NULL, array('entity')); - $r = civicrm_api('Entity', 'Get', array('version' => 3)); - $entity = CRM_Utils_String::munge($params['entity']); - if (!in_array($entity, $r['values'])) { - return civicrm_api3_create_error("Entity " . $entity . " invalid. Use api.entity.get to have the list", array('entity' => $r['values'])); - } - _civicrm_api_loadEntity($entity); - - $functions = get_defined_functions(); - $actions = array(); - $prefix = 'civicrm_api3_' . strtolower($entity) . '_'; - $prefixGeneric = 'civicrm_api3_generic_'; - foreach ($functions['user'] as $fct) { - if (strpos($fct, $prefix) === 0) { - $actions[] = substr($fct, strlen($prefix)); - } - elseif (strpos($fct, $prefixGeneric) === 0) { - $actions[] = substr($fct, strlen($prefixGeneric)); - } - } +function civicrm_api3_generic_getActions($apiRequest) { + civicrm_api3_verify_mandatory($apiRequest, NULL, array('entity')); + $mfp = \Civi\Core\Container::singleton()->get('magic_function_provider'); + $actions = $mfp->getActionNames($apiRequest['entity'], $apiRequest['version']); return civicrm_api3_create_success($actions); } -- 2.25.1