gplanchat/durable-phpstan

PHPStan extension for gplanchat/durable — resolves ActivityStub and ChildWorkflowStub calls from their typed contract, so a mistyped activity is an analysis error rather than a runtime failure

Maintainers

Package info

github.com/gplanchat/durable-phpstan

Type:phpstan-extension

pkg:composer/gplanchat/durable-phpstan

Transparency log

Statistics

Installs: 0

Dependents: 0

Suggesters: 1

Stars: 0

v0.1.0-alpha10 2026-08-31 16:59 UTC

This package is auto-updated.

Last update: 2026-09-01 10:00:47 UTC


README

Extension PHPStan pour gplanchat/durable. Elle résout les appels de ActivityStub, ChildWorkflowStub et NexusStub depuis leur contrat typé.

Read-only mirror. This repository is a subtree-split of gplanchat/durable-dev, published so Composer can require this package on its own. Issues and pull requests are disabled here — open them on the monorepo.

The tests are in the monorepo, not here. This split carries source only. What covers it is tests/unit/DurablePhpstan/ in the monorepo, run by its unit suite.

Documentation: durable.rocks.

Pour NexusStub, elle suit en plus l'héritage : un contrat Nexus se sépare en deux interfaces — celle que le gestionnaire implémente et celle qui l'étend pour l'appelant —, et le stub appelle les deux.

composer require --dev gplanchat/durable-phpstan

Avec phpstan/extension-installer, rien de plus. Sinon, dans votre phpstan.neon :

includes:
    - vendor/gplanchat/durable-phpstan/extension.neon

Le problème

Les stubs résolvent leurs appels par __call(). Sans extension, PHPStan ne voit que des objets sans méthode et signale tous les appels de stub — les corrects comme les fautifs :

$this->orders->charge($orderId, 100);   // sans extension : « undefined method » — faux
$this->orders->chrage($orderId, 100);   // sans extension : « undefined method » — vrai

Le défaut n'est donc pas le silence, c'est le bruit. Quatre erreurs dont deux fausses se mettent en ligne de base ou s'ignorent d'un bloc, et les deux vraies partent avec — ce qui revient au même que ne rien vérifier, en plus coûteux.

Depuis les décisions DUR038 et DUR039, le stub typé est la seule façon de planifier une activité ou un workflow enfant. Cette vérification-là est donc la seule qui reste.

Ce que l'extension apporte

Sur la même fixture, mesuré :

sans extension avec
charge() — appel correct ✗ signalé à tort
run() — enfant, appel correct ✗ signalé à tort
chrage() — faute de frappe
helper() — sans #[ActivityMethod]
charge($id) — un argument sur deux invisible arité vérifiée

La dernière ligne est le gain que le bruit masquait : une fois la méthode connue, PHPStan compare les arguments à ce que le contrat déclare.

Ce qu'elle exige de votre code

Le stub porte son contrat en paramètre générique, et PHPStan l'infère depuis WorkflowEnvironment::activityStub(). Encore faut-il qu'il puisse le suivre jusqu'au point d'appel — ce qui est le cas dès que la propriété est readonly et affectée une seule fois, au constructeur :

private readonly ActivityStub $orders;   // suffit

public function __construct(WorkflowEnvironment $environment)
{
    $this->orders = $environment->activityStub(OrderActivities::class);
}

Une propriété mutable perd le paramètre entre le constructeur et la méthode. Annotez-la alors explicitement :

/** @var ActivityStub<OrderActivities> */
private ActivityStub $orders;

Dans les deux cas, si le contrat reste introuvable, l'appel est simplement inconnu de PHPStan plutôt qu'accepté à l'aveugle : mieux vaut un faux positif qu'une vérification silencieusement désactivée.

Ce qu'elle ne fait pas

Une méthode absente du contrat, ou présente mais sans #[ActivityMethod] — respectivement #[WorkflowMethod] pour un enfant — reste inconnue. C'est voulu : le stub la refuse déjà à l'exécution avec un BadMethodCallException, et l'analyse le dit désormais avant.

Licence

MIT.