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:
- In a kernel test, call
Drupal::service('eca.service.condition')->conditions(). - Install configuration that a condition plugin needs in order to instantiate.
- Call
conditions()again, afterdrupal_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:
- Make any event plugin throw in
create(). - 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
- Make the caching consistent. Use
drupal_static()inConditions::conditions()andEvents::events()asActions::actions()already does, so all three can be reset uniformly. This is a small change and makes the services testable in the same way. - Make
Events::events()log. Route it through its owncreateInstance(), which already logs and already returnsNULLon failure — the same shapeConditions::conditions()uses. That removes the duplicated handler rather than adding a second one. - 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.
- Revisit the
@phpstan-ignore catch.neverThrownsuppressions 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 throughcreateInstance()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.)