feat(workflow): the workflow method receives its stubs and its environment as arguments (#419) - #423
Merged
Merged
Conversation
…nment as arguments (#419) A parameter typed WorkflowEnvironment receives the environment, and an ActivityStub parameter marked #[Activities(Contract::class)] receives $env->activityStub(Contract::class). The loader reads the signature once, in load(), and a replay only runs that plan. Injected parameters are not input: the whole-input form run(array $input) now counts input parameters only, so an injected environment beside it keeps the whole input. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…istration (#419) load() refuses, with the class, method and parameter in the message: - an ActivityStub parameter without #[Activities], - #[Activities] on a parameter not typed ActivityStub, - a contract that does not exist or declares no #[AsActivityMethod]. #[AsActivity] is optional on a contract, so it is not the criterion. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…f the signature (#419) WorkflowDefinitionLoader::inputParameters() is the one filter: - workflowMethodParameters() no longer lists injected parameters, so the Nexus payload names stay the input names; - ChildWorkflowStub maps a parent's arguments onto the input parameters only, through a new optional parameter list on StubArguments::toPayload(); - the PHPStan extension exposes the same list on ChildWorkflowStub, so $child->run('Ada') is not reported as missing arguments. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…lation (#419) WorkflowPass loads each tagged class once at compile time. The registry still loads them when it is built; the pass only moves the refusal from the first worker to the deploy. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…me two contracts (#419) The loader reads the attribute, PHPStan reads the docblock: no extension point types a parameter from an attribute. ActivitiesParameterRule reports a parameter whose two statements disagree (durable.activities.contractMismatch), registered in extension.neon. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rted once, where it is fixed (#419) Without @PARAM ActivityStub<T>, every call on the stub is already "undefined method". The rule reports the cause on the parameter, with the line to add as a tip. PHPStan has no non-failing level, so it is an error with its own identifier (durable.activities.missingGeneric) that a project can ignore. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rguments (#419) The getting-started guide shows the argument form; the workflows page explains what Durable supplies, what stays input, the PHPStan docblock, why ActivityOptions still go through activityStub(), and why the constructor form remains the one for classes implementing a contract interface. The durable-phpstan README documents the two identifiers. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…als are pinned (#419) Planning the arguments once had captured each input default at load(), so a `new` initializer was shared by every execution of a long-lived worker. The default is evaluated inside the plan again. Also pinned by tests: a contract that does not exist is refused at registration, and a Nexus-fulfilling workflow with injected parameters matches the operation on its input parameters only. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ad as the loader reads them (#419) - @PARAM ActivityStub<T>|null is the natural docblock of ?ActivityStub; the rule strips null before reading the generic, instead of reporting a missing one. - #[Activities('Fqcn')] is accepted by the loader; the rule now reads the string form too, so a disagreeing docblock no longer goes unreported. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ers (#419) SchedulingMethodReflection serves the activity, Nexus and child stubs alike. An activity method taking a WorkflowEnvironment still needs it from the caller, so the filter now applies to #[AsWorkflowMethod] only. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ntifier are complete (#419) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
gplanchat
added a commit
that referenced
this pull request
Sep 23, 2026
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
gplanchat
enabled auto-merge
September 23, 2026 18:34
gplanchat
added a commit
that referenced
this pull request
Sep 23, 2026
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #419.
A workflow method receives its activity stubs and its environment as arguments, the way a controller receives its services:
What changes
WorkflowDefinitionLoaderload():WorkflowEnvironmentby type,#[Activities]stubs through$env->activityStub(), everything else from the input by name. A replay only runs the plan.run(array $input)counts input parameters only.WorkflowDefinitionLoaderload(): anActivityStubwithout#[Activities],#[Activities]on another type, a contract that does not exist or declares no#[AsActivityMethod].inputParameters()workflowMethodParameters()(Nexus payload names),ChildWorkflowStub(through a new optional parameter list onStubArguments::toPayload()) and the PHPStan extension.WorkflowPass(bundle)durable-phpstanActivitiesParameterRule:durable.activities.contractMismatchwhen the@param ActivityStub<T>names another contract than the attribute,durable.activities.missingGenericwhen it is missing.SchedulingMethodReflectiondrops injected parameters from a child workflow's entry method, and only there.Constructor injection keeps working. It stays the form for a class implementing a contract interface: PHP does not let the implementation add required parameters to
run().Deviations from the first version of #419
Recorded in the issue:
#[AsActivity]is optional on a contract, so validation checks for at least one#[AsActivityMethod]instead.options:on the attribute:DurationandRetryLimithave private constructors, which attribute arguments cannot reach. A stub with options keeps$env->activityStub($contract, $options).cache:warmup.Review
A fresh-context review found no blocker. Its findings are fixed in the last four commits:
ActivityStub<T>|nulland#[Activities('Fqcn')];Checks
tests/unit/Durable,tests/unit/DurablePhpstan,tests/unit/DurableBundle/DependencyInjection: 880 tests, green. Every behaviour change arrived with its failing test first.loop/guardrails/verify.shis red locally for a reason outside this diff. The localvendor/lacksdoctrine/ormandilluminate/cache, which breaks the Laravel, Illuminate and SchemaListener tests and five PHPStan findings in files this PR does not touch. The same failures occur onmain. CI installs from the lock and is the reference.