Condition and event plugin lists cache in unresettable function statics, and event instantiation failures are discarded without logging

Problem/Motivation

The three ECA plugin-collection services cache their results in three different ways, and one of them discards failures without any trace. The inconsistency has two practical consequences: two of the three lists cannot be rebuilt within a request, and a failing event plugin disappears with no diagnostic at all.

The caching is inconsistent

Service Cache Resettable?
Actions::actions() $actions = &drupal_static('eca_actions'); — src/Service/Actions.php:99 yes, via drupal_static_reset('eca_actions')
Conditions::conditions() static $conditions; — src/Service/Conditions.php:99 no
Events::events() static $events; — src/Service/Events.php:64 no

A function-level static is invisible to drupal_static_reset() and to the container, so once either list is built in a request it cannot be rebuilt.

Events::events() also discards failures silently

Conditions::conditions() collects through its own createInstance() (Conditions.php:104), which logs a descriptive error when instantiation fails (Conditions.php:135-138). Actions::actions() does the same (Actions.php:113, logging at Actions.php:144-147).

Events::events() does not. It bypasses its own createInstance() — which would log (Events.php:96-112, message at :106-109) — and inlines its own handler instead:

src/Service/Events.php:69-77

try {
  /** @var \Drupal\eca\Plugin\ECA\Event\EventInterface $plugin */
  $plugin = $this->eventManager->createInstance($plugin_id);
  $events[] = $plugin;
}
// @phpstan-ignore catch.neverThrown
catch (PluginException | \Throwable) {
  // Can be ignored.
}

No exception variable, no logging, no watchdog entry. An event plugin that cannot be instantiated simply ceases to exist, with nothing anywhere to say why.

Why this matters

Diagnosability. #3590381 (closed) is a concrete example of a plugin that throws a TypeError in create() when its module's configuration is absent. For an action that produces a watchdog error; for an event it would produce absolutely nothing.

Testability. #3590382 (closed) could only work around #3590381 (closed) because Actions::actions() uses drupal_static, which a kernel test can reset after installing the required configuration. The same workaround is impossible for conditions and events. If a condition or event ever drops out for the same reason, no test can rebuild the list to see it. The testAllPluginsCanBeInstantiated() test added in #3590382 (closed) detects such a failure, but it cannot repair the enumeration the way setUp() can for actions.

The PHPStan suppressions are evidence, not noise. All three catch blocks carry @phpstan-ignore catch.neverThrown, i.e. static analysis believes nothing in there can throw. #3590381 (closed) demonstrates otherwise — a TypeError from a typed property assignment in create() is a Throwable, reaches these handlers, and is real. The suppression is currently hiding a case that actually occurs.

Steps to reproduce

The caching asymmetry:

  1. In a kernel test, call Drupal::service('eca.service.condition')->conditions().
  2. Install configuration that a condition plugin needs in order to instantiate.
  3. Call conditions() again, after drupal_static_reset() and even after rebuilding the container.

Result: the list is unchanged — the function static still holds the first result. Expected: a documented way to rebuild the list, as exists for actions.

The silent discard:

  1. Make any event plugin throw in create().
  2. Call Drupal::service('eca.service.event')->events().

Result: the plugin is missing from the list; nothing is logged. Expected: an error identifying the plugin and the reason, as actions and conditions already produce.

Proposed resolution

  1. Make the caching consistent. Use drupal_static() in Conditions::conditions() and Events::events() as Actions::actions() already does, so all three can be reset uniformly. This is a small change and makes the services testable in the same way.
  2. Make Events::events() log. Route it through its own createInstance(), which already logs and already returns NULL on failure — the same shape Conditions::conditions() uses. That removes the duplicated handler rather than adding a second one.
  3. Reconsider "Can be ignored." A plugin that cannot be instantiated is a defect somewhere; ignoring it is a reasonable resilience choice for the collection call, but it should never be an invisible one.
  4. Revisit the @phpstan-ignore catch.neverThrown suppressions once the above is done, since the premise they encode is demonstrably false.

Points 1 and 2 are small and independent. Point 3 is mostly a comment and message change. Point 4 is cleanup.

Remaining tasks

  • Convert the two function statics to drupal_static()
  • Have Events::events() collect through createInstance() so failures are logged
  • Reword or remove the "Can be ignored" comment accordingly
  • Re-evaluate the PHPStan suppressions
  • Add test coverage that a failing condition or event is reported, not silently dropped

User interface changes

None.

Data model changes

None.


Related: #3590381 (closed) is a concrete plugin that triggers this path. #3590382 (closed) adds a test that detects such failures but cannot rebuild the condition or event lists because of the caching described here. Both were found while working on #3590375 (closed).

AI-Generated: Yes (Used OpenCode to trace why a plugin was silently missing from runtime enumeration while working on #3590375 (closed) and #3590382 (closed). All file and line references were verified against the 3.1.x source.)