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 Bundle Laravel Package

integrated/workflow-bundle

A workflow bundle for PHP applications that lets you define state machines, transitions, and guards in configuration, then move records through them with a clean API. Includes event hooks, validation, and audit-friendly history.

View on GitHub
Deep Wiki
Context7

Getting Started

Reconnaissance before installation (15 minutes). This is a Bitbucket-hosted package with no Packagist presence, zero stars and zero dependents — treat it as a private bundle you're evaluating, not a community library. Clone it and answer three questions:

git clone [email protected]:eactive/integrated-workflow-bundle.git
cat composer.json            # require constraints, type, autoload, branch-alias
ls src/                     # Bundle class? DependencyInjection/? Resources/config/?
git tag && git log --oneline -20
  • Is it a Symfony bundle? The -bundle suffix almost certainly means it registers via a *Bundle class in a Symfony kernel — there will be no WorkflowServiceProvider waiting for Laravel's package discovery. Check for DependencyInjection/Extension.
  • Does it wrap symfony/workflow? If so, the portability win is large: places/transitions/guards/marking stores are a well-understood model and it's mainly YAML→service wiring you'd be replacing.
  • What does require pin? A symfony/*: ^3.4|^4.0 constraint or a hard doctrine/* dependency is a red flag on Laravel 10/11 and PHP 8.2+.

Install via VCS (no Packagist entry):

"repositories": [
  { "type": "vcs", "url": "[email protected]:eactive/integrated-workflow-bundle.git" }
],
"require": {
  "integrated/workflow-bundle": "dev-main#<commit-sha>"
}

Pin to an explicit commit or tag from day one — never let a floating dev-main reach production. For CI, provision SSH agent forwarding or a COMPOSER_AUTH Bitbucket access token; a private VCS repo that fails to clone breaks every pipeline silently.

Decide placement before writing code. Three options, in order of preference:

  1. Adapter in-process — if the core is framework-agnostic, wrap it behind your own App\Workflow\WorkflowService interface so application code never imports vendor classes.
  2. Sidecar service — if it is Symfony-kernel-bound, run it as a small Symfony service exposing an HTTP/queue API and consume it from Laravel.
  3. Direct dependency — only if you confirm a clean Laravel integration path.

First use case: one linear lifecycle on one Eloquent model. Pick something boring and high-value — Order: draft → submitted → approved → fulfilled, or Invoice: pending → paid → void. Get one can(), one apply(), and one persisted state column working end to end before touching guards, listeners, or history.

Implementation Patterns

Wrap the engine behind a narrow application interface. This is the single highest-leverage decision you'll make, because it caps blast radius if the bundle goes quiet:

interface WorkflowService
{
    public function can(Model $subject, string $transition, string $workflow = 'default'): bool;
    public function apply(Model $subject, string $transition, string $workflow = 'default'): void;
    public function enabledTransitions(Model $subject, string $workflow = 'default'): array;
}

Controllers, jobs, and form requests depend on this interface — never on vendor classes. When the upstream dies, you swap one binding.

Persist marking on the model. Symfony's marking store keeps state as an array of place names (to support parallel places). Map it to a JSON column, not a varchar, unless you're certain you'll only ever be in one place at a time:

// migration
$table->json('workflow_marking')->nullable();
$table->string('workflow_name')->default('default');

// trait
public function marking(): array { return $this->workflow_marking ?? []; }
public function inPlace(string $place): bool { return in_array($place, $this->marking(), true); }

Guards belong in Laravel policies, not bundle config. If the bundle lets you attach guards declaratively, have each guard delegate to a policy method or a dedicated App\Workflow\Guards\* class. That keeps business rules testable with Laravel's normal tooling and prevents config-driven guard logic from drifting outside code review:

// instead of closure logic in YAML
public function canApprove(Order $order): bool
{
    return $order->total < 10_000 && $order->customer->isVerified();
}

Bridge the event dispatcher, don't replace it. Symfony dispatches workflow.<name>.transition, .enter, .leave, and .guard events. If you're running it in-process, register a single subscriber that re-emits Laravel events:

Event::listen('workflow.*.transition', function ($eventName, array $payload) {
    [$name, $transition] = /* parse */;
    event(new WorkflowTransitioned($payload['subject'], $transition));
});

Then normal Laravel listeners, queued jobs, and notification mailables attach to WorkflowTransitioned. Keep side effects (email, webhooks, search indexing) in DB::afterCommit() listeners so they fire only after the transition persists.

Atomic transitions are your responsibility. The engine enforces transition validity, not database integrity. Wrap the call site:

DB::transaction(function () use ($order) {
    $locked = Order::lockForUpdate()->findOrFail($order->id);
    $this->workflow->apply($locked, 'approve');
    $locked->save();
});

Audit by subscription, not by scattering writes. A single RecordWorkflowTransition listener writing to an append-only workflow_transitions table (subject type/id, from_place, to_place, transition, actor_id, payload JSON, created_at) gives you history for free and keeps the call sites clean.

Registry pattern for multi-workflow apps. When you have more than 2–3 workflows, a WorkflowRegistry keyed by name (with definitions declared in PHP or config/workflows.php) beats sprinkling service names through controllers. Resolve by $order->workflow_name so the model knows which graph applies to it.

Testing. Unit-test workflow definitions as data (assert a given transition is permitted from a given place), and feature-test the HTTP endpoints that trigger transitions. A fast test helper pays off immediately:

$this->order->setMarking(['draft']);
$this->post("/orders/{$this->order->id}/submit")->assertRedirect();
$this->assertDatabaseHas('workflow_transitions', ['transition' => 'submit']);

Gotchas and Tips

"Bundle" ≠ "package." There is no package auto-discovery, no ServiceProvider, no facade. If it ships a Symfony Bundle class and DependencyInjection/Extension, you cannot composer require your way to a working install in Laravel — expect to write the adapter, or run a sidecar.

Silent no-op transitions. Symfony's workflow engine typically returns false from apply() on an invalid transition rather than throwing. Wrap it so invalid transitions raise a domain exception (InvalidTransitionException) — otherwise a bug in your UI logic becomes a missing audit record and no one notices until a customer complains.

Multi-place state will break naive code. Symfony's marking store allows parallel places (['draft', 'legal_review']). If you write $order->status === 'approved' anywhere, it will bite you later. Always compare via inPlace() or the engine's can().

Symfony DI expects service definitions, not closures. Guards and validators are wired through the container, so passing Laravel container-resolved services into Symfony definitions requires a compiler pass or a small bridge. Resolve guards through Laravel's container inside a thin adapter class instead of trying to teach Symfony DI about Laravel.

Transaction and event ordering. If the engine dispatches transition events before you commit, listeners see the new marking but the row isn't durable. If it dispatches after, listeners may run outside your transaction. Either way, use DB::afterCommit() for anything with side effects, and never allow queued listeners to depend on in-flight state.

Concurrency. Two admins clicking "approve" simultaneously can produce duplicate transitions. Defend with lockForUpdate() (or a version column and a unique index on (subject_type, subject_id, transition, idempotency_key)). The engine will happily let both through.

Private VCS auth in CI. A missing SSH key or expired Bitbucket app password fails the build with a confusing Composer error. Store credentials as pipeline secrets, and mirror the package to an internal Composer registry (Satis/Private Packagist) so a Bitbucket outage doesn't block deploys.

Version drift. With no changelog or release cadence, lock to a commit SHA and add a smoke test that boots the container and executes one trivial transition. That single test catches 90% of incompatible upgrades before they reach staging.

Debugging "why can't I transition?" The engine tells you false; it doesn't tell you which guard rejected. Build a small helper that iterates guards and returns the first failure reason, and log it at debug level. Without it, every support ticket becomes a source dive.

Audit table growth. Transition history grows linearly with user activity — budget a retention or archival policy before it becomes a slow-query problem. It is a common latent cost in workflow engines.

Extension points worth knowing. Custom marking stores (e.g. Eloquent-backed), guard/validator service classes, and event subscribers are the three places you'll actually modify. Everything else should be reached through your WorkflowService interface.

If the bundle is Symfony-kernel-bound, stop. Do not attempt to boot a Symfony kernel inside a Laravel process. Either run a sidecar or reject the dependency and use symfony/workflow standalone (it's genuinely framework-agnostic) or a small internal state machine. Both are cheaper than an adapter you'll maintain indefinitely.

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.
laravel2580/idehelpers
delatbabel/viewpages
anavel/gettext
muhamadrezaar/highcharts
delatbabel/keylists
frankkessler/salesforce-laravel-oauth2-rest
anavel/foundation
wdmtech/enviricons
eii/laravel-captcha
muka/shape-reader
tdt/json
refinery29/php-cs-fixer-config
hotmeteor/eco-env
mvaliolahi/hydrate
wearede/tbcpay-php
parsnick/steak
thibaud-dauce/mattermost-php
multilinguals/apollo-client
antaresbr/collection
kr0lik/phpstan-rules