Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Workflow Extensions Bundle Laravel Package

gtt/workflow-extensions-bundle

GTT Workflow Extensions Bundle extends Symfony's Workflow component with practical features for defining, visualizing, and managing state machines and business workflows. It helps PHP teams reduce boilerplate and build clearer, more maintainable workflow-driven applications.

View on GitHub
Deep Wiki
Context7

Getting Started

First, the reality check. gtt/workflow-extensions-bundle is a Symfony bundle, not a Laravel package. There is no service provider, no facade, no Artisan command, and no config/workflow.php. It extends symfony/workflow, so everything you learn here is really the Symfony Workflow component plus whatever conveniences GTT layered on top. If you are in a Laravel codebase you have two realistic options:

  1. Use symfony/workflow standalone (it is a plain component and works fine outside Symfony) and treat this bundle as reference material.
  2. Only pull the bundle in if you are running a Symfony kernel (e.g. a Laravel app that embeds Symfony via a bridge, or a separate Symfony service in the same estate).

Do not install it expecting Laravel auto-discovery to wire anything up.

Minimal steps (Symfony host):

composer require gtt/workflow-extensions-bundle

Register it in config/bundles.php:

return [
    // ...
    Gtt\WorkflowExtensionsBundle\GttWorkflowExtensionsBundle::class => ['all' => true],
];

Then define a workflow under the standard framework.workflows key and let the bundle's extensions apply on top:

# config/packages/workflow.yaml
framework:
    workflows:
        order:
            type: state_machine
            audit_trail: { enabled: true }
            marking_store:
                type: method
                property: getMarking
            supports:
                - App\Entity\Order
            places: [draft, awaiting_payment, paid, shipped, cancelled]
            transitions:
                pay:
                    from: awaiting_payment
                    to: paid
                ship:
                    from: paid
                    to: shipped

Where to look first, in this order:

  1. README.md — for a 41-star bundle this is likely thin; assume you will be reading source.
  2. composer.json — check the symfony/* version constraints. This is the single biggest compatibility risk.
  3. src/DependencyInjection/Configuration.php — the authoritative list of extension options. If it is not in this file, the bundle does not support it.
  4. src/ top level and tests/ — tests are usually the best usage documentation for small bundles.
  5. Resources/config/ (or config/) — service definitions reveal the extension points and tagged services.

First use case to validate the install: an order/state workflow with a guard and one event listener. Get a subject through draft -> awaiting_payment -> paid from a controller, then confirm the listener fires. If that works, the wiring is sound and you can start layering the extension features.

Implementation Patterns

Config-first workflow definitions

Define workflows in YAML/PHP config, not in code. This lets the bundle's compiler passes see them and lets workflow:dump render them. Keep workflow config in config/packages/workflow.yaml and treat transition names as a public contract.

Inject the registry, never a single workflow

The idiomatic access point is Symfony\Component\Workflow\Registry:

final class OrderController
{
    public function __construct(private Registry $workflows) {}

    public function pay(Order $order): Response
    {
        $workflow = $this->workflows->get($order, 'order');

        if (!$workflow->can($order, 'pay')) {
            throw new \DomainException('Order cannot be paid in its current state.');
        }

        $workflow->apply($order, 'pay');
        $this->em->flush();

        return new Response('ok');
    }
}

get($subject, 'name') is important once you have more than one workflow supporting the same entity — a common pattern is a lifecycle machine and a separate fulfilment machine on the same Order.

Entities as subjects with a method-based marking store

Use type: method with getMarking/setMarking (or property with a $marking column) and persist the marking with Doctrine. A JSON column works well:

#[ORM\Column(type: 'json')]
private array $marking = [];

public function getMarking(): array { return $this->marking; }
public function setMarking(array $marking): void { $this->marking = $marking; }

Method-based marking is the safer choice because it lets you normalise or validate the marking on write and avoids Doctrine change-tracking surprises on a raw array property.

Guards for business rules, listeners for side effects

Keep these strictly separated:

  • Guards answer "may this transition happen?" — pure, fast, no I/O where avoidable. They run on can() and getEnabledTransitions(), so every list page render will hit them.
  • Event listeners/subscribers do the side effects — email, audit rows, queue dispatch, webhook calls — on workflow.order.transition or workflow.order.entered.
#[AsEventListener(event: 'workflow.order.entered.paid')]
final class SendPaymentReceipt
{
    public function __invoke(EnteredEvent $event): void
    {
        $this->bus->dispatch(new SendReceipt($event->getSubject()->getId()));
    }
}

Dispatch to a queue rather than doing work inline; workflow events fire inside whatever transaction triggered apply().

Metadata as the UI/automation contract

Symfony workflows support metadata on both places and transitions. The practical pattern is to drive UI from it instead of hardcoding labels and colours in Blade/Twig:

places:
    paid:
        metadata:
            label: "Paid"
            colour: green
            customer_visible: true
transitions:
    ship:
        metadata:
            label: "Mark shipped"
            permission: ROLE_WAREHOUSE

Expose this through an endpoint (GET /orders/{id}/workflow) returning enabled transitions plus their metadata, and let the front end render buttons generically. This is the highest-leverage integration for a Laravel/Inertia front end talking to a Symfony workflow service.

Layer the bundle's extensions on top of the standard API

The bundle's value is in its extensions — audit trails, transition blockers, metadata resolution, and similar. The pattern is always the same: keep calling the standard Registry/WorkflowInterface API, and let the bundle's compiler passes and tagged services intercept. Read src/DependencyInjection/ to find the service tags it looks for; that is how you plug in your own implementations (custom marking store, custom guard, custom metadata provider) without forking.

Testing

Unit-test workflow behaviour without a kernel where you can:

$definition = new Definition(/* places, transitions */);
$workflow = new Workflow($definition, new MethodMarkingStore(true, 'getMarking'));

$order = new Order();
$workflow->apply($order, 'request_payment');
self::assertTrue($workflow->can($order, 'pay'));

Then keep a small number of kernel-level functional tests for the bundle's extensions and your listeners, since those only exist once the container is compiled.

Gotchas and Tips

The Symfony/Laravel mismatch is the biggest one. This bundle will not appear in php artisan about, will not be discovered, and will not work with Laravel's container. If your goal is "workflow in Laravel," use symfony/workflow plus a small service provider wrapping Registry — that gets you 90% of the value with none of the bundle coupling.

Check the Symfony version constraint before anything else. A bundle with 41 stars and zero dependents is unlikely to be actively tracking Symfony 7. composer why-not symfony/framework-bundle will tell you immediately if it is pinned to 5.x/6.x. If it is, either hold your Symfony version or vendor the relevant classes.

Config cache bites on every change. After editing workflow YAML, bin/console cache:clear — otherwise you will debug a transition that "doesn't exist" for twenty minutes. In dev, ensure the container is not warmed with stale config.

Transition name typos fail at runtime, not compile time. apply($order, 'ship') on a workflow with shipping throws. Define transition names as constants on the entity or a dedicated enum and reference them everywhere, including in listener event names (workflow.order.entered.shipped).

Guard event vs. validator event. guard can block a transition; validator runs on apply() but not on can(). If can() says yes and apply() still throws, you put a check in the wrong place.

can() is not a lock. Between can() and apply() another request can move the same entity. Wrap the check-and-apply in a transaction with optimistic locking (@Version) or a SELECT ... FOR UPDATE; workflow will not do this for you.

Marking store misconfiguration is silent. With property type, the property must exist and be nullable/array-initialised; with method, the getter/setter must be public and correctly named relative to property. A wrong config shows up as "no enabled transitions" rather than an error.

Metadata is an untyped flat array. There is no schema, no validation, and no IDE completion. Put the keys you rely on in a constants class and validate on read, or a typo in permission becomes a security hole.

Debugging toolkit:

bin/console workflow:dump order --marking=paid   # graph + transitions
bin/console debug:container --tag=workflow        # what the bundle registered
bin/console debug:event-dispatcher workflow.order.entered.paid

If the bundle's extensions are not firing, debug:container is the fastest way to see whether its services were actually registered — a missing bundle registration in bundles.php is the usual cause.

Vendor risk is real here. Low stars, low opportunity score, zero dependents. Pin the exact version in composer.json, keep your own workflows defined in config (portable to plain symfony/workflow), and avoid making the bundle's extension classes part of your public API. If you find yourself needing to extend or patch something, fork it into a repositories VCS entry rather than relying on upstream releases.

Extension points worth knowing: the bundle's compiler passes and tagged services are your seam. Custom marking stores, custom guards, and metadata providers can all be swapped via service overrides in services.yaml without touching the bundle's source — do that rather than subclassing.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings