noem / state-machine-loader
Constructs usable state machine instances from various data sources (Array, JSON,YAML)
Requires
- justinrainbow/json-schema: ^5.2
- nette/schema: ^1.2
- noem/state-machine-interface: dev-master
- psr/container: ^1.0 || ^2.0
- symfony/yaml: ^5.3
Requires (Dev)
- mockery/mockery: ^1.4
- noem/composer-file-embed: dev-master
- noem/state-machine: dev-master
- phpunit/phpunit: ^9.1
- squizlabs/php_codesniffer: ^3.6
This package is auto-updated.
Last update: 2024-11-05 00:00:27 UTC
README
Creates State Machine instances from various sources.
Installation
Install this package via composer:
composer require noem/state-machine-loader
Schema
All input data is validated against a JSON schema using justinrainbow/json-schema. The raw schema file can be found at src/schema.json Below is a description of all the relevant entities:
State
Transition
As an alternative shorthand, you can just define a string
with the target state. This will result in a simple
transition that is not enabled by any event or guard and thus will be immediately enabled as soon as the state machine
is triggered by any event. This can be useful for chaining transitions, eg. when you are more interested in the series
of enEntry/onExit events than the intermediate states. The full definition of a transition is an object
though:
Callback
Event handlers (guards, actions, entry/exit handlers) are defined with a flexible syntax
Shorthand method
In its simplest form, this is just a string
which is checked for is_callable()
(->allowing you to pass the names of PHP
functions or static methods).
my_state: action: var_dump
However, it is also possible to pull callbacks from a container:
You can optionally pass a PSR-11 ContainerInterface
into the loader object. It will be used whenever a callback is
prefixed with "@"
. For example, if you define onEntry: "@onEnterFoo"
, this will result
in $callback = $container->get('onEnterFoo')
. You can use this to integrate your framework's DI container into the
FSM's event handling.
my_state: action: @onPostRequest
Extended syntax
Shorthands are great for prototyping and/or very functional codebases, but they make code reuse hard due to the lack of parametrization mechanisms. For more flexibility, you may opt to define your handlers as objects.
Factory
The factory allows you to configure a "function that returns a function" with the arguments that you define next to it.
my_state: action: type: factory factory: createMyStateAction arguments: - Hello world - false
This assumes a function like this in your code:
function createMyStateAction(string $greeting, bool $isError){ return function(object $input) use ($greeting, $echo){ $input->greeting = $greeting; $input->isError return $input; } }
This pattern allows you to reuse the same handlers and customize their behaviour from YAML. This can be especially useful
for writing guards: Say you want to ensure a state has been active for at least 5 seconds. Without factories,
you are more or less forced to create some version of a hasBeenInStateForFiveSeconds
function.
If you want to use the same logic to wait for 10 seconds elsewhere in your application, you're in a tough situation now.
With a factory definition, you can instead write a createTimeoutGuard
which can return guards for any interval you want.
Inline
Inline handlers are a way to write your handlers directly into YAML, rather than defining them as functions in a service container.
my_state: onEntry: type: inline # language=injectablephp callback: | $context = $machine->context($transition->source()); $context['greeting'] = 'Hello World!';
Since action
and guard
callbacks leverage PSR-14-style parameter inspections, it is neccessary to specify an additional argument for them:
baz: action: # Note the additional 'trigger' type: 'inline' callback: | echo 'Hello ' . $trigger; trigger: '\Stringable'
This is useful if you want to keep things simple and don't need to reuse the handler elsewhere in your codebase. It is also a great way to start prototyping quickly. Since this is intended for basic scripting, you only supply the function body itself. You can use the following variables within your inline callbacks:
Types onEntry
& onExit
$state
- The state object where the handler is registered$from
- The state that is being transitioned away from. (In simple machines, this might be the same as$state
, but it is very much possible for a machine to be in many states at once)$machine
- The state machine object
Type action
$trigger
- The object that triggered the action$state
- The state object where the handler is registered$machine
- The state machine object
Type guard
$trigger
- The object that triggered the transition$transition
- The transition object$machine
- The state machine object
Full example
All event handlers are assumed to be configured in a Service Container.
off: transitions: - on # Shorthand used on: parallel: true context: hello: "world" onEntry: '@onBooted' children: foo: action: '@sayMyName' bar: action: '@sayMyName' parallel: true children: bar_1: action: '@sayMyName' children: bar_1_1: transitions: - target: 'bar_1_2' guard: '@guardBar_1_2' bar_1_2: transitions: - target: 'bar_1_1' guard: type: factory # Executes a function that creates the actual guard based on the given parameters factory: '@myGuardFactory' arguments: - 'hello' - 'world' bar_2: action: '@sayMyName' baz: initial: 'substate2' # if not specified, it would use the first child, 'substate1' action: - '@sayMyName' - type: factory factory: '@myActionFactory' arguments: - 'lorem' - '%getIpsum%' # Write raw PHP directly! # Note the additional 'trigger' - type: 'inline' callback: | echo 'Hello World'; trigger: '\stdClass' children: substate1: action: '@sayMyName' substate2: action: '@sayMyName' transitions: - target: 'substate3' guard: # Multiple guards for one transition are possible. Any of them can allow the transition - '@someOtherGuard' - '@guardSubstate3' # Write raw PHP directly! - type: 'inline' callback: | return false; trigger: '\stdClass' substate3: action: '@sayMyName' transitions: # If an exception is used as a trigger, # it can be used to perform a graceful shutdown - target: error guard: Throwable error: onEntry: - '@onException' - '@anotherErrorHandler' context: '@helloWorldService' transitions: - off