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

States Bundle Laravel Package

teknoo/states-bundle

Teknoo States Bundle is a Symfony bundle for defining and managing object states and transitions. It provides configurable state machines, transition rules, and workflow integration for PHP applications that need clear lifecycle handling.

View on GitHub
Deep Wiki
Context7

Getting Started

Reality check first: teknoo/states-bundle is a Symfony bundle — not a Laravel package — and it is archived and deprecated in favour of teknoo/states 3.1+. There is no ServiceProvider to register, no config to php artisan vendor:publish, no facade. What you can actually use day-to-day is the underlying framework-agnostic state-pattern library, installed directly:

composer require teknoo/states

Read in this order:

  1. vendor/teknoo/states/src/*/StateInterface.php and the StatesTrait used on stated objects — this is the entire contract.
  2. The library's tests, not the README. The README is bundle-oriented and trails the 3.x API; the tests show the real call sequence for hydrating and transitioning state.
  3. Only skim the bundle's CompilerPass if you want to see how state classes were auto-discovered under Symfony — you will reimplement that discovery yourself with a Laravel service provider or simply new the state objects.

First use case: an Order model that only permits ship() once placed and only permits refund() once shipped — replacing a match($order->status) that is duplicated across three controllers, a job, and a model observer.

use Teknoo\States\State\StatesTrait;
use Teknoo\States\State\StatedInterface;

final class Order extends Model implements StatedInterface
{
    use StatesTrait;

    protected $fillable = ['status'];
}

$order->initializeStates();   // hydrate state object from the persisted `status` column
$order->ship();               // resolves to Placed state's implementation

Confirm the exact initialisation method name against the current tests — it changed between the 2.x bundle and 3.x library.

Implementation Patterns

Persist the state name, hydrate the state object. The database column is a plain string (draft, placed, shipped). The state object is runtime behaviour and must never be serialised. Wrap hydration in an Eloquent cast or an observer:

protected static function booted(): void
{
    static::retrieved(fn (Order $order) => $order->initializeStates());
    static::saving(fn (Order $order) => $order->status = $order->getCurrentStateName());
}

One state class per state, one method per action. The State pattern's payoff is locality: if refund() only exists in ShippedState, calling it from PlacedState fails loudly at the transition boundary rather than silently in a controller. Keep state classes thin — orchestration lives in services.

Guard before mutating. Any real lifecycle has preconditions (payment captured, address verified, stock reserved). Put these in the transition guard, not the controller:

if (!$order->canShip()) {
    throw new InvalidTransitionException($order->getCurrentStateName(), 'ship');
}

Register state classes in a provider, not the container magic. Symfony auto-discovered state classes via compiler pass. In Laravel, wire them explicitly:

$this->app->bind(OrderStateFactory::class, fn () => new OrderStateFactory(
    placed: new PlacedState(),
    shipped: new ShippedState(),
    refunded: new RefundedState(),
));

Post-transition side effects go in listeners, not state methods. A refund() that also fires an email will become untestable the moment you add a third side effect. Emit a domain event from the state method and subscribe:

// in ShippedState::refund()
event(new OrderRefunded($order));

Testing: assert the transition, not the state class internals. $order->initializeStates(); $order->ship(); expect($order->getCurrentStateName())->toBe('shipped'); covers the contract you actually depend on.

Gotchas and Tips

Do not composer require teknoo/states-bundle. The bundle pins Symfony generations that will conflict with Laravel's Illuminate stack. Even if resolution succeeds, you inherit the full Symfony HttpKernel surface you will never use. Install teknoo/states alone.

You own the maintenance. Archived upstream means no fixes, no security patches, no PHP-8.x compatibility work. Pin the constraint explicitly and add it to your composer audit watch list. Budget for a fork if a future PHP minor breaks it.

State classes and Eloquent hydration fight each other. If you serialise the state object into a json column or a queue payload, you will deserialise a stale object graph. Persist the name, rebuild on retrieved(). The same trap bites SerializesModels on queued jobs — rehydrate after ModelsRestored.

Generated code drift. If you later evaluate the successor teknoo/states 3.1+ and use its generator, treat output as a build artefact: gitignore it, regenerate in CI, and add a header banner. Any committed generated class gets edited by hand within a month and fights the next regeneration in a review diff.

Transition discovery is not free. The library resolves which state a method belongs to at call time, so a typo in a state-class method name surfaces as a runtime "method not found" rather than a static error. Add a test that iterates every state class and asserts its public methods against the transition table, or you will discover gaps in production.

Where the pattern breaks down. Below roughly five states with fewer than two guards, a PHP 8.1 backed enum with match() is shorter, statically analysable, and framework-native. Reach for the state pattern when states carry genuinely different behaviour (different methods, different validation rules), not when they are just labels — otherwise you pay class proliferation for nothing.

Integration with Laravel-native alternatives. If the archived dependency is a blocker, spatie/laravel-model-states provides the same shape with active maintenance and first-class Eloquent/transition/attribute integration. The migration cost from a hand-wired teknoo/states setup is low because both express the same pattern — the difference is where the container plumbing lives.

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.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle