diff --git a/tools/extensions/phpstorm/Civi/PhpStorm/Api3Generator.php b/tools/extensions/phpstorm/Civi/PhpStorm/Api3Generator.php new file mode 100644 index 000000000000..777e3d19892e --- /dev/null +++ b/tools/extensions/phpstorm/Civi/PhpStorm/Api3Generator.php @@ -0,0 +1,43 @@ + 'generate', + ]; + } + + public function generate() { + /* + * FIXME: PHPSTORM_META doesn't seem to support compound dynamic arguments + * so even if you give it separate lists like + * ``` + * expectedArguments(\civicrm_api4('Contact'), 1, 'a', 'b'); + * expectedArguments(\civicrm_api4('Case'), 1, 'c', 'd'); + * ``` + * It doesn't differentiate them and always offers a,b,c,d for every entity. + * If they ever fix that upstream we could fetch a different list of actions per entity, + * but for now there's no point. + */ + + $entities = \civicrm_api3('entity', 'get', []); + $actions = ['create', 'delete', 'get', 'getactions', 'getcount', 'getfield', 'getfields', 'getlist', 'getoptions', 'getrefcount', 'getsingle', 'getunique', 'getvalue', 'replace', 'validate']; + + $builder = new PhpStormMetadata('api3', __CLASS__); + $builder->registerArgumentsSet('api3Entities', ...$entities['values']); + $builder->registerArgumentsSet('api3Actions', ...$actions); + $builder->addExpectedArguments('\civicrm_api3()', 0, 'api3Entities'); + $builder->addExpectedArguments('\civicrm_api3()', 1, 'api3Actions'); + $builder->write(); + } + +} diff --git a/tools/extensions/phpstorm/Civi/PhpStorm/Api4Generator.php b/tools/extensions/phpstorm/Civi/PhpStorm/Api4Generator.php new file mode 100644 index 000000000000..62a6f6598db9 --- /dev/null +++ b/tools/extensions/phpstorm/Civi/PhpStorm/Api4Generator.php @@ -0,0 +1,44 @@ + 'generate', + 'hook_civicrm_post::CustomGroup' => 'generate', + ]; + } + + public function generate() { + /* + * FIXME: PHPSTORM_META doesn't seem to support compound dynamic arguments + * so even if you give it separate lists like + * ``` + * expectedArguments(\civicrm_api4('Contact'), 1, 'a', 'b'); + * expectedArguments(\civicrm_api4('Case'), 1, 'c', 'd'); + * ``` + * It doesn't differentiate them and always offers a,b,c,d for every entity. + * If they ever fix that upstream we could fetch a different list of actions per entity, + * but for now there's no point. + */ + + $entities = \Civi\Api4\Entity::get(FALSE)->addSelect('name')->execute()->column('name'); + $actions = ['get', 'save', 'create', 'update', 'delete', 'replace', 'revert', 'export', 'autocomplete', 'getFields', 'getActions', 'checkAccess']; + + $builder = new PhpStormMetadata('api4', __CLASS__); + $builder->registerArgumentsSet('api4Entities', ...$entities); + $builder->registerArgumentsSet('api4Actions', ...$actions); + $builder->addExpectedArguments('\civicrm_api4()', 0, 'api4Entities'); + $builder->addExpectedArguments('\civicrm_api4()', 1, 'api4Actions'); + $builder->write(); + } + +} diff --git a/tools/extensions/phpstorm/Civi/PhpStorm/EventGenerator.php b/tools/extensions/phpstorm/Civi/PhpStorm/EventGenerator.php new file mode 100644 index 000000000000..81202ff2ffcb --- /dev/null +++ b/tools/extensions/phpstorm/Civi/PhpStorm/EventGenerator.php @@ -0,0 +1,50 @@ + 'generate', + ]; + } + + public function generate() { + $inspector = new CiviEventInspector(); + + $entities = \Civi\Api4\Entity::get(FALSE)->addSelect('name')->execute()->column('name'); + $specialEvents = ['hook_civicrm_post', 'hook_civicrm_pre', 'civi.api4.validate']; + foreach ($entities as $entity) { + foreach ($specialEvents as $specialEvent) { + $entityEvents [] = "$specialEvent::$entity"; + } + } + // PHP 7.4 can simplify: + // $entityEvents = array_map(fn($pair) => implode('::', $pair), \CRM_Utils_Array::product([$entities, $specialEvents])); + + + $all = array_merge(array_keys($inspector->getAll()), $entityEvents); + + $builder = new PhpStormMetadata('events', __CLASS__); + $builder->registerArgumentsSet('events', ...$all); + + foreach ([CiviEventDispatcher::class, CiviEventDispatcherInterface::class] as $class) { + foreach (['dispatch', 'addListener', 'removeListener', 'getListeners', 'hasListeners'] as $method) { + $builder->addExpectedArguments(sprintf("\\%s::%s()", $class, $method), 0, 'events'); + } + } + + $builder->write(); + } + +} diff --git a/tools/extensions/phpstorm/Civi/PhpStorm/PhpStormMetadata.php b/tools/extensions/phpstorm/Civi/PhpStorm/PhpStormMetadata.php index 854e8643eff0..260f0ebd2e4d 100644 --- a/tools/extensions/phpstorm/Civi/PhpStorm/PhpStormMetadata.php +++ b/tools/extensions/phpstorm/Civi/PhpStorm/PhpStormMetadata.php @@ -44,6 +44,30 @@ public function __construct(string $name, string $attribution) { $this->buffer = ''; } + public function registerArgumentsSet(string $name, ...$args) { + $escapedName = var_export($name, 1); + $escapedArgs = implode(', ', array_map(function($arg) { + return var_export($arg, 1); + }, $args)); + $this->buffer .= "registerArgumentsSet($escapedName, $escapedArgs);\n"; + return $this; + } + + /** + * @param string $for + * Ex: '\Civi\Core\SettingsBag::get()' + * @param int $index + * The positional offset among the arguments + * @param string $argumentSet + * Name of the argument set. (This should already be defined by `registerArgumentsSet()`.) + * @return $this + */ + public function addExpectedArguments(string $for, int $index, string $argumentSet) { + $escapedSet = var_export($argumentSet, 1); + $this->buffer .= "expectedArguments($for, $index, argumentsSet($escapedSet));\n"; + return $this; + } + /** * @param string $for * @param array $map diff --git a/tools/extensions/phpstorm/Civi/PhpStorm/SettingsGenerator.php b/tools/extensions/phpstorm/Civi/PhpStorm/SettingsGenerator.php new file mode 100644 index 000000000000..fde4baa2be12 --- /dev/null +++ b/tools/extensions/phpstorm/Civi/PhpStorm/SettingsGenerator.php @@ -0,0 +1,28 @@ + 'generate']; + } + + public function generate() { + $metadata = \Civi\Core\SettingsMetadata::getMetadata(); + $methods = ['get', 'getDefault', 'getExplicit', 'getMandatory', 'hasExplicit', 'revert', 'set']; + $builder = new PhpStormMetadata('settings', __CLASS__); + $builder->registerArgumentsSet('settingNames', ...array_keys($metadata)); + foreach ($methods as $method) { + $builder->addExpectedArguments('\Civi\Core\SettingsBag::' . $method . '()', 0, 'settingNames'); + } + $builder->write(); + } + +} diff --git a/tools/extensions/phpstorm/info.xml b/tools/extensions/phpstorm/info.xml index dc9f4ec61010..d1b1d4efd158 100644 --- a/tools/extensions/phpstorm/info.xml +++ b/tools/extensions/phpstorm/info.xml @@ -34,5 +34,6 @@ mgd-php@1.0.0 setting-php@1.0.0 smarty-v2@1.0.1 + scan-classes@1.0.0 diff --git a/tools/extensions/phpstorm/phpstorm.php b/tools/extensions/phpstorm/phpstorm.php index 320245c99cd2..fa27ce20e058 100644 --- a/tools/extensions/phpstorm/phpstorm.php +++ b/tools/extensions/phpstorm/phpstorm.php @@ -27,13 +27,22 @@ function phpstorm_metadata_dir(): string { * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_config/ */ function phpstorm_civicrm_config(&$config): void { - _phpstorm_civix_civicrm_config($config); + _phpstorm_civix_civicrm_config($config); } function phpstorm_civicrm_container(\Symfony\Component\DependencyInjection\ContainerBuilder $container) { $container->addCompilerPass(new \Civi\PhpStorm\PhpStormCompilePass(), PassConfig::TYPE_AFTER_REMOVING, 2000); } +function phpstorm_civicrm_managed(&$entities, $modules) { + // We don't currently have an event for extensions to join the "system flush" operation. Apply Skullduggery method. + // This gives a useful baseline event for most generators -- but it's _not_ for "services", and each generator may be supplemented + // by other events. + if ($modules === NULL && !defined('CIVICRM_TEST')) { + Civi::dispatcher()->dispatch('civi.phpstorm.flush'); + } +} + function phpstorm_civicrm_uninstall() { $dir = phpstorm_metadata_dir(); if (file_exists($dir)) {