From f7f5d9ce4ede20970300e9552d91833fc3810fa2 Mon Sep 17 00:00:00 2001 From: Jarek Tkaczyk Date: Sun, 22 Jul 2018 12:46:02 +0800 Subject: [PATCH] readme example --- README.md | 141 +++++++++++++++++++++++++++++++++++++++++++++- tests/FsmTest.php | 18 ++++++ 2 files changed, 158 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ba82a0f..1c92b86 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Sofa/StateMachine -#### Finite State Machine implementation +#### [Finite State Machine](https://en.wikipedia.org/wiki/Finite-state_machine) implementation [![Downloads](https://poser.pugx.org/sofa/state-machine/downloads)](https://packagist.org/packages/sofa/state-machine) [![stable](https://poser.pugx.org/sofa/state-machine/v/stable.svg)](https://packagist.org/packages/sofa/state-machine) [![Coverage Status](https://coveralls.io/repos/github/jarektkaczyk/state-machine/badge.svg?branch=master)](https://coveralls.io/github/jarektkaczyk/state-machine?branch=master) @@ -17,6 +17,145 @@ path/to/your/app$ composer require sofa/state-machine ## Usage +State machine helps you eliminate `switch` and/or `if/else` statements in your code to determine available actions in given state. + +Let's use a naive example from a Laravel's Blade view template and underlying Eloquent `Order` model: + +```php +@foreach($orders as $order) + {{ $order->reference }} status: {{ $order->status }} + + @if($order->status === 'new') + + + @elseif($order->status === 'awaiting_payment') + + + @elseif($order->status === 'awaiting_shipment') + + + @elseif($order->status === 'in_delivery') + + + + @elseif($order->status === 'complete') + + + @elseif($order->status === 'processing_claim') + + + @endif +@endforeach +``` + +This quickly gets out of hand, especially when a new status is introduced or the processing order changes. + +--- + +**To streamline it, we can implement state machine for the Order entity:** + +1. implement interface on the `Order` model + ```php + class Order extends Model implements \Sofa\StateMachine\StateMachineInterface + { + //... + + public function getCurrentState() : string + { + return $this->status; + } + + public function setState(string $state) : void + { + $this->status = $state; + $this->save(); + } + } + ``` + +2. define available transitions and prepare data for the template: + ```php + $transitions = [ + Transition::make(/*from_state*/ 'new', /*action*/ 'start processing', /*to_state*/ 'awaiting_payment'), + Transition::make('awaiting_payment', 'record payment', 'awaiting_shipment'), + Transition::make('awaiting_shipment', 'save tracking number', 'in_delivery'), + Transition::make('in_delivery', 'record delivery', 'complete'), + Transition::make('in_delivery', 'open claim', 'processing_claim'), + Transition::make('complete', 'open claim', 'processing_claim'), + Transition::make('processing_claim', 'close claim', 'complete'), + Transition::make('processing_claim', 'refund', 'refunded'), + ]; + + foreach ($orders as $order) { + $order_state = new \Sofa\StateMachine\Fsm($order, $transitions); + + $order->available_actions = $order_state->getAvailableActions(); + } + ``` + +3. and we end up with controller & template code decoupled from the Process logic & order: + ```php + @foreach($orders as $order) + {{ $order->reference }} status: {{ $order->status }} + + @foreach($order->available_actions as $action) + + @endforeach + @endforeach + ``` + +4. finally let's process the actions + ```php + // controller handling the action + public function handleAction($order_id, Request $request) + { + $order_state = new \Sofa\StateMachine\Fsm(Order::find($order_id), $transitions); + + $this->validate($request, [ + 'action' => Rule::in($order_state->getAvailableActions()), + // ... + ]); + $order_state->process($request->get('action')); + + return Redirect::to('some/place'); + } + + ``` + +With this setup we no **longer have to change our controllers or views, whenever business requirements change**. Instead we add a new transition to the state machine definition. + +--- + +#### I need more control during transition - how to? + +The above example assumes very simple transition process, ie. `$order->status = $new_status`. This can be enough sometimes, but often we will need more flexibility during transitions. To address this need you can customize your `Transition` definitions, so they turn from simple **POPO** into `callable` that will be invoked, when state machine processes appropriate **action**: + +```php +class Refund extends \Sofa\StateMachine\Transition +{ + public function __invoke(StateMachineInterface $order, $payload) + { + // $payload is any object you pass to the process method: + // $order_state->process('refund', $anything_you_need_here); + $order->refunded_at = $payload['time']; + $order->refunded_by = $payload['user_id']; + + $order->setState($this->to_state); + } +} + +// Then our transitions definition would like something like: +$transitions = [ + // ... + Transition::make('processing_claim', 'close claim', 'complete'), + Refund::make('processing_claim', 'refund', 'refunded'), +]; +``` + +--- + +Happy Coding! + #### Contribution diff --git a/tests/FsmTest.php b/tests/FsmTest.php index 85893f7..e06b09f 100644 --- a/tests/FsmTest.php +++ b/tests/FsmTest.php @@ -20,6 +20,7 @@ protected function setUp() Transition::make('idle', 'move', 'moving'), Transition::make('moving', 'stop', 'idle'), Transition::make('moving', 'turn_off', 'california_cruisin'), + CustomTransition::make('off', 'custom_start', 'idle'), ]); } @@ -43,6 +44,13 @@ public function it_puts_machine_object_in_proper_state() $this->assertEquals('california_cruisin', $this->stateful_object->getCurrentState()); } + /** @test */ + public function it_allows_customizing_transitions_logic() + { + $this->fsm->process('custom_start', ['prop' => 'customized']); + $this->assertEquals('customized', $this->stateful_object->prop); + } + /** * @test * @expectedException Sofa\StateMachine\DuplicateActionException @@ -68,6 +76,7 @@ public function it_rejects_invalid_action_for_processing() class StatefulDouble implements StateMachineInterface { public $state = 'off'; + public $prop = 'initial'; public function getCurrentState() : string { @@ -79,3 +88,12 @@ public function setState(string $state) : void $this->state = $state; } } + +class CustomTransition extends Transition +{ + public function __invoke($stateful_object, $payload) + { + $stateful_object->prop = $payload['prop']; + $stateful_object->setState($this->to_state); + } +}