Git Product home page Git Product logo

model-state-machine's Introduction

๐Ÿšจ THIS PACKAGE HAS BEEN ABANDONED ๐Ÿšจ

I no longer use Laravel and cannot justify the time needed to maintain this package. That's why I have chosen to abandon it. Feel free to fork my code and maintain your own copy.

Laravel Model State Machine

Latest Version on Packagist run-tests Check & fix styling Total Downloads

This package adds support for creating state machines for attributes on Laravel Eloquent Models.

Requirements

  • PHP 8.1 or higher
  • Laravel 9.x or higher

Installation

You can install the package via composer:

composer require bvtterfly/model-state-machine

Usage

For example, we have a blog system, and our blog posts have three statuses: draft, pending, and published. When we are writing a post, it's in the draft status. Whenever we finish writing our blog posts, we schedule them for publication in the future, which changes the status of the post to pending. Once the post is published, the status changes to published.

The simplest backed enum that holds the states of a blog post status is:

use Bvtterfly\ModelStateMachine\Attributes\AllowTransitionTo;
use Bvtterfly\ModelStateMachine\Attributes\InitialState;

enum PostState: string
{
    #[InitialState]
    #[AllowTransitionTo(self::PENDING)]
    case DRAFT = 'draft';

    #[AllowTransitionTo(self::PUBLISHED)]
    case PENDING = 'pending';
    
    case PUBLISHED = 'published';
    
}

Here's what the Blog post model would look like:

class Post extends Model
{
    use HasStateMachine;

    protected $casts = [
      'status' => PostState::class  
    ];
    
    
    public function getStateMachineFields(): array
    {
        return [
            'status'
        ];
    }
    
}

A model can have as many state machine fields as you want, You need to add them to the list using the getStateMachineFields method.

Since State machine loads state configuration from string backed enums, You need to cast state machine fields to correlated state enums in your model.

Now, You can get your state machine:

$stateMachine = $post->getStateMachine('status')

Get All states

You can use the getAllStates method, which return collection of the all available states:

$stateMachine->getAllStates();

Get All allowed transitions

You can use the getStateTransitions method, which return collection of available transitions for current/initial state

$stateMachine->getStateTransitions();

If the state field is null and the state configuration doesn't have a initial state (field in unknown state), It will throw an exception.

If you want to get available transitions for a state, You can pass it to the method:

$stateMachine->getStateTransitions(PostState::PENDING);
// or $stateMachine->getStateTransitions('pending');

Using transitions

To use transitions, call the transitionTo method on the state field as follows:

$stateMachine->transitionTo(PostState::PUBLISHED);
// or $stateMachine->transitionTo('published');

You can pass array as a second argument to the transitionTo method for additional data that you'll need in your actions and transitions.

State Actions

You can add actions to run if a state changes to a state. In the above example, Maybe we want to send a tweet and send email to subscribers when the post is published.

We can do this using the #[Actions] attribute. Here's how our PostState would look like:

enum PostState: string
{
    #[InitialState]
    #[AllowTransitionTo(self::PENDING)]
    case DRAFT = 'draft';

    #[AllowTransitionTo(self::PUBLISHED)]
    case PENDING = 'pending';

    #[Actions(SendTweetAction::class, SendEmailToSubscribers::class)]
    case PUBLISHED = 'published';

}

Actions are classes that implements Bvtterfly\ModelStateMachine\Contracts\StateMachineAction:

class SendTweetAction implements StateMachineAction
{

    public function handle(Model $model, array $additionalData): void
    {
        // send tweet...
    }
}

Your actions may also type-hint any dependencies they need on their constructors. All actions are resolved via the Laravel service container, so dependencies will be injected automatically.

Transition Actions

In addition to state actions, maybe you want to run actions only when a specific state transit to another state.

You can pass array of actions as second argument to #[AllowTransitionTo].

In the above example, If we want to send a notification to the admin when the post status change to the pending, Our PostState would look like this:

enum PostState: string
{
    #[InitialState]
    #[AllowTransitionTo(self::PENDING, [SendNotificationToAdmin::class])]
    case DRAFT = 'draft';

    #[AllowTransitionTo(self::PUBLISHED)]
    case PENDING = 'pending';

    #[Actions(SendTweetAction::class, SendEmailToSubscribers::class)]
    case PUBLISHED = 'published';
}

Transition Actions run before State Actions

Action With Validation

When using transitions, you can pass additional data as a second argument, and this data will pass to all actions. So, It's necessary to validate this data before running actions.

Validators are actions that implement Bvtterfly\ModelStateMachine\Contracts\StateMachineValidation.

In above example, We want to send notification to admin when post status changes to pending:

$stateMachine->transitionTo(PostState::PENDING, [
    'message' => '...'
]);

Here's how our SendNotificationToAdmin action would look like:

class SendNotificationToAdmin implements StateMachineAction, StateMachineValidation
{

        public function validate(Model $model, array $additionalData): void
    {
        $validator = validator($additionalData, [
            'message' => 'required',
        ]);
        
        if ($validator->fails()) {
            // throw exception
        }
    }

    public function handle(Model $model, array $additionalData): void
    {
        // send notification...
    }
}

Custom transition Classes

This package comes with a default transition class that save new state after running State & Transition Actions. If you need to do more than just changing to the new state, you can use transition classes.

Custom transition are classes that implements Bvtterfly\ModelStateMachine\Contracts\StateTransition.

For example, We want to store the user_id of who changes the status of a post to pending status in the post model:

class DraftToPending implements StateTransition
{
    public function commitTransition(
            BackedEnum|string $newState,
            Model $model,
            string $field,
            array $additionalData
        ): void {
            $model->{$field} = $newState;
            $model->causer = $additionalData['user_id']
            $model->save();
        }
}

You can pass this class as a third argument to the #[AllowTransitionTo].

Then, Our PostState would look like this:

enum PostState: string
{
    #[InitialState]
    #[AllowTransitionTo(self::PENDING, [SendNotificationToAdmin::class], DraftToPending::class)]
    case DRAFT = 'draft';

    #[AllowTransitionTo(self::PUBLISHED)]
    case PENDING = 'pending';

    #[Actions(SendTweetAction::class, SendEmailToSubscribers::class)]
    case PUBLISHED = 'published';
}

Testing

composer test

Changelog

Please see CHANGELOG for more information on what has changed recently.

Security Vulnerabilities

Please review our security policy on how to report security vulnerabilities.

Credits

License

The MIT License (MIT). Please see License File for more information.

model-state-machine's People

Contributors

bvtterfly avatar dependabot[bot] avatar github-actions[bot] avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

model-state-machine's Issues

[DISCUSSION] Conditional Transitions

Thanks for this plugin. We use it in one of our projects and like the Enum approach, but just stumbled over an edge-case:

Currently it's possible to declare possible transitions statically, but in our case some transitions are not possible depending on the parent model. E.g. depending on the type of the model, some states are never possible. My thought was to introduce a callback on AllowTransitionTo that's evaluated before collecting the possible transitions:

#[AllowTransitionTo(self::Closed, callback: fn ($model) => ! $model->hasOpenTasks())]
case Open = 'open';

This might be out of scope for a classic state machine though. What do you think? Is this something you'd support?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.