wesnick/api-platform-workflow-extension
Integrates Symfony Workflow with API Platform by exposing workflow transitions as REST operations, letting clients move resources through workflow states via configurable endpoints with guard and access-control support.
This bundle wires the Symfony Workflow component into API Platform so that workflow transitions can be exposed as API operations instead of hand-rolling a controller for every state change. It is archived, so treat this as a reference implementation to copy or fork rather than a long-term dependency. Before committing, open composer.json in the repo and confirm the constraint it pins (api-platform/core, symfony/workflow, PHP). If it targets API Platform 2.x, plan for the upgrade cost before you adopt it.
Minimal steps:
Install and register.
composer require wesnick/api-platform-workflow-extension
Add the bundle to config/bundles.php (or AppKernel) if the package does not use Symfony Flex auto-discovery. If there is no tagged release, pin a commit hash — never dev-master on an archived repo.
Look here first, in this order:
README.md for the intended annotation/attribute and request shape.src/Resources/config/services.yaml (or services.xml) to see what is registered and under which tags — this tells you exactly which API Platform extension point is being used (metadata factory decorator, operation, or event subscriber).src/ — this is the code you will most likely need to patch for API Platform 3.x.tests/ for the canonical request/response example.First use case — transition one entity through one workflow. Configure a normal Symfony workflow against a string property on a Doctrine entity:
# config/packages/workflow.yaml
framework:
workflows:
article:
type: state_machine
marking_store:
type: method
property: state
supports:
- App\Entity\Article
initial_marking: draft
places: [draft, review, published, archived]
transitions:
submit: { from: draft, to: review }
publish: { from: review, to: published }
archive: { from: published, to: archived }
Mark the entity as an API Platform resource, then declare the transition operation the bundle provides (check the README for the exact operation name — commonly a custom POST/PATCH operation pointing at the bundle's controller, e.g. /articles/{id}/transition). Send {"transition": "submit"} and confirm the response body reflects the new state.
If you only need the marking property updated and validated, this is the whole value proposition: one attribute plus workflow config replaces a custom controller and its validation logic.
Explicit transition endpoint vs. automatic transitions. The bundle's main pattern is an explicit operation: the client names the transition, the bundle loads the workflow, calls $workflow->apply($entity, $transition), and persists. Use this when transitions are user-driven actions. For automatic transitions (e.g. submit immediately on create), keep that in a Doctrine listener or API Platform state processor and reserve the bundle's endpoint for manual moves — mixing both in one place makes guard behavior hard to reason about.
Keep the marking property out of mass assignment. The state property must be writable by the workflow, but not by the client. Add it to a read: false serialization group or a denormalization group that excludes it, otherwise a client can PATCH state directly and bypass the workflow entirely. This is the most common security mistake with any workflow-on-API setup.
Guards and security are two layers — use both.
guard on the transition or a GuardListener subscriber) enforce business rules: "only an editor can publish", "cannot publish without a reviewer".security on the operation enforces authorization at the endpoint.Do not rely on the operation's security alone to prevent invalid transitions; and do not rely on guards alone for access control. When a guard rejects a transition, WorkflowInterface::apply() throws Symfony\Component\Workflow\Exception\NotPermittedException. Wrap the transition call and map it to a 409 Conflict (or 422) with the list of currently enabled transitions — a bare 500 is the default failure mode.
Expose enabled transitions to the client. A robust pattern is a GET /articles/{id}/transitions sub-resource (or a computed field) returning $workflow->getEnabledTransitions($entity). Clients then render exactly the buttons that will succeed instead of guessing and eating 409s. If the bundle does not ship this, add it as a small custom operation — it is cheap and dramatically improves the API ergonomics.
Side effects belong in workflow events, not the controller. Use EventDispatcherInterface / EventSubscriberInterface on workflow.article.transition and workflow.article.completed to send notifications, write audit rows, or dispatch Messenger messages. This keeps the transition endpoint thin and makes side effects fire regardless of whether the transition came from HTTP or from a console command.
Transactions and persistence. The transition and the EntityManager::flush() should happen inside the same request transaction. If the bundle's controller flushes for you, verify that it does not double-flush after your subscriber already persisted. For async transitions via Messenger, remember the workflow state is only consistent if the message carries the entity id and re-loads it inside the handler.
Testing strategy.
WorkflowInterface — no HTTP needed to prove guard logic.KernelBrowser: assert 2xx on a valid transition, 409 on an invalid one, and that the state column actually changed in the DB.state property directly returns 400/ignored, to lock in the serialization group.Archived means version-pinned or forked. No security patches and no compatibility work for API Platform 3.x. API Platform 3 changed resource metadata (ApiResource attributes, operations vs collectionOperations/itemOperations, StateProcessorInterface replacing some event hooks). Expect the bundle's metadata decorator or operation to be the breakage point. Fork it, or lift the ~100 lines of transition logic into your own custom operation — often cheaper than maintaining a fork.
The marking store is a leaky abstraction. marking_store.type: method requires a real getter/setter (getState/setState); type: property bypasses accessors and will not fire your entity's business logic. Verify which one the bundle assumes — many examples use method because the API Platform serializer and Doctrine both need consistent access.
Exceptions are not HTTP responses. NotPermittedException, LogicException for unknown transitions, and InvalidArgumentException for a transition not in the workflow all surface as 500s unless handled. Centralize a try/catch (or an API Platform exception listener) that translates them into 4xx responses with a stable error shape. Unknown transition names in particular should be a 400, not a crash.
Multiple workflows on one entity need explicit naming. If an entity supports article and article_review, WorkflowInterface cannot be autowired unambiguously for that entity. Resolve via $workflowRegistry->get($entity, 'article') or inject the specific service id (workflow.article). This bites when you add a second workflow later.
Concurrency and double transitions. Two concurrent publish requests can both pass the guard before either flushes. Add an optimistic-lock version column (#[ORM\Version]) and let the loser get a 409. For high-contention state machines, consider a SELECT ... FOR UPDATE inside the transition handler.
Config changes are cached. After editing workflow.yaml, places, or transitions, run bin/console cache:clear. In dev the profiler's Workflow panel shows the marking store and enabled transitions — the fastest way to confirm your config actually attached to the entity.
Debugging checklist when a transition does nothing:
bin/console debug:container workflow.article
bin/console debug:container --tag=api_platform.metadata.resource.metadata_collection_factory
bin/console debug:router # is the transition route registered?
bin/console debug:config framework workflow
Also confirm the entity's supports entry exactly matches the FQCN (a typo silently produces a workflow that never applies), and check /api/docs to see whether the operation made it into the OpenAPI schema. If it is missing there, the metadata extension did not run — that is the archived-bundle-on-newer-API-Platform failure signature.
Serialization groups are the extension point you will actually use. Rather than fighting the bundle's operation definition, most teams keep the bundle for the transition call and add their own normalization_context / custom StateProvider to shape the response. Decorating the controller (Symfony service decoration) is the least invasive way to add validation or logging without touching vendor code.
Have an exit plan. Because the repo is archived, isolate the integration behind your own TransitionHandler interface and one adapter class. When you upgrade API Platform, you replace the adapter, not every call site. Given the 11-star footprint, budget for reading the source rather than finding Stack Overflow answers — the codebase is small enough that this is feasible in an afternoon.
How can I help you explore Laravel packages today?