Skip to content

M14, M15, M19 — declare and check what the bundle actually uses - #277

Open
gplanchat wants to merge 1 commit into
mainfrom
fix/bundle-declare-ce-qu-il-utilise
Open

M14, M15, M19 — declare and check what the bundle actually uses#277
gplanchat wants to merge 1 commit into
mainfrom
fix/bundle-declare-ce-qu-il-utilise

Conversation

@gplanchat

@gplanchat gplanchat commented Sep 3, 2026

Copy link
Copy Markdown
Owner

First batch of the bundle's major findings. Three of them, one theme: the bundle accepts what it cannot honour, and nothing says so before the incident.

M15 — the configured cache pool was silently discarded

$cacheRef = null !== $cacheId && $container->hasDefinition($cacheId) ? new Reference($cacheId) : null;

hasDefinition() answers false for an alias — and Psr\Cache\CacheItemPoolInterface is one — as it does for a definition placed by an extension that runs after this one. The operator wrote activity_contracts.cache: my.pool, nothing was wired, and nothing reported it.

The reference is now placed unconditionally. A non-existent pool becomes a compile-time error, which the container knows how to name — this is the upstream convention: pass a Reference and let CheckExceptionOnInvalidReferenceBehaviorPass decide.

And its second half, which the report did not separate

The pool defaults to null. So the cache warmer warmed nothing, and above all: every activity call redid the reflection on its contract. These metadata derive from attributes, hence from code — they cannot change while the process lives.

A per-instance memo now serves them, and short-circuits the pool when there is one: on Redis, the previous code made one network round trip per activity call for immutable data. The test measures it against a spy pool — two resolutions, a single getItem.

M19 — a missing lock.factory did not say what to configure

Contrary to what the report implied, this was not silent: the container already failed. But on a "non-existent service" naming lock.factory and leaving you to search, when what you need to know is which configuration section would have placed it, and why that section is not optional here.

RequireLockFactoryPass says so, and names both ways out: framework.lock, or your own factory in durable.dbal.lock_factory. With the reason: without a lock, two workers replay the same journal at the same time.

In a compiler pass rather than in the extension, for the same reason as the pool above: when extensions load, the one that places lock.factory may not have run, and an existence test would answer false for a correctly configured application.

M14 — two bridges hard-wired, declared nowhere

The extension imports sixteen classes from durable-bridge-temporal and seven from durable-bridge-dbal. A bare composer require gplanchat/durable-bundle gave you a container that compiles and a "class not found" fatal on the first call. Both enter suggest.

One existing test changed, and why

DurableMiddlewareReachesTheBusTest built a container with the DBAL lock and without a lock factory. The new pass refuses it — rightly: an application in that state would not boot either. The test's container therefore places lock.factory, exactly as it already placed messenger.bus.default to stand in for what FrameworkExtension would have done.

It is the only existing test touched, and it is touched because it described an impossible container.

Verification

main this branch
unit suite 1073 tests, 4 errors 1081 tests, 4 errors
PHPStan 2 errors 2 errors

Pre-existing and identical errors (illuminate/cache missing on this machine). No new diagnostic.

What this batch does not do

  • M20 — reworking the 819-line procedural extension into config/services.php + AbstractBundle. That is a change in its own right, not a fix.
  • M17 (#[AsWorkflow] not autoconfigured, issue [bundle] #[AsWorkflow] is not autoconfigured: WorkflowPass finds nothing unless the class is tagged by hand #255) — the obvious fix breaks compilation, because a workflow is instantiated by reflection with a WorkflowEnvironment that is not a service. It deserves its own pass.
  • The cache warmer itself. It stays, and stays debatable: it writes into a runtime pool with a one-hour TTL instead of the build directory, and the activity_contracts.contracts list re-enumerates by hand what the tags already carry. With the memo added here, its value shrinks further. Removing it is a public API break: to be decided, not slipped into a fix.

Trois formes du même défaut : le bundle accepte ce qu'il ne peut pas tenir, et rien ne
le dit avant l'incident.

**Le pool de cache configuré était jeté en silence.** `hasDefinition($cacheId)` répond
faux pour un alias — et `Psr\Cache\CacheItemPoolInterface` en est un — comme pour une
définition posée par une extension qui tourne après celle-ci. L'exploitant écrivait
`activity_contracts.cache: mon.pool`, rien n'était câblé, rien ne le signalait. La
référence est désormais posée sans condition : un pool qui n'existe pas devient une
erreur de compilation, que le conteneur sait nommer.

**Et le résolveur re-réfléchissait à chaque appel.** Le pool vaut `null` par défaut, donc
le cache warmer ne réchauffait rien et chaque appel d'activité refaisait la réflexion sur
le contrat. Ces métadonnées dérivent des attributs, donc du code : elles ne peuvent pas
changer tant que le processus vit. Une mémoire par instance les sert, et court-circuite
aussi le pool — sur un Redis, c'était un aller-retour réseau par appel d'activité.

**`lock.factory` manquant ne disait pas quoi configurer.** Le conteneur échouait déjà,
sur un « service inexistant » qui nomme `lock.factory` et laisse chercher. Une passe le
dit : quelle section pose ce service, et pourquoi elle n'est pas optionnelle ici — sans
verrou, deux workers rejouent le même journal en même temps. Vérifié dans une passe et
non dans l'extension : au chargement des extensions, celle qui pose `lock.factory` n'a
pas forcément tourné.

**Les deux ponts entrent dans `suggest`.** L'extension importe seize classes du pont
Temporal et sept du pont DBAL ; un `composer require` du seul bundle donnait un conteneur
qui compile et un fatal au premier appel.

`DurableMiddlewareReachesTheBusTest` montait un conteneur avec le verrou DBAL et sans
fabrique. La passe le refuse maintenant, à raison : une application dans cet état ne
démarrerait pas. Le conteneur du test pose donc `lock.factory`, comme il posait déjà
`messenger.bus.default` pour la même raison.

Suite unit : 1081 tests contre 1073 sur main, mêmes 4 erreurs d'environnement. PHPStan :
2 erreurs, la base de main.

Refs: M14, M15 et M19 de documentation/audit/

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gplanchat gplanchat changed the title M14, M15, M19 — déclarer et vérifier ce que le bundle utilise réellement M14, M15, M19 — declare and check what the bundle actually uses Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant