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)) {