From bdd65b5c6b6ab214322be37f433bd6090622aa54 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Thu, 13 Apr 2017 16:51:37 -0700 Subject: [PATCH] CRM_Utils_Hook_Inspector - Add util for displaying hook metadata This is meant to facilitate better code-generators and documentation-generators. Key features: * Adds a new class, `CRM_Utils_Hook_Inspector`. * Adds a new hook, `hook_civicrm_hooks`. Extensions may use this to document their hooks. * Implements the hook in core (to report on `civicrm-core` hooks) * If you follow the practice of declaring static hook stubs (like `civicrm-core` does), then use `addStaticStubs()` to assimilate of them. --- CRM/Utils/Hook.php | 14 ++ CRM/Utils/Hook/Inspector.php | 171 ++++++++++++++++++ Civi/Core/Container.php | 1 + .../phpunit/CRM/Utils/Hook/InspectorTest.php | 32 ++++ 4 files changed, 218 insertions(+) create mode 100644 CRM/Utils/Hook/Inspector.php create mode 100644 tests/phpunit/CRM/Utils/Hook/InspectorTest.php diff --git a/CRM/Utils/Hook.php b/CRM/Utils/Hook.php index e09561a013..d552d1dd5c 100644 --- a/CRM/Utils/Hook.php +++ b/CRM/Utils/Hook.php @@ -1841,6 +1841,20 @@ abstract class CRM_Utils_Hook { ); } + /** + * Build a description of available hooks. + * + * @param CRM_Utils_Hook_Inspector $inspector + * @return null + */ + public static function hooks($inspector) { + return self::singleton()->invoke(array('inspector'), $inspector, self::$_nullObject, self::$_nullObject, + self::$_nullObject, self::$_nullObject, self::$_nullObject, + 'civicrm_hooks' + ); + } + + /** * This hook is called while preparing a profile form. * diff --git a/CRM/Utils/Hook/Inspector.php b/CRM/Utils/Hook/Inspector.php new file mode 100644 index 0000000000..8440be8349 --- /dev/null +++ b/CRM/Utils/Hook/Inspector.php @@ -0,0 +1,171 @@ +getHooks())); + * @endCode + * + * Note: The inspector is only designed for use in developer workflows, such + * as code-generation and inspection. It should be not called by regular + * runtime logic. + */ +class CRM_Utils_Hook_Inspector { + + /** + * Register the default hooks defined by 'CRM_Utils_Hook'. + * + * @param \Civi\Core\Event\GenericHookEvent $e + * @see CRM_Utils_Hook::hooks() + */ + public static function findBuiltInHooks(\Civi\Core\Event\GenericHookEvent $e) { + $skipList = array('singleton'); + $e->inspector->addStaticStubs('CRM_Utils_Hook', 'hook_civicrm_', + function ($hook, $method) use ($skipList) { + return in_array($method->name, $skipList) ? NULL : $hook; + }); + } + + /** + * @var array + * Array(string $name => array $hookDef). + * + * Ex: $hooks['hook_civicrm_foo']['description_html'] = 'Hello world'; + */ + protected $hooks; + + /** + * Perform a scan to identify/describe all hooks. + * + * @param bool $force + * @return CRM_Utils_Hook_Inspector + */ + public function build($force = FALSE) { + if ($force || $this->hooks === NULL) { + $this->hooks = array(); + CRM_Utils_Hook::hooks($this); + ksort($this->hooks); + } + return $this; + } + + /** + * Get a list of all hooks. + * + * @return array + * Array(string $name => array $hookDef). + * Ex: $hooks['hook_civicrm_foo']['description_html'] = 'Hello world'; + */ + public function getAll() { + $this->build(); + return $this->hooks; + } + + /** + * Get the definition of one hook. + * + * @param string $name + * Ex: 'hook_civicrm_alterSettingsMetaData'. + * @return array + * Ex: $hook['description_html'] = 'Hello world'; + */ + public function get($name) { + $this->build(); + return $this->hooks[$name]; + } + + /** + * @param $hook + * @return bool + * TRUE if valid. + */ + public function validate($hook) { + return + is_array($hook) + && !empty($hook['name']) + && isset($hook['signature']) + && is_array($hook['fields']); + } + + /** + * Add a new hook definition. + * + * @param array $hook + * @return CRM_Utils_Hook_Inspector + */ + public function add($hook) { + $name = isset($hook['name']) ? $hook['name'] : NULL; + + if (empty($hook['signature'])) { + $hook['signature'] = implode(', ', array_map( + function ($field) { + $sigil = $field['ref'] ? '&$' : '$'; + return $sigil . $field['name']; + }, + $hook['fields'] + )); + } + + if (TRUE !== $this->validate($hook)) { + throw new CRM_Core_Exception("Failed to register hook ($name). Invalid definition."); + } + + $this->hooks[$name] = $hook; + return $this; + } + + /** + * Scan a class for hook stubs, and add all of them. + * + * @param string $className + * The name of a class which contains static stub functions. + * Ex: 'CRM_Utils_Hook'. + * @param string $prefix + * A prefix to apply to all hook names. + * Ex: 'hook_civicrm_'. + * @param null|callable $filter + * An optional function to filter/rewrite the metadata for each hook. + * @return CRM_Utils_Hook_Inspector + */ + public function addStaticStubs($className, $prefix = 'hook_', $filter = NULL) { + $class = new ReflectionClass($className); + + foreach ($class->getMethods(ReflectionMethod::IS_STATIC) as $method) { + if (!isset($method->name)) { + continue; + } + + $hook = array( + 'name' => $prefix . $method->name, + 'description_html' => $method->getDocComment() ? CRM_Admin_Page_APIExplorer::formatDocBlock($method->getDocComment()) : '', + 'fields' => array(), + 'class' => 'Civi\Core\Event\GenericHookEvent', + ); + + foreach ($method->getParameters() as $parameter) { + $hook['fields'][$parameter->getName()] = array( + 'name' => $parameter->getName(), + 'ref' => (bool) $parameter->isPassedByReference(), + // WISHLIST: 'type' => 'mixed', + ); + } + + if ($filter !== NULL) { + $hook = $filter($hook, $method); + if ($hook === NULL) { + continue; + } + } + + $this->add($hook); + } + + return $this; + } + +} diff --git a/Civi/Core/Container.php b/Civi/Core/Container.php index 8693d24272..d428e3daac 100644 --- a/Civi/Core/Container.php +++ b/Civi/Core/Container.php @@ -250,6 +250,7 @@ class Container { $dispatcher->addListener('hook_civicrm_post::Case', array('\Civi\CCase\Events', 'fireCaseChange')); $dispatcher->addListener('hook_civicrm_caseChange', array('\Civi\CCase\Events', 'delegateToXmlListeners')); $dispatcher->addListener('hook_civicrm_caseChange', array('\Civi\CCase\SequenceListener', 'onCaseChange_static')); + $dispatcher->addListener('hook_civicrm_hooks', array('CRM_Utils_Hook_Inspector', 'findBuiltInHooks')); $dispatcher->addListener('civi.dao.postInsert', array('\CRM_Core_BAO_RecurringEntity', 'triggerInsert')); $dispatcher->addListener('civi.dao.postUpdate', array('\CRM_Core_BAO_RecurringEntity', 'triggerUpdate')); $dispatcher->addListener('civi.dao.postDelete', array('\CRM_Core_BAO_RecurringEntity', 'triggerDelete')); diff --git a/tests/phpunit/CRM/Utils/Hook/InspectorTest.php b/tests/phpunit/CRM/Utils/Hook/InspectorTest.php new file mode 100644 index 0000000000..14e67f2764 --- /dev/null +++ b/tests/phpunit/CRM/Utils/Hook/InspectorTest.php @@ -0,0 +1,32 @@ +get('hook_civicrm_alterSettingsMetaData'); + $this->assertEquals('hook_civicrm_alterSettingsMetaData', $hook['name']); + $this->assertEquals(array('settingsMetaData', 'domainID', 'profile'), array_keys($hook['fields'])); + $this->assertTrue($hook['fields']['settingsMetaData']['ref']); + $this->assertFalse($hook['fields']['domainID']['ref']); + $this->assertEquals('&$settingsMetaData, $domainID, $profile', $hook['signature']); + $this->assertTrue($inspector->validate($hook)); + } + + public function testGetAll() { + $inspector = new CRM_Utils_Hook_Inspector(); + $all = $inspector->getAll(); + $this->assertTrue(count($all) > 1); + $this->assertTrue(isset($all['hook_civicrm_alterSettingsMetaData'])); + foreach ($all as $name => $hook) { + $this->assertEquals($name, $hook['name']); + $this->assertNotEmpty($hook['description_html']); + $this->assertTrue($inspector->validate($hook)); + } + } + +} -- 2.25.1