Skip to content

Domain entity references in activity arguments: API and failing tests (#996) - #1126

Draft
rmcdaniel wants to merge 2 commits into
mainfrom
feature/996-entity-references
Draft

rmcdaniel wants to merge 2 commits into
mainfrom
feature/996-entity-references

Conversation

@rmcdaniel

Copy link
Copy Markdown
Member

The next step on #996 (chrom's request, design accepted on 2026-11-03/04): a draft with the adapter/resolver API and failing tests. Implementation follows on this PR.

API (experimental, Workflow\V2\EntityReferences)

  • EntityReferenceAdapter: supports(object), identify(object): string, version(): int. Registered per logical type. The domain class implements nothing.
  • EntityReferenceResolver: resolve(EntityReference, EntityReferenceContext): object. It runs before each attempt with trusted context (namespace, run, activity, attempt, argument position, declared parameter type). A tenant inside a reference is never trusted on its own.
  • EntityReference {type, id, version} is an ordinary Avro map, so the value schema is unchanged and arguments without references stay byte-identical.
  • Failures:
    • EntityReferenceRefused (unknown type, unsupported version, parameter mismatch) is non-retryable and raised before the activity runs;
    • resolvers throw EntityNotFound and EntityAccessDenied, which follow the activity's retry policy;
    • other resolver exceptions also follow the retry policy.
  • Config: workflows.v2.entity_references maps 'orders.order' => ['adapter' => …, 'resolver' => …].

Behavior pinned by V2EntityReferenceTest (4 fail until implemented)

  1. The workflow passes new Order(…). History stores [{type: orders.order, id, version: 1}, 'checked'] plus entity_reference_positions: [0] on the execution and on ActivityScheduled. The activity receives the current persisted Order in its typed parameter.
  2. Replay (WorkflowReplayer) never calls the resolver.
  3. A missing entity fails the attempt with EntityNotFound.
  4. A recorded type the worker no longer knows is refused non-retryably before the activity runs, with no repository load.
  5. An unregistered object still fails, with no PHP object serialization (this passes today).

Still to add with the implementation: ordinary arrays that match the shape are never resolved; a version mismatch; a parameter type mismatch; per-attempt current-state reload on retry; a local-activity path; docs.

Scope (from the decision)

In scope: top-level activity arguments for in-process V2 workers (queue and local activities). Out of scope: nested references, collections, results, workflow arguments, and other SDKs.

🤖 Generated with Claude Code

)

Experimental API under Workflow\V2\EntityReferences, following the decisions on #996: EntityReferenceAdapter (supports/identify/version), EntityReferenceResolver (resolve with a trusted EntityReferenceContext), the portable EntityReference {type, id, version}, EntityNotFound / EntityAccessDenied for resolvers, and the non-retryable EntityReferenceRefused (unknown type, unsupported version, parameter mismatch). Registration is workflows.v2.entity_references by logical type.

V2EntityReferenceTest pins the accepted behavior with a plain Order entity: the activity receives the current persisted entity, history records {type, id, version} plus entity_reference_positions, replay never resolves, a missing entity fails with EntityNotFound, an unknown recorded type is refused non-retryably before the activity runs, and an unregistered object still fails. The first four fail until the implementation lands; this is a draft.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rmcdaniel

Copy link
Copy Markdown
Member Author

Workflow owns the implementation on this draft. The failing behavior tests are intentional API-first evidence, not a finished implementation or a merge candidate. I reviewed the accepted scope and current head e0fb9505 while reconciling the queue.

Next source action after the active #863 Security corrections: encode registered top-level arguments as portable references with recorded positions, then resolve only at real queue/local activity attempts using trusted context. Plain maps, retry reload, resolver failures, version/parameter mismatch and resolver-free replay remain required before this leaves draft. No contributor implementation or feedback is required to start that work.

@rmcdaniel

Copy link
Copy Markdown
Member Author

An implementation-preparation fix is now on this draft: EntityReference::fromArray() accepts the exact {type, id, version} key set independently of map insertion order. The earlier array_keys() comparison incorrectly refused a valid portable map whose order changed during serialization or storage.

Seven focused controls pass (10 assertions): reordered valid keys, missing/extra/substituted fields, a string version, empty identifier and non-positive version. ECS and static analysis pass on the changed files. The runtime adapter/resolver behavior remains unimplemented and the feature tests remain the existing API-first draft work; this is not a merge or delivery claim.

The runtime implementation also needs to preserve Laravel dependency injection around referenced arguments and keep resolver calls outside replay. Those remain part of the upcoming queue/local activity slice.

@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant