Actualización

This commit is contained in:
Xes
2025-04-10 12:24:57 +02:00
parent 8969cc929d
commit 45420b6f0d
39760 changed files with 4303286 additions and 0 deletions

View File

@@ -0,0 +1,144 @@
<?php
/*
* This file is part of the StateMachine package.
*
* (c) Alexandre Bacco
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SM\Callback;
use SM\Event\TransitionEvent;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\Component\PropertyAccess\PropertyAccessor;
class Callback implements CallbackInterface
{
/**
* @var array
*/
protected $specs;
/**
* @var mixed
*/
protected $callable;
/**
* @param array $specs Specification for the Callback to be called
* @param mixed $callable Closure or Callable that will be called if specifications pass
*/
public function __construct(array $specs, $callable)
{
foreach (array('from', 'to', 'on', 'excluded_from', 'excluded_to', 'excluded_on') as $clause) {
if (!isset($specs[$clause])) {
$specs[$clause] = array();
} elseif (!is_array($specs[$clause])) {
$specs[$clause] = array($specs[$clause]);
}
}
$this->specs = $specs;
$this->callable = $callable;
}
/**
* @param TransitionEvent $event
*
* @return mixed The returned value from the callback
*/
public function call(TransitionEvent $event)
{
if (!isset($this->specs['args'])) {
$args = array($event);
} else {
$expr = new ExpressionLanguage();
$args = array_map(
function($arg) use($expr, $event) {
if (!is_string($arg)) {
return $arg;
}
return $expr->evaluate($arg, array(
'object' => $event->getStateMachine()->getObject(),
'event' => $event
));
}, $this->specs['args']
);
}
$callable = $this->filterCallable($this->callable, $event);
return call_user_func_array($callable, $args);
}
/**
* {@inheritDoc}
*/
public function __invoke(TransitionEvent $event)
{
if ($this->isSatisfiedBy($event)) {
return $this->call($event);
}
return true;
}
/**
* {@inheritDoc}
*/
public function isSatisfiedBy(TransitionEvent $event)
{
$config = $event->getConfig();
return
$this->isSatisfiedByClause('on', $event->getTransition())
&& $this->isSatisfiedByClause('from', $event->getState())
&& $this->isSatisfiedByClause('to', $config['to'])
;
}
/**
* @param string $clause The clause to check (on, from or to)
* @param string $value The value to check the clause against
*
* @return bool
*/
protected function isSatisfiedByClause($clause, $value)
{
if (0 < count($this->specs[$clause]) && !in_array($value, $this->specs[$clause])) {
return false;
}
if (0 < count($this->specs['excluded_'.$clause]) && in_array($value, $this->specs['excluded_'.$clause])) {
return false;
}
return true;
}
/**
* @param callable|array $callable A callable or array with index 0 starting with "object" that will evaluated as a property path with "object" being the object undergoing the transition
* @param TransitionEvent $event
*
* @return callable
*/
protected function filterCallable($callable, TransitionEvent $event)
{
if (is_array($callable) && isset($callable[0]) && is_string($callable[0]) && 'object' === substr($callable[0], 0, 6)) {
$object = $event->getStateMachine()->getObject();
// callable could be "object.property" and not just "object", so we evaluate the "property" path
if ('object' !== $callable[0]) {
$accessor = new PropertyAccessor();
$object = $accessor->getValue($object, substr($callable[0], 7));
}
return array($object, $callable[1]);
}
return $callable;
}
}

View File

@@ -0,0 +1,50 @@
<?php
/*
* This file is part of the StateMachine package.
*
* (c) Alexandre Bacco
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SM\Callback;
use SM\SMException;
class CallbackFactory implements CallbackFactoryInterface
{
/**
* @var string
*/
protected $class;
public function __construct($class)
{
if (!class_exists($class)) {
throw new SMException(sprintf(
'Class %s given to CallbackFactory does not exist.',
$class
));
}
$this->class = $class;
}
/**
* {@inheritDoc}
*/
public function get(array $specs)
{
if (!isset($specs['do'])) {
throw new SMException(sprintf(
'CallbackFactory::get needs the index "do" to be able to build a callback, array %s given.',
json_encode($specs)
));
}
$class = $this->class;
return new $class($specs, $specs['do']);
}
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* This file is part of the StateMachine package.
*
* (c) Alexandre Bacco
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SM\Callback;
interface CallbackFactoryInterface
{
/**
* Return an instance of CallbackInterface loaded with the given $specs
*
* @param array $specs
*
* @return CallbackInterface
*/
public function get(array $specs);
}

View File

@@ -0,0 +1,35 @@
<?php
/*
* This file is part of the StateMachine package.
*
* (c) Alexandre Bacco
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SM\Callback;
use SM\Event\TransitionEvent;
interface CallbackInterface
{
/**
* Calls the callback if its specifications pass the event
*
* @param TransitionEvent $event
*
* @return mixed
*/
public function __invoke(TransitionEvent $event);
/**
* Determines if the callback specifications pass the event
*
* @param TransitionEvent $event
*
* @return bool
*/
public function isSatisfiedBy(TransitionEvent $event);
}

View File

@@ -0,0 +1,64 @@
<?php
/*
* This file is part of the StateMachine package.
*
* (c) Alexandre Bacco
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SM\Callback;
use SM\Event\TransitionEvent;
use SM\Factory\FactoryInterface;
/**
* Add the ability to cascade a transition to a different graph or different object via a simple callback
*
* @author Alexandre Bacco <alexandre.bacco@gmail.com>
*/
class CascadeTransitionCallback
{
/**
* @var FactoryInterface
*/
protected $factory;
/**
* @param FactoryInterface $factory
*/
public function __construct(FactoryInterface $factory)
{
$this->factory = $factory;
}
/**
* Apply a transition to the object that has just undergone a transition
*
* @param \Traversable|array $objects Object or array|traversable of objects to apply the transition on
* @param TransitionEvent $event Transition event
* @param string|null $transition Transition that is to be applied (if null, same as the trigger)
* @param string|null $graph Graph on which the new transition will apply (if null, same as the trigger)
* @param bool $soft If true, check if it can apply the transition first (no Exception thrown)
*/
public function apply($objects, TransitionEvent $event, $transition = null, $graph = null, $soft = true)
{
if (!is_array($objects) && !$objects instanceof \Traversable) {
$objects = array($objects);
}
if (null === $transition) {
$transition = $event->getTransition();
}
if (null === $graph) {
$graph = $event->getStateMachine()->getGraph();
}
foreach ($objects as $object) {
$this->factory->get($object, $graph)->apply($transition, $soft);
}
}
}

View File

@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the StateMachine package.
*
* (c) Alexandre Bacco
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SM\Event;
abstract class SMEvents
{
const PRE_TRANSITION = 'winzou.state_machine.pre_transition';
const POST_TRANSITION = 'winzou.state_machine.post_transition';
const TEST_TRANSITION = 'winzou.state_machine.test_transition';
}

View File

@@ -0,0 +1,105 @@
<?php
/*
* This file is part of the StateMachine package.
*
* (c) Alexandre Bacco
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SM\Event;
use SM\StateMachine\StateMachineInterface;
use Symfony\Component\EventDispatcher\Event;
class TransitionEvent extends Event
{
/**
* @var string
*/
protected $transition;
/**
* @var string
*/
protected $fromState;
/**
* @var array
*/
protected $config;
/**
* @var StateMachineInterface
*/
protected $stateMachine;
/**
* @var bool
*/
protected $rejected = false;
/**
* @param string $transition Name of the transition being applied
* @param string $fromState State from which the transition is applied
* @param array $config Configuration of the transition
* @param StateMachineInterface $stateMachine State machine
*/
public function __construct($transition, $fromState, array $config, StateMachineInterface $stateMachine)
{
$this->transition = $transition;
$this->fromState = $fromState;
$this->config = $config;
$this->stateMachine = $stateMachine;
}
/**
* @return string
*/
public function getTransition()
{
return $this->transition;
}
/**
* @return array
*/
public function getConfig()
{
return $this->config;
}
/**
* @return StateMachineInterface
*/
public function getStateMachine()
{
return $this->stateMachine;
}
/**
* @return string
*/
public function getState()
{
return $this->fromState;
}
/**
* @param bool $reject
*/
public function setRejected($reject = true)
{
$this->rejected = (bool) $reject;
}
/**
* @return bool
*/
public function isRejected()
{
return $this->rejected;
}
}

View File

@@ -0,0 +1,84 @@
<?php
/*
* This file is part of the StateMachine package.
*
* (c) Alexandre Bacco
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SM\Extension\Twig;
use SM\Factory\FactoryInterface;
class SMExtension extends \Twig_Extension
{
/**
* @var FactoryInterface
*/
protected $factory;
/**
* @param FactoryInterface $factory
*/
public function __construct(FactoryInterface $factory)
{
$this->factory = $factory;
}
/**
* @{inheritDoc}
*/
public function getFunctions()
{
return array(
new \Twig_SimpleFunction('sm_can', array($this, 'can')),
new \Twig_SimpleFunction('sm_state', array($this, 'getState')),
new \Twig_SimpleFunction('sm_possible_transitions', array($this, 'getPossibleTransitions')),
);
}
/**
* @param object $object
* @param string $transition
* @param string $graph
*
* @return bool
*/
public function can($object, $transition, $graph = 'default')
{
return $this->factory->get($object, $graph)->can($transition);
}
/**
* @param object $object
* @param string $graph
*
* @return string
*/
public function getState($object, $graph = 'default')
{
return $this->factory->get($object, $graph)->getState();
}
/**
* @param object $object
* @param string $graph
*
* @return array
*/
public function getPossibleTransitions($object, $graph = 'default')
{
return $this->factory->get($object, $graph)->getPossibleTransitions();
}
/**
* @{inheritDoc}
*/
public function getName()
{
return 'sm';
}
}

View File

@@ -0,0 +1,104 @@
<?php
/*
* This file is part of the StateMachine package.
*
* (c) Alexandre Bacco
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SM\Factory;
use SM\SMException;
use SM\StateMachine\StateMachineInterface;
abstract class AbstractFactory implements ClearableFactoryInterface
{
/**
* @var array
*/
protected $configs;
/**
* @var array
*/
protected $stateMachines = array();
/**
* @param array $configs Array of configs for the available state machines
*/
public function __construct(array $configs)
{
foreach ($configs as $graph => $config) {
$this->addConfig($config, $graph);
}
}
/**
* {@inheritDoc}
*/
public function get($object, $graph = 'default')
{
$hash = spl_object_hash($object);
if (isset($this->stateMachines[$hash][$graph])) {
return $this->stateMachines[$hash][$graph];
}
foreach ($this->configs as $config) {
if ($config['graph'] === $graph && $object instanceof $config['class']) {
return $this->stateMachines[$hash][$graph] = $this->createStateMachine($object, $config);
}
}
throw new SMException(sprintf(
'Cannot create a state machine because the configuration for object "%s" with graph "%s" does not exist.',
get_class($object),
$graph
));
}
/**
* {@inheritDoc}
*/
public function clear()
{
$this->stateMachines = array();
}
/**
* Adds a new config
*
* @param array $config
* @param string $graph
*
* @throws SMException If the index "class" is not configured
*/
public function addConfig(array $config, $graph = 'default')
{
if (!isset($config['graph'])) {
$config['graph'] = $graph;
}
if (!isset($config['class'])) {
throw new SMException(sprintf(
'Index "class" needed for the state machine configuration of graph "%s"',
$config['graph']
));
}
$this->configs[] = $config;
}
/**
* Create a state machine for the given object and config
*
* @param $object
* @param array $config
*
* @return StateMachineInterface
*/
abstract protected function createStateMachine($object, array $config);
}

View File

@@ -0,0 +1,20 @@
<?php
/*
* This file is part of the StateMachine package.
*
* (c) Alexandre Bacco
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SM\Factory;
interface ClearableFactoryInterface extends FactoryInterface
{
/**
* Clears all state machines from the factory
*/
public function clear();
}

View File

@@ -0,0 +1,59 @@
<?php
/*
* This file is part of the StateMachine package.
*
* (c) Alexandre Bacco
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SM\Factory;
use SM\Callback\CallbackFactoryInterface;
use SM\SMException;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class Factory extends AbstractFactory
{
/**
* @var EventDispatcherInterface
*/
protected $dispatcher;
/**
* @var CallbackFactoryInterface
*/
protected $callbackFactory;
public function __construct(
array $configs,
EventDispatcherInterface $dispatcher = null,
CallbackFactoryInterface $callbackFactory = null
) {
parent::__construct($configs);
$this->dispatcher = $dispatcher;
$this->callbackFactory = $callbackFactory;
}
/**
* {@inheritDoc}
*/
protected function createStateMachine($object, array $config)
{
if (!isset($config['state_machine_class'])) {
$class = 'SM\\StateMachine\\StateMachine';
} elseif (class_exists($config['state_machine_class'])) {
$class = $config['state_machine_class'];
} else {
throw new SMException(sprintf(
'Class "%s" for creating a new state machine does not exist.',
$config['state_machine_class']
));
}
return new $class($object, $config, $this->dispatcher, $this->callbackFactory);
}
}

View File

@@ -0,0 +1,27 @@
<?php
/*
* This file is part of the StateMachine package.
*
* (c) Alexandre Bacco
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SM\Factory;
use SM\StateMachine\StateMachineInterface;
interface FactoryInterface
{
/**
* Returns the state machine match the couple object/graph
*
* @param object $object
* @param string $graph
*
* @return StateMachineInterface
*/
public function get($object, $graph = 'default');
}

View File

@@ -0,0 +1,16 @@
<?php
/*
* This file is part of the StateMachine package.
*
* (c) Alexandre Bacco
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SM;
class SMException extends \Exception
{
}

View File

@@ -0,0 +1,235 @@
<?php
/*
* This file is part of the StateMachine package.
*
* (c) Alexandre Bacco
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SM\StateMachine;
use SM\Callback\CallbackFactory;
use SM\Callback\CallbackFactoryInterface;
use SM\Callback\CallbackInterface;
use SM\Event\SMEvents;
use SM\Event\TransitionEvent;
use SM\SMException;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
use Symfony\Component\PropertyAccess\PropertyAccessor;
class StateMachine implements StateMachineInterface
{
/**
* @var object
*/
protected $object;
/**
* @var array
*/
protected $config;
/**
* @var EventDispatcherInterface
*/
protected $dispatcher;
/**
* @var CallbackFactoryInterface
*/
protected $callbackFactory;
/**
* @param object $object Underlying object for the state machine
* @param array $config Config array of the graph
* @param EventDispatcherInterface $dispatcher EventDispatcher or null not to dispatch events
* @param CallbackFactoryInterface $callbackFactory CallbackFactory or null to use the default one
*
* @throws SMException If object doesn't have configured property path for state
*/
public function __construct(
$object,
array $config,
EventDispatcherInterface $dispatcher = null,
CallbackFactoryInterface $callbackFactory = null
) {
$this->object = $object;
$this->dispatcher = $dispatcher;
$this->callbackFactory = $callbackFactory ?: new CallbackFactory('SM\Callback\Callback');
if (!isset($config['property_path'])) {
$config['property_path'] = 'state';
}
$this->config = $config;
// Test if the given object has the given state property path
try {
$this->getState();
} catch (NoSuchPropertyException $e) {
throw new SMException(sprintf(
'Cannot access to configured property path "%s" on object %s with graph "%s"',
$config['property_path'],
get_class($object),
$config['graph']
));
}
}
/**
* {@inheritDoc}
*/
public function can($transition)
{
if (!isset($this->config['transitions'][$transition])) {
throw new SMException(sprintf(
'Transition "%s" does not exist on object "%s" with graph "%s"',
$transition,
get_class($this->object),
$this->config['graph']
));
}
if (!in_array($this->getState(), $this->config['transitions'][$transition]['from'])) {
return false;
}
$can = true;
$event = new TransitionEvent($transition, $this->getState(), $this->config['transitions'][$transition], $this);
if (null !== $this->dispatcher) {
$this->dispatcher->dispatch(SMEvents::TEST_TRANSITION, $event);
$can = !$event->isRejected();
}
return $can && $this->callCallbacks($event, 'guard');
}
/**
* {@inheritDoc}
*/
public function apply($transition, $soft = false)
{
if (!$this->can($transition)) {
if ($soft) {
return false;
}
throw new SMException(sprintf(
'Transition "%s" cannot be applied on state "%s" of object "%s" with graph "%s"',
$transition,
$this->getState(),
get_class($this->object),
$this->config['graph']
));
}
$event = new TransitionEvent($transition, $this->getState(), $this->config['transitions'][$transition], $this);
if (null !== $this->dispatcher) {
$this->dispatcher->dispatch(SMEvents::PRE_TRANSITION, $event);
if ($event->isRejected()) {
return false;
}
}
$this->callCallbacks($event, 'before');
$this->setState($this->config['transitions'][$transition]['to']);
$this->callCallbacks($event, 'after');
if (null !== $this->dispatcher) {
$this->dispatcher->dispatch(SMEvents::POST_TRANSITION, $event);
}
return true;
}
/**
* {@inheritDoc}
*/
public function getState()
{
$accessor = new PropertyAccessor();
return $accessor->getValue($this->object, $this->config['property_path']);
}
/**
* {@inheritDoc}
*/
public function getObject()
{
return $this->object;
}
/**
* {@inheritDoc}
*/
public function getGraph()
{
return $this->config['graph'];
}
/**
* {@inheritDoc}
*/
public function getPossibleTransitions()
{
return array_filter(
array_keys($this->config['transitions']),
array($this, 'can')
);
}
/**
* Set a new state to the underlying object
*
* @param string $state
*
* @throws SMException
*/
protected function setState($state)
{
if (!in_array($state, $this->config['states'])) {
throw new SMException(sprintf(
'Cannot set the state to "%s" to object "%s" with graph %s because it is not pre-defined.',
$state,
get_class($this->object),
$this->config['graph']
));
}
$accessor = new PropertyAccessor();
$accessor->setValue($this->object, $this->config['property_path'], $state);
}
/**
* Builds and calls the defined callbacks
*
* @param TransitionEvent $event
* @param string $position
* @return bool
*/
protected function callCallbacks(TransitionEvent $event, $position)
{
if (!isset($this->config['callbacks'][$position])) {
return true;
}
$result = true;
foreach ($this->config['callbacks'][$position] as &$callback) {
if (!$callback instanceof CallbackInterface) {
$callback = $this->callbackFactory->get($callback);
}
$result = call_user_func($callback, $event) && $result;
}
return $result;
}
}

View File

@@ -0,0 +1,68 @@
<?php
/*
* This file is part of the StateMachine package.
*
* (c) Alexandre Bacco
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SM\StateMachine;
use SM\SMException;
interface StateMachineInterface
{
/**
* Can the transition be applied on the underlying object
*
* @param string $transition
*
* @return bool
*
* @throws SMException If transition doesn't exist
*/
public function can($transition);
/**
* Applies the transition on the underlying object
*
* @param string $transition Transition to apply
* @param bool $soft Soft means do nothing if transition can't be applied (no exception thrown)
*
* @return bool If the transition has been applied or not (in case of soft apply or rejected pre transition event)
*
* @throws SMException If transition can't be applied or doesn't exist
*/
public function apply($transition, $soft = false);
/**
* Returns the current state
*
* @return string
*/
public function getState();
/**
* Returns the underlying object
*
* @return object
*/
public function getObject();
/**
* Returns the current graph
*
* @return string
*/
public function getGraph();
/**
* Returns the possible transitions
*
* @return array
*/
public function getPossibleTransitions();
}