├── .gitignore ├── .scrutinizer.yml ├── tests ├── TestCase.php ├── Fixtures │ └── SebdesignExample.php ├── phpcs.xml.dist └── Feature │ └── FiniteStateMachineTest.php ├── src ├── Contracts │ ├── EventDispatcher.php │ ├── ObjectProxyFactory.php │ ├── Loader.php │ ├── GuardCallback.php │ ├── GuardCallbackFactory.php │ ├── TransitionEventCallbackFactory.php │ ├── ObjectProxy.php │ ├── TransitionEventCallback.php │ ├── FiniteStateMachine.php │ └── Event.php ├── Event │ ├── Applied.php │ ├── Started.php │ ├── NullEventDispatcher.php │ ├── TransitionEventDispatcher.php │ └── EventBehavior.php ├── Guard │ ├── CallableGuardCallbackFactory.php │ ├── CallableGuardCallback.php │ ├── GuardCollection.php │ ├── GuardCallbackResolver.php │ └── Guard.php ├── Graph │ ├── GraphResolver.php │ ├── GraphCollection.php │ └── Graph.php ├── TransitionEvent │ ├── CallableTransitionEventCallbackFactory.php │ ├── CallableTransitionEventCallback.php │ ├── TransitionEventCallbackResolver.php │ ├── TransitionEventCollection.php │ └── TransitionEvent.php ├── Testing │ └── RecordOnlyEventDispatcher.php ├── State │ ├── StateCollection.php │ └── State.php ├── ObjectProxy │ ├── PropertyObjectProxy.php │ ├── ObjectProxyResolver.php │ └── PropertyObjectProxyFactory.php ├── FiniteStateMachineFactory.php ├── Transition │ ├── Transition.php │ └── TransitionCollection.php ├── Loader │ └── WinzouArrayLoader.php └── FiniteStateMachine.php ├── .editorconfig ├── psalm.xml ├── phpcs.xml.dist ├── phpunit.xml ├── composer.json ├── LICENSE ├── .github └── workflows │ └── ci.yml ├── README.md └── composer.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | tools: 2 | external_code_coverage: 3 | timeout: 600 4 | build: 5 | environment: 6 | php: 7.4.5 7 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | state = $state; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Event/Applied.php: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/Contracts/FiniteStateMachine.php: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ./src 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Guard/CallableGuardCallbackFactory.php: -------------------------------------------------------------------------------- 1 | graphCollection = new GraphCollection(); 14 | } 15 | 16 | public function register(Graph $graph): void 17 | { 18 | $this->graphCollection->add($graph); 19 | } 20 | 21 | public function resolve(object $object, string $graph = 'default'): Graph 22 | { 23 | return $this->graphCollection->for($object, $graph); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | . 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/TransitionEvent/CallableTransitionEventCallbackFactory.php: -------------------------------------------------------------------------------- 1 | history[] = $event; 20 | } 21 | 22 | public function history(): array 23 | { 24 | return $this->history; 25 | } 26 | 27 | public function flush(): void 28 | { 29 | $this->history = []; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | ./tests/Unit 9 | 10 | 11 | 12 | ./tests/Feature 13 | 14 | 15 | 16 | 17 | ./src 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Guard/CallableGuardCallback.php: -------------------------------------------------------------------------------- 1 | callable = $callable; 24 | } 25 | 26 | public function __invoke(object $object, Transition $transition, State $fromState, State $toState): bool 27 | { 28 | return ($this->callable)($object, $transition, $fromState, $toState); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dflydev/finite-state-machine", 3 | "description": "Yet another finite-state machine implementation", 4 | "type": "library", 5 | "keywords": ["finite-state machine", "fsm", "state machine"], 6 | "require": { 7 | "php": "^7.4|^8.0" 8 | }, 9 | "require-dev": { 10 | "phpunit/phpunit": "^9.1", 11 | "psalm/phar": "^3.11", 12 | "squizlabs/php_codesniffer": "^3.5" 13 | }, 14 | "autoload": { 15 | "psr-4": { 16 | "Dflydev\\FiniteStateMachine\\": "src/" 17 | } 18 | }, 19 | "autoload-dev": { 20 | "psr-4": { 21 | "Tests\\": "tests/" 22 | } 23 | }, 24 | "license": "MIT", 25 | "authors": [ 26 | { 27 | "name": "Beau Simensen", 28 | "email": "beau@dflydev.com" 29 | } 30 | ], 31 | "extra": { 32 | "branch-alias": { 33 | "dev-master": "0.0-dev" 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Event/TransitionEventDispatcher.php: -------------------------------------------------------------------------------- 1 | eventDispatcher = $eventDispatcher; 19 | $this->transitionEventCollection = $transitionEventCollection; 20 | } 21 | 22 | public function dispatch(Event $event): void 23 | { 24 | $this->transitionEventCollection->fireIfMatches($event); 25 | $this->eventDispatcher->dispatch($event); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/TransitionEvent/CallableTransitionEventCallback.php: -------------------------------------------------------------------------------- 1 | callable = $callable; 26 | } 27 | 28 | public function __invoke( 29 | string $when, 30 | object $object, 31 | Transition $transition, 32 | State $fromState, 33 | State $toState 34 | ): void { 35 | ($this->callable)($when, $object, $transition, $fromState, $toState); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/State/StateCollection.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | private array $states = []; 13 | 14 | /** 15 | * @var array 16 | */ 17 | private array $statesByName = []; 18 | 19 | public function __construct(State ...$states) 20 | { 21 | foreach ($states as $state) { 22 | $this->add($state); 23 | } 24 | } 25 | 26 | public function add(State $state): void 27 | { 28 | $this->states[] = $state; 29 | $this->statesByName[$state->name()] = $state; 30 | } 31 | 32 | public function named(string $name): State 33 | { 34 | if (! isset($this->statesByName[$name])) { 35 | throw new \RuntimeException(sprintf('No state named "%s"', $name)); 36 | } 37 | 38 | return $this->statesByName[$name]; 39 | } 40 | 41 | public function names(): array 42 | { 43 | return array_keys($this->statesByName); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Dragonfly Development Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/ObjectProxy/PropertyObjectProxy.php: -------------------------------------------------------------------------------- 1 | object = $object; 20 | $this->property = $property; 21 | } 22 | 23 | public function object(): object 24 | { 25 | return $this->object; 26 | } 27 | 28 | public function state(): ?string 29 | { 30 | return $this->property->getValue($this->object); 31 | } 32 | 33 | public function apply( 34 | Transition $transition, 35 | State $fromState, 36 | State $toState 37 | ): void { 38 | $this->property->setValue($this->object, $toState->name()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Guard/GuardCollection.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | private array $guards = []; 16 | 17 | /** 18 | * @var array 19 | */ 20 | private array $guardsByName = []; 21 | 22 | public function __construct(Guard ...$guards) 23 | { 24 | foreach ($guards as $guard) { 25 | $this->add($guard); 26 | } 27 | } 28 | 29 | public function add(Guard $guard): void 30 | { 31 | $this->guards[] = $guard; 32 | $this->guardsByName[$guard->name()] = $guard; 33 | } 34 | 35 | public function cannot(object $object, Transition $transition, State $fromState, State $toState): bool 36 | { 37 | foreach ($this->guards as $guard) { 38 | if ($guard->cannot($object, $transition, $fromState, $toState)) { 39 | return true; 40 | } 41 | } 42 | 43 | return false; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/ObjectProxy/ObjectProxyResolver.php: -------------------------------------------------------------------------------- 1 | objectProxyFactories = $objectProxyFactories; 20 | } 21 | 22 | public function add(ObjectProxyFactory $objectProxyFactory): void 23 | { 24 | $this->objectProxyFactories[] = $objectProxyFactory; 25 | } 26 | 27 | public function resolve(object $object, array $options = []): ObjectProxy 28 | { 29 | foreach ($this->objectProxyFactories as $objectProxyFactory) { 30 | if ($objectProxyFactory->supports($object, $options)) { 31 | return $objectProxyFactory->build($object, $options); 32 | } 33 | } 34 | 35 | throw new \RuntimeException(sprintf('No object proxy found for "%s"', get_class($object))); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/ObjectProxy/PropertyObjectProxyFactory.php: -------------------------------------------------------------------------------- 1 | defaultPropertyName = $defaultPropertyName; 19 | } 20 | 21 | public function build(object $object, array $options): ObjectProxy 22 | { 23 | $property = new ReflectionProperty($object, $options['property_path'] ?? $this->defaultPropertyName); 24 | $property->setAccessible(true); 25 | 26 | return new PropertyObjectProxy( 27 | $object, 28 | $property 29 | ); 30 | } 31 | 32 | public function supports(object $object, array $options): bool 33 | { 34 | $class = new ReflectionClass($object); 35 | 36 | return $class->hasProperty($options['property_path'] ?? $this->defaultPropertyName); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/State/State.php: -------------------------------------------------------------------------------- 1 | type = $type; 20 | $this->name = $name; 21 | $this->metadata = $metadata; 22 | } 23 | 24 | public function name(): string 25 | { 26 | return $this->name; 27 | } 28 | 29 | public function metadata(): array 30 | { 31 | return $this->metadata; 32 | } 33 | 34 | public static function initialTyped(string $name, array $metadata): self 35 | { 36 | return new static(static::INITIAL, $name, $metadata); 37 | } 38 | 39 | public static function normalTyped(string $name, array $metadata): self 40 | { 41 | return new static(static::NORMAL, $name, $metadata); 42 | } 43 | 44 | public static function finalTyped(string $name, array $metadata): self 45 | { 46 | return new static(static::FINAL, $name, $metadata); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/FiniteStateMachineFactory.php: -------------------------------------------------------------------------------- 1 | graphResolver = $graphResolver; 23 | $this->objectProxyResolver = $objectProxyResolver; 24 | $this->eventDispatcher = $eventDispatcher; 25 | } 26 | 27 | public function build(object $object, string $graph = 'default'): FiniteStateMachine 28 | { 29 | $graph = $this->graphResolver->resolve($object, $graph); 30 | $objectProxy = $this->objectProxyResolver->resolve($object, $graph->objectProxyOptions()); 31 | 32 | return new FiniteStateMachine($objectProxy, $graph, $this->eventDispatcher); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Event/EventBehavior.php: -------------------------------------------------------------------------------- 1 | graph = $graph; 22 | $this->object = $object; 23 | $this->transition = $transition; 24 | $this->fromState = $fromState; 25 | $this->toState = $toState; 26 | } 27 | 28 | public function graph(): Graph 29 | { 30 | return $this->graph; 31 | } 32 | 33 | public function object(): object 34 | { 35 | return $this->object; 36 | } 37 | 38 | public function transition(): Transition 39 | { 40 | return $this->transition; 41 | } 42 | 43 | public function fromState(): State 44 | { 45 | return $this->fromState; 46 | } 47 | 48 | public function toState(): State 49 | { 50 | return $this->toState; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Guard/GuardCallbackResolver.php: -------------------------------------------------------------------------------- 1 | guardCallbackFactories = $guardCallbackFactories + static::defaultGuardCallbackFactories(); 17 | } 18 | 19 | public function register(GuardCallbackFactory $guardCallbackFactory): void 20 | { 21 | $this->guardCallbackFactories[] = $guardCallbackFactory; 22 | } 23 | 24 | /** 25 | * @param mixed $do 26 | */ 27 | public function resolve($do): GuardCallback 28 | { 29 | foreach ($this->guardCallbackFactories as $guardCallbackFactory) { 30 | if ($guardCallbackFactory->supports($do)) { 31 | return $guardCallbackFactory->build($do); 32 | } 33 | } 34 | 35 | throw new \RuntimeException('Could not resolve guard callback'); 36 | } 37 | 38 | public static function defaultGuardCallbackFactories(): array 39 | { 40 | return [ 41 | new CallableGuardCallbackFactory(), 42 | ]; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Graph/GraphCollection.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | private array $graphs = []; 13 | 14 | /** 15 | * @var array> 16 | */ 17 | private array $graphsByName = []; 18 | 19 | public function __construct(Graph ...$graphs) 20 | { 21 | foreach ($graphs as $graph) { 22 | $this->add($graph); 23 | } 24 | } 25 | 26 | public function add(Graph $graph): void 27 | { 28 | $this->graphs[] = $graph; 29 | 30 | if (!array_key_exists($graph->className(), $this->graphsByName)) { 31 | $this->graphsByName[$graph->className()] = []; 32 | } 33 | 34 | $this->graphsByName[$graph->className()][$graph->graph()] = $graph; 35 | } 36 | 37 | public function for(object $object, string $graph = 'default'): Graph 38 | { 39 | $className = get_class($object); 40 | 41 | foreach ($this->graphsByName as $classNameForGraph => $graphs) { 42 | if (! $object instanceof $classNameForGraph) { 43 | continue; 44 | } 45 | 46 | if (isset($graphs[$graph])) { 47 | return $graphs[$graph]; 48 | } 49 | } 50 | 51 | throw new \RuntimeException(sprintf( 52 | 'No graph named "%s" for class named "%s"', 53 | $graph, 54 | $className 55 | )); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Transition/Transition.php: -------------------------------------------------------------------------------- 1 | name = $name; 19 | $this->toStateName = $toStateName; 20 | $this->fromStateNames = $fromStateNames; 21 | $this->metadata = $metadata; 22 | } 23 | 24 | public function name(): string 25 | { 26 | return $this->name; 27 | } 28 | 29 | public function metadata(): array 30 | { 31 | return $this->metadata; 32 | } 33 | 34 | public function toStateName(): string 35 | { 36 | return $this->toStateName; 37 | } 38 | 39 | public function canTransitionFrom(State $state): bool 40 | { 41 | foreach ($this->fromStateNames as $fromStateName) { 42 | if ($state->name() === $fromStateName) { 43 | return true; 44 | } 45 | } 46 | 47 | return false; 48 | } 49 | 50 | public function cannotTransitionFrom(State $state): bool 51 | { 52 | foreach ($this->fromStateNames as $fromStateName) { 53 | if ($state->name() === $fromStateName) { 54 | return false; 55 | } 56 | } 57 | 58 | return true; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/TransitionEvent/TransitionEventCallbackResolver.php: -------------------------------------------------------------------------------- 1 | transitionEventCallbackFactories = $transitionEventCallbackFactories + static::defaultTransitionEventCallbackFactories(); 17 | } 18 | 19 | public function register(TransitionEventCallbackFactory $transitionEventCallbackFactory): void 20 | { 21 | $this->transitionEventCallbackFactories[] = $transitionEventCallbackFactory; 22 | } 23 | 24 | /** 25 | * @param mixed $do 26 | */ 27 | public function resolve($do): TransitionEventCallback 28 | { 29 | foreach ($this->transitionEventCallbackFactories as $transitionEventCallbackFactory) { 30 | if ($transitionEventCallbackFactory->supports($do)) { 31 | return $transitionEventCallbackFactory->build($do); 32 | } 33 | } 34 | 35 | throw new \RuntimeException('Could not resolve transition event callback'); 36 | } 37 | 38 | public static function defaultTransitionEventCallbackFactories(): array 39 | { 40 | return [ 41 | new CallableTransitionEventCallbackFactory(), 42 | ]; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Transition/TransitionCollection.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | private array $transitions = []; 15 | 16 | /** 17 | * @var array 18 | */ 19 | private array $transitionsByName = []; 20 | 21 | public function __construct(Transition ...$transitions) 22 | { 23 | foreach ($transitions as $transition) { 24 | $this->add($transition); 25 | } 26 | } 27 | 28 | public function add(Transition $transition): void 29 | { 30 | $this->transitions[] = $transition; 31 | $this->transitionsByName[$transition->name()] = $transition; 32 | } 33 | 34 | public function named(string $name): Transition 35 | { 36 | if (! isset($this->transitionsByName[$name])) { 37 | throw new \RuntimeException(sprintf('No transition named "%s"', $name)); 38 | } 39 | 40 | return $this->transitionsByName[$name]; 41 | } 42 | 43 | public function names(): array 44 | { 45 | return array_keys($this->transitionsByName); 46 | } 47 | 48 | public function fromState(State $state): self 49 | { 50 | /** @var Transition[] $transitions */ 51 | $transitions = array_filter( 52 | $this->transitions, 53 | fn(Transition $transition) => $transition->canTransitionFrom($state) 54 | ); 55 | 56 | return new static(...$transitions); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/TransitionEvent/TransitionEventCollection.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | private array $transitionEvents = []; 15 | 16 | /** 17 | * @var array > 18 | */ 19 | private array $transitionEventsByWhenAndName = []; 20 | 21 | public function __construct(TransitionEvent ...$transitionEvents) 22 | { 23 | foreach ($transitionEvents as $transitionEvent) { 24 | $this->add($transitionEvent); 25 | } 26 | } 27 | 28 | public function add(TransitionEvent $transitionEvent): void 29 | { 30 | $this->transitionEvents[] = $transitionEvent; 31 | 32 | if (!array_key_exists($transitionEvent->when(), $this->transitionEventsByWhenAndName)) { 33 | $this->transitionEventsByWhenAndName[$transitionEvent->when()] = []; 34 | } 35 | 36 | $this->transitionEventsByWhenAndName[$transitionEvent->when()][$transitionEvent->name()] = $transitionEvent; 37 | } 38 | 39 | public function fireIfMatches(Event $event): void 40 | { 41 | if (! isset($this->transitionEventsByWhenAndName[$event->when()])) { 42 | return; 43 | } 44 | 45 | /** @var TransitionEvent[] $transitionEvents */ 46 | $transitionEvents = $this->transitionEventsByWhenAndName[$event->when()]; 47 | 48 | foreach ($transitionEvents as $transitionEvent) { 49 | $transitionEvent->fireIfMatches( 50 | $event->when(), 51 | $event->object(), 52 | $event->transition(), 53 | $event->fromState(), 54 | $event->toState() 55 | ); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Guard/Guard.php: -------------------------------------------------------------------------------- 1 | name = $name; 27 | $this->transitionNames = $transitionNames; 28 | $this->fromStateNames = $fromStateNames; 29 | $this->toStateNames = $toStateNames; 30 | $this->callback = $callback; 31 | } 32 | 33 | public function name(): string 34 | { 35 | return $this->name; 36 | } 37 | 38 | public function cannot(object $object, Transition $transition, State $fromState, State $toState): bool 39 | { 40 | $matches = []; 41 | 42 | if (count($this->transitionNames) > 0) { 43 | $matches[] = in_array($transition->name(), $this->transitionNames); 44 | } 45 | 46 | if (count($this->fromStateNames) > 0) { 47 | $matches[] = in_array($fromState->name(), $this->fromStateNames); 48 | } 49 | 50 | if (count($this->toStateNames) > 0) { 51 | $matches[] = in_array($toState->name(), $this->toStateNames); 52 | } 53 | 54 | if (count($matches) === 0) { 55 | return false; 56 | } 57 | 58 | if (in_array(false, $matches)) { 59 | return false; 60 | } 61 | 62 | return ! $this->callback->__invoke($object, $transition, $fromState, $toState); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Graph/Graph.php: -------------------------------------------------------------------------------- 1 | className = $className; 34 | $this->graph = $graph; 35 | $this->objectProxyOptions = $objectProxyOptions; 36 | $this->metadata = $metadata; 37 | $this->stateCollection = $stateCollection; 38 | $this->transitionCollection = $transitionCollection; 39 | $this->guardCollection = $guardCollection; 40 | $this->transitionEventCollection = $transitionEventCollection; 41 | } 42 | 43 | public function className(): string 44 | { 45 | return $this->className; 46 | } 47 | 48 | public function graph(): string 49 | { 50 | return $this->graph; 51 | } 52 | 53 | public function objectProxyOptions(): array 54 | { 55 | return $this->objectProxyOptions; 56 | } 57 | 58 | public function metadata(): array 59 | { 60 | return $this->metadata; 61 | } 62 | 63 | public function stateCollection(): StateCollection 64 | { 65 | return $this->stateCollection; 66 | } 67 | 68 | public function transitionCollection(): TransitionCollection 69 | { 70 | return $this->transitionCollection; 71 | } 72 | 73 | public function guardCollection(): GuardCollection 74 | { 75 | return $this->guardCollection; 76 | } 77 | 78 | public function transitionEventCollection(): TransitionEventCollection 79 | { 80 | return $this->transitionEventCollection; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/TransitionEvent/TransitionEvent.php: -------------------------------------------------------------------------------- 1 | when = $when; 29 | $this->name = $name; 30 | $this->transitionNames = $transitionNames; 31 | $this->fromStateNames = $fromStateNames; 32 | $this->toStateNames = $toStateNames; 33 | $this->transitionEventCallback = $transitionEventCallback; 34 | } 35 | 36 | public function when(): string 37 | { 38 | return $this->when; 39 | } 40 | 41 | public function name(): string 42 | { 43 | return $this->name; 44 | } 45 | 46 | public function transitionNames(): array 47 | { 48 | return $this->transitionNames; 49 | } 50 | 51 | public function fromStateNames(): array 52 | { 53 | return $this->fromStateNames; 54 | } 55 | 56 | public function toStateNames(): array 57 | { 58 | return $this->toStateNames; 59 | } 60 | 61 | public function transitionEventCallback(): TransitionEventCallback 62 | { 63 | return $this->transitionEventCallback; 64 | } 65 | 66 | public function fireIfMatches( 67 | string $when, 68 | object $object, 69 | Transition $transition, 70 | State $fromState, 71 | State $toState 72 | ): void { 73 | if ($when !== $this->when) { 74 | return; 75 | } 76 | 77 | $matches = []; 78 | 79 | if (count($this->transitionNames) > 0) { 80 | $matches[] = in_array($transition->name(), $this->transitionNames); 81 | } 82 | 83 | if (count($this->fromStateNames) > 0) { 84 | $matches[] = in_array($fromState->name(), $this->fromStateNames); 85 | } 86 | 87 | if (count($this->toStateNames) > 0) { 88 | $matches[] = in_array($toState->name(), $this->toStateNames); 89 | } 90 | 91 | if (count($matches) === 0) { 92 | return; 93 | } 94 | 95 | if (in_array(false, $matches)) { 96 | return; 97 | } 98 | 99 | $this->transitionEventCallback->__invoke($when, $object, $transition, $fromState, $toState); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build Status 2 | on: 3 | push: 4 | paths-ignore: 5 | - '**.md' 6 | - 'examples/**' 7 | pull_request: 8 | paths-ignore: 9 | - '**.md' 10 | - 'examples/**' 11 | jobs: 12 | phpunit: 13 | name: PHPUnit (PHP ${{ matrix.php_versions }}) 14 | runs-on: ubuntu-latest 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | php_versions: ['7.4'] 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v2 22 | - name: Setup PHP, with composer and extensions 23 | uses: shivammathur/setup-php@v2 24 | with: 25 | php-version: ${{ matrix.php_versions }} 26 | coverage: pcov 27 | - name: Get composer cache directory 28 | id: composer_cache 29 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 30 | - name: Cache dependencies 31 | uses: actions/cache@v1 32 | with: 33 | path: ${{ steps.composer_cache.outputs.dir }} 34 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} 35 | restore-keys: ${{ runner.os }}-composer- 36 | - name: Install Composer dependencies 37 | run: composer install --no-progress --no-suggest --prefer-dist --optimize-autoloader 38 | - name: Test with PHPUnit 39 | run: ./vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover 40 | - name: Send to Scrutinizer 41 | run: | 42 | wget https://scrutinizer-ci.com/ocular.phar 43 | php ocular.phar code-coverage:upload --format=php-clover coverage.clover 44 | - name: Send to Code Climate 45 | uses: paambaati/codeclimate-action@v2.5.6 46 | env: 47 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} 48 | with: 49 | coverageCommand: true 50 | coverageLocations: 51 | "${{github.workspace}}/coverage.clover:clover" 52 | psalm: 53 | name: Psalm 54 | runs-on: ubuntu-latest 55 | needs: phpunit 56 | strategy: 57 | fail-fast: false 58 | matrix: 59 | php_versions: ['7.4'] 60 | steps: 61 | - name: Checkout 62 | uses: actions/checkout@v2 63 | - name: Setup PHP, with composer and extensions 64 | uses: shivammathur/setup-php@v2 65 | with: 66 | php-version: ${{ matrix.php_versions }} 67 | - name: Install dependencies 68 | run: composer install --no-progress --no-suggest --prefer-dist 69 | - name: Analyze with Psalm 70 | run: ./vendor/bin/psalm.phar 71 | phpcs: 72 | name: PHP_CodeSniffer 73 | runs-on: ubuntu-latest 74 | needs: phpunit 75 | strategy: 76 | fail-fast: false 77 | matrix: 78 | php_versions: ['7.4'] 79 | steps: 80 | - name: Checkout 81 | uses: actions/checkout@v2 82 | - name: Setup PHP, with composer and extensions 83 | uses: shivammathur/setup-php@v2 84 | with: 85 | php-version: ${{ matrix.php_versions }} 86 | - name: Install dependencies 87 | run: composer install --no-progress --no-suggest --prefer-dist 88 | - name: Check for coding standard violations 89 | run: ./vendor/bin/phpcs 90 | - name: Check for coding standard violations (tests) 91 | run: ./vendor/bin/phpcs --standard=tests/phpcs.xml.dist 92 | -------------------------------------------------------------------------------- /src/Loader/WinzouArrayLoader.php: -------------------------------------------------------------------------------- 1 | graphResolver = $graphResolver; 33 | $this->guardCallbackResolver = $guardCallbackResolver ?? new GuardCallbackResolver(); 34 | $this->transitionEventCallbackResolver = $transitionEventCallbackResolver ?? new TransitionEventCallbackResolver(); 35 | } 36 | 37 | public function load($resource): void 38 | { 39 | $stateCollection = new StateCollection(); 40 | $transitionCollection = new TransitionCollection(); 41 | $guardCollection = new GuardCollection(); 42 | $transitionEventCollection = new TransitionEventCollection(); 43 | 44 | foreach ($resource['transitions'] as $name => $transition) { 45 | $transitionCollection->add(new Transition( 46 | $name, 47 | $transition['to'], 48 | $transition['from'], 49 | $transition['metadata'] ?? [] 50 | )); 51 | } 52 | 53 | foreach ($resource['states'] as $state) { 54 | $stateCollection->add(State::normalTyped( 55 | is_array($state) ? $state['name'] : $state, 56 | is_array($state) ? $state['metadata'] ?? [] : [] 57 | )); 58 | } 59 | 60 | if (isset($resource['callbacks']['guard'])) { 61 | foreach ($resource['callbacks']['guard'] as $name => $setup) { 62 | $transitionNames = $setup['on'] ?? []; 63 | $fromStateNames = $setup['from'] ?? []; 64 | $toStateNames = $setup['to'] ?? []; 65 | $do = $setup['do'] ?? fn(): bool => false; 66 | 67 | $guardCollection->add(new Guard( 68 | $name, 69 | is_array($transitionNames) ? $transitionNames : [$transitionNames], 70 | is_array($fromStateNames) ? $fromStateNames : [$fromStateNames], 71 | is_array($toStateNames) ? $toStateNames : [$toStateNames], 72 | $this->guardCallbackResolver->resolve($do) 73 | )); 74 | } 75 | } 76 | 77 | foreach (['before', 'after'] as $when) { 78 | if (isset($resource['callbacks'][$when])) { 79 | foreach ($resource['callbacks'][$when] as $name => $setup) { 80 | $transitionNames = $setup['on'] ?? []; 81 | $fromStateNames = $setup['from'] ?? []; 82 | $toStateNames = $setup['to'] ?? []; 83 | $do = $setup['do'] ?? function (): void { 84 | }; 85 | 86 | $transitionEventCollection->add(new TransitionEvent( 87 | $when, 88 | $name, 89 | is_array($transitionNames) ? $transitionNames : [$transitionNames], 90 | is_array($fromStateNames) ? $fromStateNames : [$fromStateNames], 91 | is_array($toStateNames) ? $toStateNames : [$toStateNames], 92 | $this->transitionEventCallbackResolver->resolve($do) 93 | )); 94 | } 95 | } 96 | } 97 | 98 | $graph = new Graph( 99 | $resource['class'], 100 | $resource['graph'] ?? 'default', 101 | $resource, 102 | $resource['metadata'] ?? [], 103 | $stateCollection, 104 | $transitionCollection, 105 | $guardCollection, 106 | $transitionEventCollection 107 | ); 108 | 109 | $this->graphResolver->register($graph); 110 | } 111 | 112 | public function supports($resource): bool 113 | { 114 | return is_array($resource) && isset($resource['class'], $resource['states'], $resource['transitions']); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/FiniteStateMachine.php: -------------------------------------------------------------------------------- 1 | graph = $graph; 36 | $this->objectProxy = $objectProxy; 37 | $this->stateCollection = $graph->stateCollection(); 38 | $this->transitionCollection = $graph->transitionCollection(); 39 | $this->guardCollection = $graph->guardCollection(); 40 | $this->eventDispatcher = new TransitionEventDispatcher( 41 | $eventDispatcher ?? new NullEventDispatcher(), 42 | $graph->transitionEventCollection() 43 | ); 44 | } 45 | 46 | public function graph(): Graph 47 | { 48 | return $this->graph; 49 | } 50 | 51 | public function can(string $transition): bool 52 | { 53 | $resolvedTransition = $this->resolveTransition($transition); 54 | 55 | $currentState = $this->currentState(); 56 | 57 | if ($resolvedTransition->cannotTransitionFrom($currentState)) { 58 | return false; 59 | } 60 | 61 | $toState = $this->stateCollection->named($resolvedTransition->toStateName()); 62 | 63 | if ($this->guardCollection->cannot($this->objectProxy->object(), $resolvedTransition, $currentState, $toState)) { 64 | return false; 65 | } 66 | 67 | return true; 68 | } 69 | 70 | public function apply(string $transition): void 71 | { 72 | $resolvedTransition = $this->resolveTransition($transition); 73 | 74 | $currentState = $this->currentState(); 75 | 76 | if ($resolvedTransition->cannotTransitionFrom($currentState)) { 77 | throw new \RuntimeException(sprintf('Cannot apply transition "%s"', $resolvedTransition->name())); 78 | } 79 | 80 | $toState = $this->stateCollection->named($resolvedTransition->toStateName()); 81 | 82 | if ($this->guardCollection->cannot($this->objectProxy->object(), $resolvedTransition, $currentState, $toState)) { 83 | throw new \RuntimeException(sprintf('Cannot apply transition "%s"', $resolvedTransition->name())); 84 | } 85 | 86 | $this->eventDispatcher->dispatch(new Started( 87 | $this->graph, 88 | $this->objectProxy->object(), 89 | $resolvedTransition, 90 | $currentState, 91 | $toState 92 | )); 93 | 94 | $this->objectProxy->apply( 95 | $resolvedTransition, 96 | $currentState, 97 | $toState 98 | ); 99 | 100 | $this->eventDispatcher->dispatch(new Applied( 101 | $this->graph, 102 | $this->objectProxy->object(), 103 | $resolvedTransition, 104 | $currentState, 105 | $toState 106 | )); 107 | } 108 | 109 | public function currentState(): State 110 | { 111 | $currentState = $this->objectProxy->state(); 112 | 113 | if (is_null($currentState)) { 114 | throw new \RuntimeException('No state currently set'); 115 | } 116 | 117 | return $this->stateCollection->named($currentState); 118 | } 119 | 120 | public function allStates(): StateCollection 121 | { 122 | return $this->stateCollection; 123 | } 124 | 125 | public function state(string $name): State 126 | { 127 | return $this->stateCollection->named($name); 128 | } 129 | 130 | public function allTransitions(): TransitionCollection 131 | { 132 | return $this->transitionCollection; 133 | } 134 | 135 | public function transition(string $name): Transition 136 | { 137 | return $this->transitionCollection->named($name); 138 | } 139 | 140 | public function availableTransitions(): TransitionCollection 141 | { 142 | return $this->transitionCollection->fromState($this->currentState()); 143 | } 144 | 145 | /** 146 | * @param Transition|string $transitionOrTransitionName 147 | */ 148 | private function resolveTransition($transitionOrTransitionName): Transition 149 | { 150 | if ($transitionOrTransitionName instanceof Transition) { 151 | return $transitionOrTransitionName; 152 | } 153 | 154 | return $this->transitionCollection->named($transitionOrTransitionName); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Finite-State Machine 2 | 3 | This library is yet another finite-state machine implementation. 4 | 5 | ![Build Status](https://github.com/dflydev/dflydev-finite-state-machine/workflows/Build%20Status/badge.svg) 6 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/dflydev/dflydev-finite-state-machine/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/dflydev/dflydev-finite-state-machine/?branch=master) 7 | [![Code Coverage](https://scrutinizer-ci.com/g/dflydev/dflydev-finite-state-machine/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/dflydev/dflydev-finite-state-machine/?branch=master) 8 | [![Code Climate](https://codeclimate.com/github/dflydev/dflydev-finite-state-machine/badges/gpa.svg)](https://codeclimate.com/github/dflydev/dflydev-finite-state-machine) 9 | 10 | ## Installation 11 | 12 | ```bash 13 | composer require dflydev/finite-state-machine 14 | ``` 15 | 16 | ## Usage 17 | 18 | Given the following definition for a domain class: 19 | 20 | ```php 21 | class DomainObject 22 | { 23 | public string $state; 24 | public ?string $spy = null; 25 | 26 | public function __construct(string $state = 'new') 27 | { 28 | $this->state = $state; 29 | } 30 | } 31 | ``` 32 | 33 | Given the following state definition for the "graphA" graph of our domain object: 34 | 35 | ```php 36 | $domainObjectGraphDefinition = [ 37 | 'class' => DomainObject::class, 38 | 'graph' => 'graphA', // default is "default" 39 | 'property_path' => 'state', // Configures `PropertyObjectProxy` 40 | 'metadata' => [ 41 | 'title' => 'Graph A', 42 | ], 43 | 'states' => [ 44 | // a state as associative array 45 | ['name' => 'new'], 46 | // a state as associative array with metadata 47 | [ 48 | 'name' => 'pending_review', 49 | 'metadata' => ['title' => 'Pending Review'], 50 | ], 51 | // states as string 52 | 'awaiting_changes', 53 | 'accepted', 54 | 'published', 55 | 'rejected', 56 | ], 57 | 58 | // list of all possible transitions 59 | 'transitions' => [ 60 | 'create' => [ 61 | 'from' => ['new'], 62 | 'to' => 'pending_review', 63 | ], 64 | 'ask_for_changes' => [ 65 | 'from' => ['pending_review', 'accepted'], 66 | 'to' => 'awaiting_changes', 67 | 'metadata' => ['title' => 'Ask for changes'], 68 | ], 69 | 'cancel_changes' => [ 70 | 'from' => ['awaiting_changes'], 71 | 'to' => 'pending_review', 72 | ], 73 | 'submit_changes' => [ 74 | 'from' => ['awaiting_changes'], 75 | 'to' => 'pending_review', 76 | ], 77 | 'approve' => [ 78 | 'from' => ['pending_review', 'rejected'], 79 | 'to' => 'accepted', 80 | ], 81 | 'publish' => [ 82 | 'from' => ['accepted'], 83 | 'to' => 'published', 84 | ], 85 | ], 86 | 87 | // list of all callbacks 88 | 'callbacks' => [ 89 | // will be called when testing a transition 90 | 'guard' => [ 91 | 'guard_on_approving_from_rejected' => [ 92 | // call the callback on a specific transition 93 | 'on' => 'approve', 94 | 'from' => 'rejected', 95 | // will call the method of this class 96 | 'do' => function ( 97 | object $object, 98 | Transition $transition, 99 | State $fromState, 100 | State $toState 101 | ) { 102 | $object->spy = 'guard_on_approving_from_rejected'; 103 | 104 | // If a guard returns false, the transition will not happen 105 | return false; 106 | }, 107 | // arguments for the callback 108 | 'args' => ['object'], 109 | ], 110 | ], 111 | 112 | // will be called before applying a transition 113 | 'before' => [ 114 | 'spy-before-approve' => [ 115 | 'on' => 'ask_for_changes', 116 | 'from' => 'accepted', 117 | 'do' => function ( 118 | string $when, 119 | object $object, 120 | Transition $transition, 121 | State $fromState, 122 | State $toState 123 | ) { 124 | Assert::equals($fromState->name(), $object->state); 125 | 126 | $object->spy = $when . ' ask_for_changes from accepted'; 127 | }, 128 | ] 129 | ], 130 | 131 | // will be called after applying a transition 132 | 'after' => [ 133 | 'spy-after-approve' => [ 134 | 'on' => 'ask_for_changes', 135 | 'from' => 'accepted', 136 | 'do' => function ( 137 | string $when, 138 | object $object, 139 | Transition $transition, 140 | State $fromState, 141 | State $toState 142 | ) { 143 | Assert::equals($toState->name(), $object->state); 144 | Assert::equals('before ask_for_changes from accepted', $object->spy); 145 | 146 | $object->spy = $when . ' ask_for_changes from accepted'; 147 | }, 148 | ] 149 | ], 150 | ] 151 | ]; 152 | ``` 153 | 154 | ```php 155 | use Dflydev\FiniteStateMachine\FiniteStateMachineFactory; 156 | use Dflydev\FiniteStateMachine\Graph\GraphResolver; 157 | use Dflydev\FiniteStateMachine\Loader\WinzouArrayLoader; 158 | use Dflydev\FiniteStateMachine\ObjectProxy\ObjectProxyResolver; 159 | use Dflydev\FiniteStateMachine\ObjectProxy\PropertyObjectProxyFactory; 160 | 161 | $graphResolver = new GraphResolver(); 162 | $objectProxyResolver = new ObjectProxyResolver(); 163 | 164 | // Add an object proxy that can directly read the state property from our objects 165 | $objectProxyResolver->add(new PropertyObjectProxyFactory()); 166 | 167 | // Load a graph definition into our graph resolver 168 | (new WinzouArrayLoader($graphResolver))->load($domainObjectGraphDefinition); 169 | 170 | $finiteStateMachineFactory = new FiniteStateMachineFactory( 171 | $this->getGraphResolver(), 172 | $this->getObjectProxyResolver() 173 | ); 174 | 175 | $finiteStateMachine = $finiteStateMachineFactory->build($object); 176 | 177 | // "new" 178 | $finiteStateMachine->currentState()->name(); 179 | 180 | // (bool) false 181 | $finiteStateMachine->can('ask_for_changes'); 182 | 183 | // (bool) true 184 | $finiteStateMachine->can('create'); 185 | 186 | $finiteStateMachine->apply('create'); 187 | 188 | // "pending_review" 189 | $finiteStateMachine->currentState()->name(); 190 | ``` 191 | 192 | ## Graph Definitions and Loaders 193 | 194 | A graph is a named collection of states, transitions, and callbacks. An object may have multiple graphs defined. 195 | 196 | The `GraphResolver` is responsible for resolving the graph definition for a given object (and optionally a graph name). 197 | 198 | A `Graph` can be created manually and added to a `GraphResolver`. A `Loader` can be used to load a `Graph` into a `GraphResolver` based on specific types of resources. 199 | 200 | ### winzou/state-machine 201 | 202 | This library ships with `WinzouArrayLoader`, a `Loader` implementation that is loosely drop-in compatible with [winzou/state-machine](https://github.com/winzou/state-machine) array-based graph definitions. 203 | 204 | ### Custom 205 | 206 | This library ships with a `Loader` contract. Implementing this interface allows for the creation of custom graph definitions. 207 | 208 | ## License 209 | 210 | MIT, see [LICENSE](LICENSE). 211 | -------------------------------------------------------------------------------- /tests/Feature/FiniteStateMachineTest.php: -------------------------------------------------------------------------------- 1 | graphResolver)) { 31 | $this->graphResolver = new GraphResolver(); 32 | } 33 | 34 | return $this->graphResolver; 35 | } 36 | 37 | public function getObjectProxyResolver(): ObjectProxyResolver 38 | { 39 | if (! isset($this->objectProxyResolver)) { 40 | $this->objectProxyResolver = new ObjectProxyResolver(); 41 | } 42 | 43 | return $this->objectProxyResolver; 44 | } 45 | 46 | public function getFiniteStateMachineFactory(): FiniteStateMachineFactory 47 | { 48 | if (! isset($this->finiteStateMachineFactory)) { 49 | $this->finiteStateMachineFactory = new FiniteStateMachineFactory( 50 | $this->getGraphResolver(), 51 | $this->getObjectProxyResolver(), 52 | $this->getEventDispatcher() 53 | ); 54 | } 55 | 56 | return $this->finiteStateMachineFactory; 57 | } 58 | 59 | public function getEventDispatcher(): RecordOnlyEventDispatcher 60 | { 61 | if (! isset($this->eventDispatcher)) { 62 | $this->eventDispatcher = new RecordOnlyEventDispatcher(); 63 | } 64 | 65 | return $this->eventDispatcher; 66 | } 67 | 68 | /** @test */ 69 | public function it_cannot_resolve_unknown_object() 70 | { 71 | $this->expectException(Throwable::class); 72 | $this->expectExceptionMessage( 73 | 'No graph named "default" for class named "stdClass"' 74 | ); 75 | 76 | $badApple = new stdClass(); 77 | 78 | $finiteStateMachine = $this->getDefaultFiniteStateMachineFactory() 79 | ->build($badApple); 80 | } 81 | 82 | /** @test */ 83 | public function it_cannot_find_missing_graph() 84 | { 85 | $this->expectException(Throwable::class); 86 | $this->expectExceptionMessage( 87 | 'No graph named "default" for class named "Tests\Fixtures\SebdesignExample"' 88 | ); 89 | 90 | $sebdesignExample = new SebdesignExample(); 91 | 92 | $finiteStateMachine = $this->getDefaultFiniteStateMachineFactory() 93 | ->build($sebdesignExample); 94 | } 95 | 96 | /** @test */ 97 | public function it_gets_graph_metadata() 98 | { 99 | $sebdesignExample = new SebdesignExample(); 100 | 101 | $finiteStateMachine = $this->getDefaultFiniteStateMachineFactory() 102 | ->build($sebdesignExample, 'graphA'); 103 | 104 | $this->assertEquals(['title' => 'Graph A'], $finiteStateMachine->graph()->metadata()); 105 | } 106 | 107 | /** @test */ 108 | public function it_gets_all_state_names() 109 | { 110 | $sebdesignExample = new SebdesignExample(); 111 | 112 | $finiteStateMachine = $this->getDefaultFiniteStateMachineFactory() 113 | ->build($sebdesignExample, 'graphA'); 114 | 115 | $this->assertEquals([ 116 | 'new', 117 | 'pending_review', 118 | 'awaiting_changes', 119 | 'accepted', 120 | 'published', 121 | 'rejected', 122 | ], $finiteStateMachine->allStates()->names()); 123 | } 124 | 125 | /** @test */ 126 | public function it_gets_all_transition_names() 127 | { 128 | $sebdesignExample = new SebdesignExample(); 129 | 130 | $finiteStateMachine = $this->getDefaultFiniteStateMachineFactory() 131 | ->build($sebdesignExample, 'graphA'); 132 | 133 | $this->assertEquals([ 134 | 'create', 135 | 'ask_for_changes', 136 | 'cancel_changes', 137 | 'submit_changes', 138 | 'approve', 139 | 'publish', 140 | ], $finiteStateMachine->allTransitions()->names()); 141 | } 142 | 143 | /** @test */ 144 | public function it_gets_state_by_name() 145 | { 146 | $sebdesignExample = new SebdesignExample(); 147 | 148 | $finiteStateMachine = $this->getDefaultFiniteStateMachineFactory() 149 | ->build($sebdesignExample, 'graphA'); 150 | 151 | $this->assertEquals('awaiting_changes', $finiteStateMachine->state('awaiting_changes')->name()); 152 | } 153 | 154 | /** @test */ 155 | public function it_gets_transition_by_name() 156 | { 157 | $sebdesignExample = new SebdesignExample(); 158 | 159 | $finiteStateMachine = $this->getDefaultFiniteStateMachineFactory() 160 | ->build($sebdesignExample, 'graphA'); 161 | 162 | $this->assertEquals('publish', $finiteStateMachine->transition('publish')->name()); 163 | } 164 | 165 | /** @test */ 166 | public function it_is_new() 167 | { 168 | $sebdesignExample = new SebdesignExample(); 169 | 170 | $finiteStateMachine = $this->getDefaultFiniteStateMachineFactory() 171 | ->build($sebdesignExample, 'graphA'); 172 | 173 | $this->assertEquals('new', $finiteStateMachine->currentState()->name()); 174 | $this->assertEmpty($finiteStateMachine->currentState()->metadata()); 175 | $this->assertTrue($finiteStateMachine->can('create')); 176 | $this->assertEquals(['create'], $finiteStateMachine->availableTransitions()->names()); 177 | } 178 | 179 | /** @test */ 180 | public function it_is_pending_review() 181 | { 182 | $sebdesignExample = new SebdesignExample('pending_review'); 183 | 184 | $finiteStateMachine = $this->getDefaultFiniteStateMachineFactory() 185 | ->build($sebdesignExample, 'graphA'); 186 | 187 | $this->assertEquals('pending_review', $finiteStateMachine->currentState()->name()); 188 | $this->assertEquals(['title' => 'Pending Review'], $finiteStateMachine->currentState()->metadata()); 189 | $this->assertTrue($finiteStateMachine->can('ask_for_changes')); 190 | $this->assertTrue($finiteStateMachine->can('approve')); 191 | $this->assertEquals(['ask_for_changes', 'approve'], $finiteStateMachine->availableTransitions()->names()); 192 | } 193 | 194 | /** @test */ 195 | public function it_is_awaiting_changes() 196 | { 197 | $sebdesignExample = new SebdesignExample('awaiting_changes'); 198 | 199 | $finiteStateMachine = $this->getDefaultFiniteStateMachineFactory() 200 | ->build($sebdesignExample, 'graphA'); 201 | 202 | $this->assertEquals('awaiting_changes', $finiteStateMachine->currentState()->name()); 203 | $this->assertEmpty($finiteStateMachine->currentState()->metadata()); 204 | $this->assertTrue($finiteStateMachine->can('cancel_changes')); 205 | $this->assertTrue($finiteStateMachine->can('submit_changes')); 206 | $this->assertEquals(['cancel_changes', 'submit_changes'], $finiteStateMachine->availableTransitions()->names()); 207 | } 208 | 209 | /** @test */ 210 | public function it_is_accepted() 211 | { 212 | $sebdesignExample = new SebdesignExample('accepted'); 213 | 214 | $finiteStateMachine = $this->getDefaultFiniteStateMachineFactory() 215 | ->build($sebdesignExample, 'graphA'); 216 | 217 | $this->assertEquals('accepted', $finiteStateMachine->currentState()->name()); 218 | $this->assertEmpty($finiteStateMachine->currentState()->metadata()); 219 | $this->assertTrue($finiteStateMachine->can('ask_for_changes')); 220 | $this->assertTrue($finiteStateMachine->can('publish')); 221 | $this->assertEquals(['ask_for_changes', 'publish'], $finiteStateMachine->availableTransitions()->names()); 222 | } 223 | 224 | /** @test */ 225 | public function it_is_published() 226 | { 227 | $sebdesignExample = new SebdesignExample('published'); 228 | 229 | $finiteStateMachine = $this->getDefaultFiniteStateMachineFactory() 230 | ->build($sebdesignExample, 'graphA'); 231 | 232 | $this->assertEquals('published', $finiteStateMachine->currentState()->name()); 233 | $this->assertEmpty($finiteStateMachine->currentState()->metadata()); 234 | $this->assertEmpty($finiteStateMachine->availableTransitions()->names()); 235 | } 236 | 237 | /** @test */ 238 | public function it_is_rejected() 239 | { 240 | $sebdesignExample = new SebdesignExample('rejected'); 241 | 242 | $finiteStateMachine = $this->getDefaultFiniteStateMachineFactory() 243 | ->build($sebdesignExample, 'graphA'); 244 | 245 | $this->assertEquals('rejected', $finiteStateMachine->currentState()->name()); 246 | $this->assertEmpty($finiteStateMachine->currentState()->metadata()); 247 | $this->assertFalse($finiteStateMachine->can('approve')); 248 | $this->assertEquals('guard_on_approving_from_rejected', $sebdesignExample->spy); 249 | $this->assertEquals(['approve'], $finiteStateMachine->availableTransitions()->names()); 250 | } 251 | 252 | /** @test */ 253 | public function it_transitions_from_new_to_pending_review_via_create() 254 | { 255 | $sebdesignExample = new SebdesignExample(); 256 | 257 | $finiteStateMachine = $this->getDefaultFiniteStateMachineFactory() 258 | ->build($sebdesignExample, 'graphA'); 259 | 260 | $finiteStateMachine->apply('create'); 261 | 262 | $this->assertEquals('pending_review', $sebdesignExample->state); 263 | 264 | $this->assertEventsFired([ 265 | ['graphA', $sebdesignExample, 'new', 'pending_review', 'create'], 266 | ['graphA', $sebdesignExample, 'new', 'pending_review', 'create'], 267 | ]); 268 | } 269 | 270 | protected function assertEventsFired(array $expectedEvents): void 271 | { 272 | $expectedEvents = array_map(function ($expectedEvent) { 273 | $expectedEvent[1] = spl_object_hash($expectedEvent[1]); 274 | return $expectedEvent; 275 | }, $expectedEvents); 276 | 277 | $actualEvents = array_map(function (Event $event) { 278 | return [ 279 | $event->graph()->graph(), 280 | spl_object_hash($event->object()), 281 | $event->fromState()->name(), 282 | $event->toState()->name(), 283 | $event->transition()->name() 284 | ]; 285 | }, $this->getEventDispatcher()->history()); 286 | 287 | $this->assertEquals($expectedEvents, $actualEvents); 288 | } 289 | 290 | /** @test */ 291 | public function it_transitions_from_pending_review_to_awaiting_changes_via_ask_for_changes() 292 | { 293 | $sebdesignExample = new SebdesignExample('pending_review'); 294 | 295 | $finiteStateMachine = $this->getDefaultFiniteStateMachineFactory() 296 | ->build($sebdesignExample, 'graphA'); 297 | 298 | $this->assertEquals([ 299 | 'title' => 'Ask for changes' 300 | ], $finiteStateMachine->availableTransitions()->named('ask_for_changes')->metadata()); 301 | 302 | $finiteStateMachine->apply('ask_for_changes'); 303 | 304 | $this->assertEquals('awaiting_changes', $sebdesignExample->state); 305 | $this->assertNull($sebdesignExample->spy); 306 | } 307 | 308 | /** @test */ 309 | public function it_transitions_from_accepted_to_awaiting_changes_via_ask_for_changes() 310 | { 311 | $sebdesignExample = new SebdesignExample('accepted'); 312 | 313 | $finiteStateMachine = $this->getDefaultFiniteStateMachineFactory() 314 | ->build($sebdesignExample, 'graphA'); 315 | 316 | $this->assertEquals([ 317 | 'title' => 'Ask for changes' 318 | ], $finiteStateMachine->availableTransitions()->named('ask_for_changes')->metadata()); 319 | 320 | $finiteStateMachine->apply('ask_for_changes'); 321 | 322 | $this->assertEquals('awaiting_changes', $sebdesignExample->state); 323 | $this->assertEquals('after ask_for_changes from accepted', $sebdesignExample->spy); 324 | } 325 | 326 | /** @test */ 327 | public function it_transitions_from_awaiting_changes_to_pending_review_via_cancel_changes() 328 | { 329 | $sebdesignExample = new SebdesignExample('awaiting_changes'); 330 | 331 | $finiteStateMachine = $this->getDefaultFiniteStateMachineFactory() 332 | ->build($sebdesignExample, 'graphA'); 333 | 334 | $finiteStateMachine->apply('cancel_changes'); 335 | 336 | $this->assertEquals('pending_review', $sebdesignExample->state); 337 | } 338 | 339 | /** @test */ 340 | public function it_transitions_from_awaiting_changes_to_pending_review_via_submit_changes() 341 | { 342 | $sebdesignExample = new SebdesignExample('awaiting_changes'); 343 | 344 | $finiteStateMachine = $this->getDefaultFiniteStateMachineFactory() 345 | ->build($sebdesignExample, 'graphA'); 346 | 347 | $finiteStateMachine->apply('submit_changes'); 348 | 349 | $this->assertEquals('pending_review', $sebdesignExample->state); 350 | } 351 | 352 | /** @test */ 353 | public function it_transitions_from_pending_review_to_accepted_via_approve() 354 | { 355 | $sebdesignExample = new SebdesignExample('pending_review'); 356 | 357 | $finiteStateMachine = $this->getDefaultFiniteStateMachineFactory() 358 | ->build($sebdesignExample, 'graphA'); 359 | 360 | $finiteStateMachine->apply('approve'); 361 | 362 | $this->assertEquals('accepted', $sebdesignExample->state); 363 | } 364 | 365 | /** @test */ 366 | public function it_transitions_from_rejected_to_accepted_via_approve() 367 | { 368 | $sebdesignExample = new SebdesignExample('rejected'); 369 | 370 | $finiteStateMachine = $this->getDefaultFiniteStateMachineFactory() 371 | ->build($sebdesignExample, 'graphA'); 372 | 373 | $this->expectException(Throwable::class); 374 | 375 | $finiteStateMachine->apply('approve'); 376 | } 377 | 378 | /** @test */ 379 | public function it_transitions_from_accepted_to_published_via_approve() 380 | { 381 | $sebdesignExample = new SebdesignExample('accepted'); 382 | 383 | $finiteStateMachine = $this->getDefaultFiniteStateMachineFactory() 384 | ->build($sebdesignExample, 'graphA'); 385 | 386 | $finiteStateMachine->apply('publish'); 387 | 388 | $this->assertEquals('published', $sebdesignExample->state); 389 | } 390 | 391 | public function getDefaultFiniteStateMachineFactory(): FiniteStateMachineFactory 392 | { 393 | (new WinzouArrayLoader($this->getGraphResolver()))->load(static::getSebdesignExampleConfiguration()); 394 | 395 | $this->getObjectProxyResolver()->add(new PropertyObjectProxyFactory()); 396 | 397 | return $this->getFiniteStateMachineFactory(); 398 | } 399 | 400 | public static function getSebdesignExampleConfiguration(): array 401 | { 402 | return [ 403 | // class of your domain object 404 | 'class' => SebdesignExample::class, 405 | 406 | // name of the graph (default is "default") 407 | 'graph' => 'graphA', 408 | 409 | // property of your object holding the actual state (default is "state") 410 | 'property_path' => 'state', 411 | 412 | 'metadata' => [ 413 | 'title' => 'Graph A', 414 | ], 415 | 416 | // list of all possible states 417 | 'states' => [ 418 | // a state as associative array 419 | ['name' => 'new'], 420 | // a state as associative array with metadata 421 | [ 422 | 'name' => 'pending_review', 423 | 'metadata' => ['title' => 'Pending Review'], 424 | ], 425 | // states as string 426 | 'awaiting_changes', 427 | 'accepted', 428 | 'published', 429 | 'rejected', 430 | ], 431 | 432 | // list of all possible transitions 433 | 'transitions' => [ 434 | 'create' => [ 435 | 'from' => ['new'], 436 | 'to' => 'pending_review', 437 | ], 438 | 'ask_for_changes' => [ 439 | 'from' => ['pending_review', 'accepted'], 440 | 'to' => 'awaiting_changes', 441 | 'metadata' => ['title' => 'Ask for changes'], 442 | ], 443 | 'cancel_changes' => [ 444 | 'from' => ['awaiting_changes'], 445 | 'to' => 'pending_review', 446 | ], 447 | 'submit_changes' => [ 448 | 'from' => ['awaiting_changes'], 449 | 'to' => 'pending_review', 450 | ], 451 | 'approve' => [ 452 | 'from' => ['pending_review', 'rejected'], 453 | 'to' => 'accepted', 454 | ], 455 | 'publish' => [ 456 | 'from' => ['accepted'], 457 | 'to' => 'published', 458 | ], 459 | ], 460 | 461 | // list of all callbacks 462 | 'callbacks' => [ 463 | // will be called when testing a transition 464 | 'guard' => [ 465 | 'guard_on_approving_from_rejected' => [ 466 | // call the callback on a specific transition 467 | 'on' => 'approve', 468 | 'from' => 'rejected', 469 | // will call the method of this class 470 | 'do' => function (object $object, Transition $transition, State $fromState, State $toState) { 471 | $object->spy = 'guard_on_approving_from_rejected'; 472 | 473 | return false; 474 | }, 475 | // arguments for the callback 476 | 'args' => ['object'], 477 | ], 478 | ], 479 | 480 | // will be called before applying a transition 481 | 'before' => [ 482 | 'spy-before-approve' => [ 483 | 'on' => 'ask_for_changes', 484 | 'from' => 'accepted', 485 | 'do' => function (string $when, object $object, Transition $transition, State $fromState, State $toState) { 486 | static::assertEquals($fromState->name(), $object->state); 487 | 488 | $object->spy = $when . ' ask_for_changes from accepted'; 489 | }, 490 | ] 491 | ], 492 | 493 | // will be called after applying a transition 494 | 'after' => [ 495 | 'spy-after-approve' => [ 496 | 'on' => 'ask_for_changes', 497 | 'from' => 'accepted', 498 | 'do' => function (string $when, object $object, Transition $transition, State $fromState, State $toState) { 499 | static::assertEquals($toState->name(), $object->state); 500 | static::assertEquals('before ask_for_changes from accepted', $object->spy); 501 | 502 | $object->spy = $when . ' ask_for_changes from accepted'; 503 | }, 504 | ] 505 | ], 506 | ] 507 | ]; 508 | } 509 | } 510 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "1b577e792aa88cca526580b092039371", 8 | "packages": [], 9 | "packages-dev": [ 10 | { 11 | "name": "doctrine/instantiator", 12 | "version": "1.3.0", 13 | "source": { 14 | "type": "git", 15 | "url": "https://github.com/doctrine/instantiator.git", 16 | "reference": "ae466f726242e637cebdd526a7d991b9433bacf1" 17 | }, 18 | "dist": { 19 | "type": "zip", 20 | "url": "https://api.github.com/repos/doctrine/instantiator/zipball/ae466f726242e637cebdd526a7d991b9433bacf1", 21 | "reference": "ae466f726242e637cebdd526a7d991b9433bacf1", 22 | "shasum": "" 23 | }, 24 | "require": { 25 | "php": "^7.1" 26 | }, 27 | "require-dev": { 28 | "doctrine/coding-standard": "^6.0", 29 | "ext-pdo": "*", 30 | "ext-phar": "*", 31 | "phpbench/phpbench": "^0.13", 32 | "phpstan/phpstan-phpunit": "^0.11", 33 | "phpstan/phpstan-shim": "^0.11", 34 | "phpunit/phpunit": "^7.0" 35 | }, 36 | "type": "library", 37 | "extra": { 38 | "branch-alias": { 39 | "dev-master": "1.2.x-dev" 40 | } 41 | }, 42 | "autoload": { 43 | "psr-4": { 44 | "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" 45 | } 46 | }, 47 | "notification-url": "https://packagist.org/downloads/", 48 | "license": [ 49 | "MIT" 50 | ], 51 | "authors": [ 52 | { 53 | "name": "Marco Pivetta", 54 | "email": "ocramius@gmail.com", 55 | "homepage": "http://ocramius.github.com/" 56 | } 57 | ], 58 | "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", 59 | "homepage": "https://www.doctrine-project.org/projects/instantiator.html", 60 | "keywords": [ 61 | "constructor", 62 | "instantiate" 63 | ], 64 | "time": "2019-10-21T16:45:58+00:00" 65 | }, 66 | { 67 | "name": "myclabs/deep-copy", 68 | "version": "1.9.5", 69 | "source": { 70 | "type": "git", 71 | "url": "https://github.com/myclabs/DeepCopy.git", 72 | "reference": "b2c28789e80a97badd14145fda39b545d83ca3ef" 73 | }, 74 | "dist": { 75 | "type": "zip", 76 | "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/b2c28789e80a97badd14145fda39b545d83ca3ef", 77 | "reference": "b2c28789e80a97badd14145fda39b545d83ca3ef", 78 | "shasum": "" 79 | }, 80 | "require": { 81 | "php": "^7.1" 82 | }, 83 | "replace": { 84 | "myclabs/deep-copy": "self.version" 85 | }, 86 | "require-dev": { 87 | "doctrine/collections": "^1.0", 88 | "doctrine/common": "^2.6", 89 | "phpunit/phpunit": "^7.1" 90 | }, 91 | "type": "library", 92 | "autoload": { 93 | "psr-4": { 94 | "DeepCopy\\": "src/DeepCopy/" 95 | }, 96 | "files": [ 97 | "src/DeepCopy/deep_copy.php" 98 | ] 99 | }, 100 | "notification-url": "https://packagist.org/downloads/", 101 | "license": [ 102 | "MIT" 103 | ], 104 | "description": "Create deep copies (clones) of your objects", 105 | "keywords": [ 106 | "clone", 107 | "copy", 108 | "duplicate", 109 | "object", 110 | "object graph" 111 | ], 112 | "time": "2020-01-17T21:11:47+00:00" 113 | }, 114 | { 115 | "name": "phar-io/manifest", 116 | "version": "1.0.3", 117 | "source": { 118 | "type": "git", 119 | "url": "https://github.com/phar-io/manifest.git", 120 | "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4" 121 | }, 122 | "dist": { 123 | "type": "zip", 124 | "url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", 125 | "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", 126 | "shasum": "" 127 | }, 128 | "require": { 129 | "ext-dom": "*", 130 | "ext-phar": "*", 131 | "phar-io/version": "^2.0", 132 | "php": "^5.6 || ^7.0" 133 | }, 134 | "type": "library", 135 | "extra": { 136 | "branch-alias": { 137 | "dev-master": "1.0.x-dev" 138 | } 139 | }, 140 | "autoload": { 141 | "classmap": [ 142 | "src/" 143 | ] 144 | }, 145 | "notification-url": "https://packagist.org/downloads/", 146 | "license": [ 147 | "BSD-3-Clause" 148 | ], 149 | "authors": [ 150 | { 151 | "name": "Arne Blankerts", 152 | "email": "arne@blankerts.de", 153 | "role": "Developer" 154 | }, 155 | { 156 | "name": "Sebastian Heuer", 157 | "email": "sebastian@phpeople.de", 158 | "role": "Developer" 159 | }, 160 | { 161 | "name": "Sebastian Bergmann", 162 | "email": "sebastian@phpunit.de", 163 | "role": "Developer" 164 | } 165 | ], 166 | "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", 167 | "time": "2018-07-08T19:23:20+00:00" 168 | }, 169 | { 170 | "name": "phar-io/version", 171 | "version": "2.0.1", 172 | "source": { 173 | "type": "git", 174 | "url": "https://github.com/phar-io/version.git", 175 | "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6" 176 | }, 177 | "dist": { 178 | "type": "zip", 179 | "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6", 180 | "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6", 181 | "shasum": "" 182 | }, 183 | "require": { 184 | "php": "^5.6 || ^7.0" 185 | }, 186 | "type": "library", 187 | "autoload": { 188 | "classmap": [ 189 | "src/" 190 | ] 191 | }, 192 | "notification-url": "https://packagist.org/downloads/", 193 | "license": [ 194 | "BSD-3-Clause" 195 | ], 196 | "authors": [ 197 | { 198 | "name": "Arne Blankerts", 199 | "email": "arne@blankerts.de", 200 | "role": "Developer" 201 | }, 202 | { 203 | "name": "Sebastian Heuer", 204 | "email": "sebastian@phpeople.de", 205 | "role": "Developer" 206 | }, 207 | { 208 | "name": "Sebastian Bergmann", 209 | "email": "sebastian@phpunit.de", 210 | "role": "Developer" 211 | } 212 | ], 213 | "description": "Library for handling version information and constraints", 214 | "time": "2018-07-08T19:19:57+00:00" 215 | }, 216 | { 217 | "name": "phpdocumentor/reflection-common", 218 | "version": "2.0.0", 219 | "source": { 220 | "type": "git", 221 | "url": "https://github.com/phpDocumentor/ReflectionCommon.git", 222 | "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a" 223 | }, 224 | "dist": { 225 | "type": "zip", 226 | "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/63a995caa1ca9e5590304cd845c15ad6d482a62a", 227 | "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a", 228 | "shasum": "" 229 | }, 230 | "require": { 231 | "php": ">=7.1" 232 | }, 233 | "require-dev": { 234 | "phpunit/phpunit": "~6" 235 | }, 236 | "type": "library", 237 | "extra": { 238 | "branch-alias": { 239 | "dev-master": "2.x-dev" 240 | } 241 | }, 242 | "autoload": { 243 | "psr-4": { 244 | "phpDocumentor\\Reflection\\": "src/" 245 | } 246 | }, 247 | "notification-url": "https://packagist.org/downloads/", 248 | "license": [ 249 | "MIT" 250 | ], 251 | "authors": [ 252 | { 253 | "name": "Jaap van Otterdijk", 254 | "email": "opensource@ijaap.nl" 255 | } 256 | ], 257 | "description": "Common reflection classes used by phpdocumentor to reflect the code structure", 258 | "homepage": "http://www.phpdoc.org", 259 | "keywords": [ 260 | "FQSEN", 261 | "phpDocumentor", 262 | "phpdoc", 263 | "reflection", 264 | "static analysis" 265 | ], 266 | "time": "2018-08-07T13:53:10+00:00" 267 | }, 268 | { 269 | "name": "phpdocumentor/reflection-docblock", 270 | "version": "5.1.0", 271 | "source": { 272 | "type": "git", 273 | "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", 274 | "reference": "cd72d394ca794d3466a3b2fc09d5a6c1dc86b47e" 275 | }, 276 | "dist": { 277 | "type": "zip", 278 | "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/cd72d394ca794d3466a3b2fc09d5a6c1dc86b47e", 279 | "reference": "cd72d394ca794d3466a3b2fc09d5a6c1dc86b47e", 280 | "shasum": "" 281 | }, 282 | "require": { 283 | "ext-filter": "^7.1", 284 | "php": "^7.2", 285 | "phpdocumentor/reflection-common": "^2.0", 286 | "phpdocumentor/type-resolver": "^1.0", 287 | "webmozart/assert": "^1" 288 | }, 289 | "require-dev": { 290 | "doctrine/instantiator": "^1", 291 | "mockery/mockery": "^1" 292 | }, 293 | "type": "library", 294 | "extra": { 295 | "branch-alias": { 296 | "dev-master": "5.x-dev" 297 | } 298 | }, 299 | "autoload": { 300 | "psr-4": { 301 | "phpDocumentor\\Reflection\\": "src" 302 | } 303 | }, 304 | "notification-url": "https://packagist.org/downloads/", 305 | "license": [ 306 | "MIT" 307 | ], 308 | "authors": [ 309 | { 310 | "name": "Mike van Riel", 311 | "email": "me@mikevanriel.com" 312 | }, 313 | { 314 | "name": "Jaap van Otterdijk", 315 | "email": "account@ijaap.nl" 316 | } 317 | ], 318 | "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", 319 | "time": "2020-02-22T12:28:44+00:00" 320 | }, 321 | { 322 | "name": "phpdocumentor/type-resolver", 323 | "version": "1.1.0", 324 | "source": { 325 | "type": "git", 326 | "url": "https://github.com/phpDocumentor/TypeResolver.git", 327 | "reference": "7462d5f123dfc080dfdf26897032a6513644fc95" 328 | }, 329 | "dist": { 330 | "type": "zip", 331 | "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/7462d5f123dfc080dfdf26897032a6513644fc95", 332 | "reference": "7462d5f123dfc080dfdf26897032a6513644fc95", 333 | "shasum": "" 334 | }, 335 | "require": { 336 | "php": "^7.2", 337 | "phpdocumentor/reflection-common": "^2.0" 338 | }, 339 | "require-dev": { 340 | "ext-tokenizer": "^7.2", 341 | "mockery/mockery": "~1" 342 | }, 343 | "type": "library", 344 | "extra": { 345 | "branch-alias": { 346 | "dev-master": "1.x-dev" 347 | } 348 | }, 349 | "autoload": { 350 | "psr-4": { 351 | "phpDocumentor\\Reflection\\": "src" 352 | } 353 | }, 354 | "notification-url": "https://packagist.org/downloads/", 355 | "license": [ 356 | "MIT" 357 | ], 358 | "authors": [ 359 | { 360 | "name": "Mike van Riel", 361 | "email": "me@mikevanriel.com" 362 | } 363 | ], 364 | "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", 365 | "time": "2020-02-18T18:59:58+00:00" 366 | }, 367 | { 368 | "name": "phpspec/prophecy", 369 | "version": "v1.10.3", 370 | "source": { 371 | "type": "git", 372 | "url": "https://github.com/phpspec/prophecy.git", 373 | "reference": "451c3cd1418cf640de218914901e51b064abb093" 374 | }, 375 | "dist": { 376 | "type": "zip", 377 | "url": "https://api.github.com/repos/phpspec/prophecy/zipball/451c3cd1418cf640de218914901e51b064abb093", 378 | "reference": "451c3cd1418cf640de218914901e51b064abb093", 379 | "shasum": "" 380 | }, 381 | "require": { 382 | "doctrine/instantiator": "^1.0.2", 383 | "php": "^5.3|^7.0", 384 | "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0", 385 | "sebastian/comparator": "^1.2.3|^2.0|^3.0|^4.0", 386 | "sebastian/recursion-context": "^1.0|^2.0|^3.0|^4.0" 387 | }, 388 | "require-dev": { 389 | "phpspec/phpspec": "^2.5 || ^3.2", 390 | "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1" 391 | }, 392 | "type": "library", 393 | "extra": { 394 | "branch-alias": { 395 | "dev-master": "1.10.x-dev" 396 | } 397 | }, 398 | "autoload": { 399 | "psr-4": { 400 | "Prophecy\\": "src/Prophecy" 401 | } 402 | }, 403 | "notification-url": "https://packagist.org/downloads/", 404 | "license": [ 405 | "MIT" 406 | ], 407 | "authors": [ 408 | { 409 | "name": "Konstantin Kudryashov", 410 | "email": "ever.zet@gmail.com", 411 | "homepage": "http://everzet.com" 412 | }, 413 | { 414 | "name": "Marcello Duarte", 415 | "email": "marcello.duarte@gmail.com" 416 | } 417 | ], 418 | "description": "Highly opinionated mocking framework for PHP 5.3+", 419 | "homepage": "https://github.com/phpspec/prophecy", 420 | "keywords": [ 421 | "Double", 422 | "Dummy", 423 | "fake", 424 | "mock", 425 | "spy", 426 | "stub" 427 | ], 428 | "time": "2020-03-05T15:02:03+00:00" 429 | }, 430 | { 431 | "name": "phpunit/php-code-coverage", 432 | "version": "8.0.1", 433 | "source": { 434 | "type": "git", 435 | "url": "https://github.com/sebastianbergmann/php-code-coverage.git", 436 | "reference": "31e94ccc084025d6abee0585df533eb3a792b96a" 437 | }, 438 | "dist": { 439 | "type": "zip", 440 | "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/31e94ccc084025d6abee0585df533eb3a792b96a", 441 | "reference": "31e94ccc084025d6abee0585df533eb3a792b96a", 442 | "shasum": "" 443 | }, 444 | "require": { 445 | "ext-dom": "*", 446 | "ext-xmlwriter": "*", 447 | "php": "^7.3", 448 | "phpunit/php-file-iterator": "^3.0", 449 | "phpunit/php-text-template": "^2.0", 450 | "phpunit/php-token-stream": "^4.0", 451 | "sebastian/code-unit-reverse-lookup": "^2.0", 452 | "sebastian/environment": "^5.0", 453 | "sebastian/version": "^3.0", 454 | "theseer/tokenizer": "^1.1.3" 455 | }, 456 | "require-dev": { 457 | "phpunit/phpunit": "^9.0" 458 | }, 459 | "suggest": { 460 | "ext-pcov": "*", 461 | "ext-xdebug": "*" 462 | }, 463 | "type": "library", 464 | "extra": { 465 | "branch-alias": { 466 | "dev-master": "8.0-dev" 467 | } 468 | }, 469 | "autoload": { 470 | "classmap": [ 471 | "src/" 472 | ] 473 | }, 474 | "notification-url": "https://packagist.org/downloads/", 475 | "license": [ 476 | "BSD-3-Clause" 477 | ], 478 | "authors": [ 479 | { 480 | "name": "Sebastian Bergmann", 481 | "email": "sebastian@phpunit.de", 482 | "role": "lead" 483 | } 484 | ], 485 | "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", 486 | "homepage": "https://github.com/sebastianbergmann/php-code-coverage", 487 | "keywords": [ 488 | "coverage", 489 | "testing", 490 | "xunit" 491 | ], 492 | "time": "2020-02-19T13:41:19+00:00" 493 | }, 494 | { 495 | "name": "phpunit/php-file-iterator", 496 | "version": "3.0.0", 497 | "source": { 498 | "type": "git", 499 | "url": "https://github.com/sebastianbergmann/php-file-iterator.git", 500 | "reference": "354d4a5faa7449a377a18b94a2026ca3415e3d7a" 501 | }, 502 | "dist": { 503 | "type": "zip", 504 | "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/354d4a5faa7449a377a18b94a2026ca3415e3d7a", 505 | "reference": "354d4a5faa7449a377a18b94a2026ca3415e3d7a", 506 | "shasum": "" 507 | }, 508 | "require": { 509 | "php": "^7.3" 510 | }, 511 | "require-dev": { 512 | "phpunit/phpunit": "^9.0" 513 | }, 514 | "type": "library", 515 | "extra": { 516 | "branch-alias": { 517 | "dev-master": "3.0-dev" 518 | } 519 | }, 520 | "autoload": { 521 | "classmap": [ 522 | "src/" 523 | ] 524 | }, 525 | "notification-url": "https://packagist.org/downloads/", 526 | "license": [ 527 | "BSD-3-Clause" 528 | ], 529 | "authors": [ 530 | { 531 | "name": "Sebastian Bergmann", 532 | "email": "sebastian@phpunit.de", 533 | "role": "lead" 534 | } 535 | ], 536 | "description": "FilterIterator implementation that filters files based on a list of suffixes.", 537 | "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", 538 | "keywords": [ 539 | "filesystem", 540 | "iterator" 541 | ], 542 | "time": "2020-02-07T06:05:22+00:00" 543 | }, 544 | { 545 | "name": "phpunit/php-invoker", 546 | "version": "3.0.0", 547 | "source": { 548 | "type": "git", 549 | "url": "https://github.com/sebastianbergmann/php-invoker.git", 550 | "reference": "7579d5a1ba7f3ac11c80004d205877911315ae7a" 551 | }, 552 | "dist": { 553 | "type": "zip", 554 | "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/7579d5a1ba7f3ac11c80004d205877911315ae7a", 555 | "reference": "7579d5a1ba7f3ac11c80004d205877911315ae7a", 556 | "shasum": "" 557 | }, 558 | "require": { 559 | "php": "^7.3" 560 | }, 561 | "require-dev": { 562 | "ext-pcntl": "*", 563 | "phpunit/phpunit": "^9.0" 564 | }, 565 | "suggest": { 566 | "ext-pcntl": "*" 567 | }, 568 | "type": "library", 569 | "extra": { 570 | "branch-alias": { 571 | "dev-master": "3.0-dev" 572 | } 573 | }, 574 | "autoload": { 575 | "classmap": [ 576 | "src/" 577 | ] 578 | }, 579 | "notification-url": "https://packagist.org/downloads/", 580 | "license": [ 581 | "BSD-3-Clause" 582 | ], 583 | "authors": [ 584 | { 585 | "name": "Sebastian Bergmann", 586 | "email": "sebastian@phpunit.de", 587 | "role": "lead" 588 | } 589 | ], 590 | "description": "Invoke callables with a timeout", 591 | "homepage": "https://github.com/sebastianbergmann/php-invoker/", 592 | "keywords": [ 593 | "process" 594 | ], 595 | "time": "2020-02-07T06:06:11+00:00" 596 | }, 597 | { 598 | "name": "phpunit/php-text-template", 599 | "version": "2.0.0", 600 | "source": { 601 | "type": "git", 602 | "url": "https://github.com/sebastianbergmann/php-text-template.git", 603 | "reference": "526dc996cc0ebdfa428cd2dfccd79b7b53fee346" 604 | }, 605 | "dist": { 606 | "type": "zip", 607 | "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/526dc996cc0ebdfa428cd2dfccd79b7b53fee346", 608 | "reference": "526dc996cc0ebdfa428cd2dfccd79b7b53fee346", 609 | "shasum": "" 610 | }, 611 | "require": { 612 | "php": "^7.3" 613 | }, 614 | "type": "library", 615 | "extra": { 616 | "branch-alias": { 617 | "dev-master": "2.0-dev" 618 | } 619 | }, 620 | "autoload": { 621 | "classmap": [ 622 | "src/" 623 | ] 624 | }, 625 | "notification-url": "https://packagist.org/downloads/", 626 | "license": [ 627 | "BSD-3-Clause" 628 | ], 629 | "authors": [ 630 | { 631 | "name": "Sebastian Bergmann", 632 | "email": "sebastian@phpunit.de", 633 | "role": "lead" 634 | } 635 | ], 636 | "description": "Simple template engine.", 637 | "homepage": "https://github.com/sebastianbergmann/php-text-template/", 638 | "keywords": [ 639 | "template" 640 | ], 641 | "time": "2020-02-01T07:43:44+00:00" 642 | }, 643 | { 644 | "name": "phpunit/php-timer", 645 | "version": "3.0.0", 646 | "source": { 647 | "type": "git", 648 | "url": "https://github.com/sebastianbergmann/php-timer.git", 649 | "reference": "4118013a4d0f97356eae8e7fb2f6c6472575d1df" 650 | }, 651 | "dist": { 652 | "type": "zip", 653 | "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/4118013a4d0f97356eae8e7fb2f6c6472575d1df", 654 | "reference": "4118013a4d0f97356eae8e7fb2f6c6472575d1df", 655 | "shasum": "" 656 | }, 657 | "require": { 658 | "php": "^7.3" 659 | }, 660 | "require-dev": { 661 | "phpunit/phpunit": "^9.0" 662 | }, 663 | "type": "library", 664 | "extra": { 665 | "branch-alias": { 666 | "dev-master": "3.0-dev" 667 | } 668 | }, 669 | "autoload": { 670 | "classmap": [ 671 | "src/" 672 | ] 673 | }, 674 | "notification-url": "https://packagist.org/downloads/", 675 | "license": [ 676 | "BSD-3-Clause" 677 | ], 678 | "authors": [ 679 | { 680 | "name": "Sebastian Bergmann", 681 | "email": "sebastian@phpunit.de", 682 | "role": "lead" 683 | } 684 | ], 685 | "description": "Utility class for timing", 686 | "homepage": "https://github.com/sebastianbergmann/php-timer/", 687 | "keywords": [ 688 | "timer" 689 | ], 690 | "time": "2020-02-07T06:08:11+00:00" 691 | }, 692 | { 693 | "name": "phpunit/php-token-stream", 694 | "version": "4.0.0", 695 | "source": { 696 | "type": "git", 697 | "url": "https://github.com/sebastianbergmann/php-token-stream.git", 698 | "reference": "b2560a0c33f7710e4d7f8780964193e8e8f8effe" 699 | }, 700 | "dist": { 701 | "type": "zip", 702 | "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/b2560a0c33f7710e4d7f8780964193e8e8f8effe", 703 | "reference": "b2560a0c33f7710e4d7f8780964193e8e8f8effe", 704 | "shasum": "" 705 | }, 706 | "require": { 707 | "ext-tokenizer": "*", 708 | "php": "^7.3" 709 | }, 710 | "require-dev": { 711 | "phpunit/phpunit": "^9.0" 712 | }, 713 | "type": "library", 714 | "extra": { 715 | "branch-alias": { 716 | "dev-master": "4.0-dev" 717 | } 718 | }, 719 | "autoload": { 720 | "classmap": [ 721 | "src/" 722 | ] 723 | }, 724 | "notification-url": "https://packagist.org/downloads/", 725 | "license": [ 726 | "BSD-3-Clause" 727 | ], 728 | "authors": [ 729 | { 730 | "name": "Sebastian Bergmann", 731 | "email": "sebastian@phpunit.de" 732 | } 733 | ], 734 | "description": "Wrapper around PHP's tokenizer extension.", 735 | "homepage": "https://github.com/sebastianbergmann/php-token-stream/", 736 | "keywords": [ 737 | "tokenizer" 738 | ], 739 | "time": "2020-02-07T06:19:00+00:00" 740 | }, 741 | { 742 | "name": "phpunit/phpunit", 743 | "version": "9.1.1", 744 | "source": { 745 | "type": "git", 746 | "url": "https://github.com/sebastianbergmann/phpunit.git", 747 | "reference": "848f6521c906500e66229668768576d35de0227e" 748 | }, 749 | "dist": { 750 | "type": "zip", 751 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/848f6521c906500e66229668768576d35de0227e", 752 | "reference": "848f6521c906500e66229668768576d35de0227e", 753 | "shasum": "" 754 | }, 755 | "require": { 756 | "doctrine/instantiator": "^1.2.0", 757 | "ext-dom": "*", 758 | "ext-json": "*", 759 | "ext-libxml": "*", 760 | "ext-mbstring": "*", 761 | "ext-xml": "*", 762 | "ext-xmlwriter": "*", 763 | "myclabs/deep-copy": "^1.9.1", 764 | "phar-io/manifest": "^1.0.3", 765 | "phar-io/version": "^2.0.1", 766 | "php": "^7.3", 767 | "phpspec/prophecy": "^1.8.1", 768 | "phpunit/php-code-coverage": "^8.0.1", 769 | "phpunit/php-file-iterator": "^3.0", 770 | "phpunit/php-invoker": "^3.0", 771 | "phpunit/php-text-template": "^2.0", 772 | "phpunit/php-timer": "^3.0", 773 | "sebastian/code-unit": "^1.0", 774 | "sebastian/comparator": "^4.0", 775 | "sebastian/diff": "^4.0", 776 | "sebastian/environment": "^5.0.1", 777 | "sebastian/exporter": "^4.0", 778 | "sebastian/global-state": "^4.0", 779 | "sebastian/object-enumerator": "^4.0", 780 | "sebastian/resource-operations": "^3.0", 781 | "sebastian/type": "^2.0", 782 | "sebastian/version": "^3.0" 783 | }, 784 | "require-dev": { 785 | "ext-pdo": "*" 786 | }, 787 | "suggest": { 788 | "ext-soap": "*", 789 | "ext-xdebug": "*" 790 | }, 791 | "bin": [ 792 | "phpunit" 793 | ], 794 | "type": "library", 795 | "extra": { 796 | "branch-alias": { 797 | "dev-master": "9.1-dev" 798 | } 799 | }, 800 | "autoload": { 801 | "classmap": [ 802 | "src/" 803 | ], 804 | "files": [ 805 | "src/Framework/Assert/Functions.php" 806 | ] 807 | }, 808 | "notification-url": "https://packagist.org/downloads/", 809 | "license": [ 810 | "BSD-3-Clause" 811 | ], 812 | "authors": [ 813 | { 814 | "name": "Sebastian Bergmann", 815 | "email": "sebastian@phpunit.de", 816 | "role": "lead" 817 | } 818 | ], 819 | "description": "The PHP Unit Testing framework.", 820 | "homepage": "https://phpunit.de/", 821 | "keywords": [ 822 | "phpunit", 823 | "testing", 824 | "xunit" 825 | ], 826 | "time": "2020-04-03T14:40:04+00:00" 827 | }, 828 | { 829 | "name": "psalm/phar", 830 | "version": "3.11.2", 831 | "source": { 832 | "type": "git", 833 | "url": "https://github.com/psalm/phar.git", 834 | "reference": "ec69029b77696f4f620460812942be8fdfc3b3bb" 835 | }, 836 | "dist": { 837 | "type": "zip", 838 | "url": "https://api.github.com/repos/psalm/phar/zipball/ec69029b77696f4f620460812942be8fdfc3b3bb", 839 | "reference": "ec69029b77696f4f620460812942be8fdfc3b3bb", 840 | "shasum": "" 841 | }, 842 | "require": { 843 | "php": "^7.1" 844 | }, 845 | "conflict": { 846 | "vimeo/psalm": "*" 847 | }, 848 | "bin": [ 849 | "psalm.phar" 850 | ], 851 | "type": "library", 852 | "notification-url": "https://packagist.org/downloads/", 853 | "license": [ 854 | "MIT" 855 | ], 856 | "description": "Composer-based Psalm Phar", 857 | "time": "2020-04-13T13:16:55+00:00" 858 | }, 859 | { 860 | "name": "sebastian/code-unit", 861 | "version": "1.0.0", 862 | "source": { 863 | "type": "git", 864 | "url": "https://github.com/sebastianbergmann/code-unit.git", 865 | "reference": "8d8f09bd47c75159921e6e84fdef146343962866" 866 | }, 867 | "dist": { 868 | "type": "zip", 869 | "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/8d8f09bd47c75159921e6e84fdef146343962866", 870 | "reference": "8d8f09bd47c75159921e6e84fdef146343962866", 871 | "shasum": "" 872 | }, 873 | "require": { 874 | "php": "^7.3" 875 | }, 876 | "require-dev": { 877 | "phpunit/phpunit": "^9.0" 878 | }, 879 | "type": "library", 880 | "extra": { 881 | "branch-alias": { 882 | "dev-master": "1.0-dev" 883 | } 884 | }, 885 | "autoload": { 886 | "classmap": [ 887 | "src/" 888 | ] 889 | }, 890 | "notification-url": "https://packagist.org/downloads/", 891 | "license": [ 892 | "BSD-3-Clause" 893 | ], 894 | "authors": [ 895 | { 896 | "name": "Sebastian Bergmann", 897 | "email": "sebastian@phpunit.de", 898 | "role": "lead" 899 | } 900 | ], 901 | "description": "Collection of value objects that represent the PHP code units", 902 | "homepage": "https://github.com/sebastianbergmann/code-unit", 903 | "time": "2020-03-30T11:59:20+00:00" 904 | }, 905 | { 906 | "name": "sebastian/code-unit-reverse-lookup", 907 | "version": "2.0.0", 908 | "source": { 909 | "type": "git", 910 | "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", 911 | "reference": "5b5dbe0044085ac41df47e79d34911a15b96d82e" 912 | }, 913 | "dist": { 914 | "type": "zip", 915 | "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5b5dbe0044085ac41df47e79d34911a15b96d82e", 916 | "reference": "5b5dbe0044085ac41df47e79d34911a15b96d82e", 917 | "shasum": "" 918 | }, 919 | "require": { 920 | "php": "^7.3" 921 | }, 922 | "require-dev": { 923 | "phpunit/phpunit": "^9.0" 924 | }, 925 | "type": "library", 926 | "extra": { 927 | "branch-alias": { 928 | "dev-master": "2.0-dev" 929 | } 930 | }, 931 | "autoload": { 932 | "classmap": [ 933 | "src/" 934 | ] 935 | }, 936 | "notification-url": "https://packagist.org/downloads/", 937 | "license": [ 938 | "BSD-3-Clause" 939 | ], 940 | "authors": [ 941 | { 942 | "name": "Sebastian Bergmann", 943 | "email": "sebastian@phpunit.de" 944 | } 945 | ], 946 | "description": "Looks up which function or method a line of code belongs to", 947 | "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", 948 | "time": "2020-02-07T06:20:13+00:00" 949 | }, 950 | { 951 | "name": "sebastian/comparator", 952 | "version": "4.0.0", 953 | "source": { 954 | "type": "git", 955 | "url": "https://github.com/sebastianbergmann/comparator.git", 956 | "reference": "85b3435da967696ed618ff745f32be3ff4a2b8e8" 957 | }, 958 | "dist": { 959 | "type": "zip", 960 | "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/85b3435da967696ed618ff745f32be3ff4a2b8e8", 961 | "reference": "85b3435da967696ed618ff745f32be3ff4a2b8e8", 962 | "shasum": "" 963 | }, 964 | "require": { 965 | "php": "^7.3", 966 | "sebastian/diff": "^4.0", 967 | "sebastian/exporter": "^4.0" 968 | }, 969 | "require-dev": { 970 | "phpunit/phpunit": "^9.0" 971 | }, 972 | "type": "library", 973 | "extra": { 974 | "branch-alias": { 975 | "dev-master": "4.0-dev" 976 | } 977 | }, 978 | "autoload": { 979 | "classmap": [ 980 | "src/" 981 | ] 982 | }, 983 | "notification-url": "https://packagist.org/downloads/", 984 | "license": [ 985 | "BSD-3-Clause" 986 | ], 987 | "authors": [ 988 | { 989 | "name": "Sebastian Bergmann", 990 | "email": "sebastian@phpunit.de" 991 | }, 992 | { 993 | "name": "Jeff Welch", 994 | "email": "whatthejeff@gmail.com" 995 | }, 996 | { 997 | "name": "Volker Dusch", 998 | "email": "github@wallbash.com" 999 | }, 1000 | { 1001 | "name": "Bernhard Schussek", 1002 | "email": "bschussek@2bepublished.at" 1003 | } 1004 | ], 1005 | "description": "Provides the functionality to compare PHP values for equality", 1006 | "homepage": "https://github.com/sebastianbergmann/comparator", 1007 | "keywords": [ 1008 | "comparator", 1009 | "compare", 1010 | "equality" 1011 | ], 1012 | "time": "2020-02-07T06:08:51+00:00" 1013 | }, 1014 | { 1015 | "name": "sebastian/diff", 1016 | "version": "4.0.0", 1017 | "source": { 1018 | "type": "git", 1019 | "url": "https://github.com/sebastianbergmann/diff.git", 1020 | "reference": "c0c26c9188b538bfa985ae10c9f05d278f12060d" 1021 | }, 1022 | "dist": { 1023 | "type": "zip", 1024 | "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c0c26c9188b538bfa985ae10c9f05d278f12060d", 1025 | "reference": "c0c26c9188b538bfa985ae10c9f05d278f12060d", 1026 | "shasum": "" 1027 | }, 1028 | "require": { 1029 | "php": "^7.3" 1030 | }, 1031 | "require-dev": { 1032 | "phpunit/phpunit": "^9.0", 1033 | "symfony/process": "^4 || ^5" 1034 | }, 1035 | "type": "library", 1036 | "extra": { 1037 | "branch-alias": { 1038 | "dev-master": "4.0-dev" 1039 | } 1040 | }, 1041 | "autoload": { 1042 | "classmap": [ 1043 | "src/" 1044 | ] 1045 | }, 1046 | "notification-url": "https://packagist.org/downloads/", 1047 | "license": [ 1048 | "BSD-3-Clause" 1049 | ], 1050 | "authors": [ 1051 | { 1052 | "name": "Sebastian Bergmann", 1053 | "email": "sebastian@phpunit.de" 1054 | }, 1055 | { 1056 | "name": "Kore Nordmann", 1057 | "email": "mail@kore-nordmann.de" 1058 | } 1059 | ], 1060 | "description": "Diff implementation", 1061 | "homepage": "https://github.com/sebastianbergmann/diff", 1062 | "keywords": [ 1063 | "diff", 1064 | "udiff", 1065 | "unidiff", 1066 | "unified diff" 1067 | ], 1068 | "time": "2020-02-07T06:09:38+00:00" 1069 | }, 1070 | { 1071 | "name": "sebastian/environment", 1072 | "version": "5.1.0", 1073 | "source": { 1074 | "type": "git", 1075 | "url": "https://github.com/sebastianbergmann/environment.git", 1076 | "reference": "c753f04d68cd489b6973cf9b4e505e191af3b05c" 1077 | }, 1078 | "dist": { 1079 | "type": "zip", 1080 | "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/c753f04d68cd489b6973cf9b4e505e191af3b05c", 1081 | "reference": "c753f04d68cd489b6973cf9b4e505e191af3b05c", 1082 | "shasum": "" 1083 | }, 1084 | "require": { 1085 | "php": "^7.3" 1086 | }, 1087 | "require-dev": { 1088 | "phpunit/phpunit": "^9.0" 1089 | }, 1090 | "suggest": { 1091 | "ext-posix": "*" 1092 | }, 1093 | "type": "library", 1094 | "extra": { 1095 | "branch-alias": { 1096 | "dev-master": "5.0-dev" 1097 | } 1098 | }, 1099 | "autoload": { 1100 | "classmap": [ 1101 | "src/" 1102 | ] 1103 | }, 1104 | "notification-url": "https://packagist.org/downloads/", 1105 | "license": [ 1106 | "BSD-3-Clause" 1107 | ], 1108 | "authors": [ 1109 | { 1110 | "name": "Sebastian Bergmann", 1111 | "email": "sebastian@phpunit.de" 1112 | } 1113 | ], 1114 | "description": "Provides functionality to handle HHVM/PHP environments", 1115 | "homepage": "http://www.github.com/sebastianbergmann/environment", 1116 | "keywords": [ 1117 | "Xdebug", 1118 | "environment", 1119 | "hhvm" 1120 | ], 1121 | "time": "2020-04-14T13:36:52+00:00" 1122 | }, 1123 | { 1124 | "name": "sebastian/exporter", 1125 | "version": "4.0.0", 1126 | "source": { 1127 | "type": "git", 1128 | "url": "https://github.com/sebastianbergmann/exporter.git", 1129 | "reference": "80c26562e964016538f832f305b2286e1ec29566" 1130 | }, 1131 | "dist": { 1132 | "type": "zip", 1133 | "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/80c26562e964016538f832f305b2286e1ec29566", 1134 | "reference": "80c26562e964016538f832f305b2286e1ec29566", 1135 | "shasum": "" 1136 | }, 1137 | "require": { 1138 | "php": "^7.3", 1139 | "sebastian/recursion-context": "^4.0" 1140 | }, 1141 | "require-dev": { 1142 | "ext-mbstring": "*", 1143 | "phpunit/phpunit": "^9.0" 1144 | }, 1145 | "type": "library", 1146 | "extra": { 1147 | "branch-alias": { 1148 | "dev-master": "4.0-dev" 1149 | } 1150 | }, 1151 | "autoload": { 1152 | "classmap": [ 1153 | "src/" 1154 | ] 1155 | }, 1156 | "notification-url": "https://packagist.org/downloads/", 1157 | "license": [ 1158 | "BSD-3-Clause" 1159 | ], 1160 | "authors": [ 1161 | { 1162 | "name": "Sebastian Bergmann", 1163 | "email": "sebastian@phpunit.de" 1164 | }, 1165 | { 1166 | "name": "Jeff Welch", 1167 | "email": "whatthejeff@gmail.com" 1168 | }, 1169 | { 1170 | "name": "Volker Dusch", 1171 | "email": "github@wallbash.com" 1172 | }, 1173 | { 1174 | "name": "Adam Harvey", 1175 | "email": "aharvey@php.net" 1176 | }, 1177 | { 1178 | "name": "Bernhard Schussek", 1179 | "email": "bschussek@gmail.com" 1180 | } 1181 | ], 1182 | "description": "Provides the functionality to export PHP variables for visualization", 1183 | "homepage": "http://www.github.com/sebastianbergmann/exporter", 1184 | "keywords": [ 1185 | "export", 1186 | "exporter" 1187 | ], 1188 | "time": "2020-02-07T06:10:52+00:00" 1189 | }, 1190 | { 1191 | "name": "sebastian/global-state", 1192 | "version": "4.0.0", 1193 | "source": { 1194 | "type": "git", 1195 | "url": "https://github.com/sebastianbergmann/global-state.git", 1196 | "reference": "bdb1e7c79e592b8c82cb1699be3c8743119b8a72" 1197 | }, 1198 | "dist": { 1199 | "type": "zip", 1200 | "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bdb1e7c79e592b8c82cb1699be3c8743119b8a72", 1201 | "reference": "bdb1e7c79e592b8c82cb1699be3c8743119b8a72", 1202 | "shasum": "" 1203 | }, 1204 | "require": { 1205 | "php": "^7.3", 1206 | "sebastian/object-reflector": "^2.0", 1207 | "sebastian/recursion-context": "^4.0" 1208 | }, 1209 | "require-dev": { 1210 | "ext-dom": "*", 1211 | "phpunit/phpunit": "^9.0" 1212 | }, 1213 | "suggest": { 1214 | "ext-uopz": "*" 1215 | }, 1216 | "type": "library", 1217 | "extra": { 1218 | "branch-alias": { 1219 | "dev-master": "4.0-dev" 1220 | } 1221 | }, 1222 | "autoload": { 1223 | "classmap": [ 1224 | "src/" 1225 | ] 1226 | }, 1227 | "notification-url": "https://packagist.org/downloads/", 1228 | "license": [ 1229 | "BSD-3-Clause" 1230 | ], 1231 | "authors": [ 1232 | { 1233 | "name": "Sebastian Bergmann", 1234 | "email": "sebastian@phpunit.de" 1235 | } 1236 | ], 1237 | "description": "Snapshotting of global state", 1238 | "homepage": "http://www.github.com/sebastianbergmann/global-state", 1239 | "keywords": [ 1240 | "global state" 1241 | ], 1242 | "time": "2020-02-07T06:11:37+00:00" 1243 | }, 1244 | { 1245 | "name": "sebastian/object-enumerator", 1246 | "version": "4.0.0", 1247 | "source": { 1248 | "type": "git", 1249 | "url": "https://github.com/sebastianbergmann/object-enumerator.git", 1250 | "reference": "e67516b175550abad905dc952f43285957ef4363" 1251 | }, 1252 | "dist": { 1253 | "type": "zip", 1254 | "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/e67516b175550abad905dc952f43285957ef4363", 1255 | "reference": "e67516b175550abad905dc952f43285957ef4363", 1256 | "shasum": "" 1257 | }, 1258 | "require": { 1259 | "php": "^7.3", 1260 | "sebastian/object-reflector": "^2.0", 1261 | "sebastian/recursion-context": "^4.0" 1262 | }, 1263 | "require-dev": { 1264 | "phpunit/phpunit": "^9.0" 1265 | }, 1266 | "type": "library", 1267 | "extra": { 1268 | "branch-alias": { 1269 | "dev-master": "4.0-dev" 1270 | } 1271 | }, 1272 | "autoload": { 1273 | "classmap": [ 1274 | "src/" 1275 | ] 1276 | }, 1277 | "notification-url": "https://packagist.org/downloads/", 1278 | "license": [ 1279 | "BSD-3-Clause" 1280 | ], 1281 | "authors": [ 1282 | { 1283 | "name": "Sebastian Bergmann", 1284 | "email": "sebastian@phpunit.de" 1285 | } 1286 | ], 1287 | "description": "Traverses array structures and object graphs to enumerate all referenced objects", 1288 | "homepage": "https://github.com/sebastianbergmann/object-enumerator/", 1289 | "time": "2020-02-07T06:12:23+00:00" 1290 | }, 1291 | { 1292 | "name": "sebastian/object-reflector", 1293 | "version": "2.0.0", 1294 | "source": { 1295 | "type": "git", 1296 | "url": "https://github.com/sebastianbergmann/object-reflector.git", 1297 | "reference": "f4fd0835cabb0d4a6546d9fe291e5740037aa1e7" 1298 | }, 1299 | "dist": { 1300 | "type": "zip", 1301 | "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/f4fd0835cabb0d4a6546d9fe291e5740037aa1e7", 1302 | "reference": "f4fd0835cabb0d4a6546d9fe291e5740037aa1e7", 1303 | "shasum": "" 1304 | }, 1305 | "require": { 1306 | "php": "^7.3" 1307 | }, 1308 | "require-dev": { 1309 | "phpunit/phpunit": "^9.0" 1310 | }, 1311 | "type": "library", 1312 | "extra": { 1313 | "branch-alias": { 1314 | "dev-master": "2.0-dev" 1315 | } 1316 | }, 1317 | "autoload": { 1318 | "classmap": [ 1319 | "src/" 1320 | ] 1321 | }, 1322 | "notification-url": "https://packagist.org/downloads/", 1323 | "license": [ 1324 | "BSD-3-Clause" 1325 | ], 1326 | "authors": [ 1327 | { 1328 | "name": "Sebastian Bergmann", 1329 | "email": "sebastian@phpunit.de" 1330 | } 1331 | ], 1332 | "description": "Allows reflection of object attributes, including inherited and non-public ones", 1333 | "homepage": "https://github.com/sebastianbergmann/object-reflector/", 1334 | "time": "2020-02-07T06:19:40+00:00" 1335 | }, 1336 | { 1337 | "name": "sebastian/recursion-context", 1338 | "version": "4.0.0", 1339 | "source": { 1340 | "type": "git", 1341 | "url": "https://github.com/sebastianbergmann/recursion-context.git", 1342 | "reference": "cdd86616411fc3062368b720b0425de10bd3d579" 1343 | }, 1344 | "dist": { 1345 | "type": "zip", 1346 | "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cdd86616411fc3062368b720b0425de10bd3d579", 1347 | "reference": "cdd86616411fc3062368b720b0425de10bd3d579", 1348 | "shasum": "" 1349 | }, 1350 | "require": { 1351 | "php": "^7.3" 1352 | }, 1353 | "require-dev": { 1354 | "phpunit/phpunit": "^9.0" 1355 | }, 1356 | "type": "library", 1357 | "extra": { 1358 | "branch-alias": { 1359 | "dev-master": "4.0-dev" 1360 | } 1361 | }, 1362 | "autoload": { 1363 | "classmap": [ 1364 | "src/" 1365 | ] 1366 | }, 1367 | "notification-url": "https://packagist.org/downloads/", 1368 | "license": [ 1369 | "BSD-3-Clause" 1370 | ], 1371 | "authors": [ 1372 | { 1373 | "name": "Sebastian Bergmann", 1374 | "email": "sebastian@phpunit.de" 1375 | }, 1376 | { 1377 | "name": "Jeff Welch", 1378 | "email": "whatthejeff@gmail.com" 1379 | }, 1380 | { 1381 | "name": "Adam Harvey", 1382 | "email": "aharvey@php.net" 1383 | } 1384 | ], 1385 | "description": "Provides functionality to recursively process PHP variables", 1386 | "homepage": "http://www.github.com/sebastianbergmann/recursion-context", 1387 | "time": "2020-02-07T06:18:20+00:00" 1388 | }, 1389 | { 1390 | "name": "sebastian/resource-operations", 1391 | "version": "3.0.0", 1392 | "source": { 1393 | "type": "git", 1394 | "url": "https://github.com/sebastianbergmann/resource-operations.git", 1395 | "reference": "8c98bf0dfa1f9256d0468b9803a1e1df31b6fa98" 1396 | }, 1397 | "dist": { 1398 | "type": "zip", 1399 | "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/8c98bf0dfa1f9256d0468b9803a1e1df31b6fa98", 1400 | "reference": "8c98bf0dfa1f9256d0468b9803a1e1df31b6fa98", 1401 | "shasum": "" 1402 | }, 1403 | "require": { 1404 | "php": "^7.3" 1405 | }, 1406 | "require-dev": { 1407 | "phpunit/phpunit": "^9.0" 1408 | }, 1409 | "type": "library", 1410 | "extra": { 1411 | "branch-alias": { 1412 | "dev-master": "3.0-dev" 1413 | } 1414 | }, 1415 | "autoload": { 1416 | "classmap": [ 1417 | "src/" 1418 | ] 1419 | }, 1420 | "notification-url": "https://packagist.org/downloads/", 1421 | "license": [ 1422 | "BSD-3-Clause" 1423 | ], 1424 | "authors": [ 1425 | { 1426 | "name": "Sebastian Bergmann", 1427 | "email": "sebastian@phpunit.de" 1428 | } 1429 | ], 1430 | "description": "Provides a list of PHP built-in functions that operate on resources", 1431 | "homepage": "https://www.github.com/sebastianbergmann/resource-operations", 1432 | "time": "2020-02-07T06:13:02+00:00" 1433 | }, 1434 | { 1435 | "name": "sebastian/type", 1436 | "version": "2.0.0", 1437 | "source": { 1438 | "type": "git", 1439 | "url": "https://github.com/sebastianbergmann/type.git", 1440 | "reference": "9e8f42f740afdea51f5f4e8cec2035580e797ee1" 1441 | }, 1442 | "dist": { 1443 | "type": "zip", 1444 | "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/9e8f42f740afdea51f5f4e8cec2035580e797ee1", 1445 | "reference": "9e8f42f740afdea51f5f4e8cec2035580e797ee1", 1446 | "shasum": "" 1447 | }, 1448 | "require": { 1449 | "php": "^7.3" 1450 | }, 1451 | "require-dev": { 1452 | "phpunit/phpunit": "^9.0" 1453 | }, 1454 | "type": "library", 1455 | "extra": { 1456 | "branch-alias": { 1457 | "dev-master": "2.0-dev" 1458 | } 1459 | }, 1460 | "autoload": { 1461 | "classmap": [ 1462 | "src/" 1463 | ] 1464 | }, 1465 | "notification-url": "https://packagist.org/downloads/", 1466 | "license": [ 1467 | "BSD-3-Clause" 1468 | ], 1469 | "authors": [ 1470 | { 1471 | "name": "Sebastian Bergmann", 1472 | "email": "sebastian@phpunit.de", 1473 | "role": "lead" 1474 | } 1475 | ], 1476 | "description": "Collection of value objects that represent the types of the PHP type system", 1477 | "homepage": "https://github.com/sebastianbergmann/type", 1478 | "time": "2020-02-07T06:13:43+00:00" 1479 | }, 1480 | { 1481 | "name": "sebastian/version", 1482 | "version": "3.0.0", 1483 | "source": { 1484 | "type": "git", 1485 | "url": "https://github.com/sebastianbergmann/version.git", 1486 | "reference": "0411bde656dce64202b39c2f4473993a9081d39e" 1487 | }, 1488 | "dist": { 1489 | "type": "zip", 1490 | "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/0411bde656dce64202b39c2f4473993a9081d39e", 1491 | "reference": "0411bde656dce64202b39c2f4473993a9081d39e", 1492 | "shasum": "" 1493 | }, 1494 | "require": { 1495 | "php": "^7.3" 1496 | }, 1497 | "type": "library", 1498 | "extra": { 1499 | "branch-alias": { 1500 | "dev-master": "3.0-dev" 1501 | } 1502 | }, 1503 | "autoload": { 1504 | "classmap": [ 1505 | "src/" 1506 | ] 1507 | }, 1508 | "notification-url": "https://packagist.org/downloads/", 1509 | "license": [ 1510 | "BSD-3-Clause" 1511 | ], 1512 | "authors": [ 1513 | { 1514 | "name": "Sebastian Bergmann", 1515 | "email": "sebastian@phpunit.de", 1516 | "role": "lead" 1517 | } 1518 | ], 1519 | "description": "Library that helps with managing the version number of Git-hosted PHP projects", 1520 | "homepage": "https://github.com/sebastianbergmann/version", 1521 | "time": "2020-01-21T06:36:37+00:00" 1522 | }, 1523 | { 1524 | "name": "squizlabs/php_codesniffer", 1525 | "version": "3.5.4", 1526 | "source": { 1527 | "type": "git", 1528 | "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", 1529 | "reference": "dceec07328401de6211037abbb18bda423677e26" 1530 | }, 1531 | "dist": { 1532 | "type": "zip", 1533 | "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/dceec07328401de6211037abbb18bda423677e26", 1534 | "reference": "dceec07328401de6211037abbb18bda423677e26", 1535 | "shasum": "" 1536 | }, 1537 | "require": { 1538 | "ext-simplexml": "*", 1539 | "ext-tokenizer": "*", 1540 | "ext-xmlwriter": "*", 1541 | "php": ">=5.4.0" 1542 | }, 1543 | "require-dev": { 1544 | "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" 1545 | }, 1546 | "bin": [ 1547 | "bin/phpcs", 1548 | "bin/phpcbf" 1549 | ], 1550 | "type": "library", 1551 | "extra": { 1552 | "branch-alias": { 1553 | "dev-master": "3.x-dev" 1554 | } 1555 | }, 1556 | "notification-url": "https://packagist.org/downloads/", 1557 | "license": [ 1558 | "BSD-3-Clause" 1559 | ], 1560 | "authors": [ 1561 | { 1562 | "name": "Greg Sherwood", 1563 | "role": "lead" 1564 | } 1565 | ], 1566 | "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", 1567 | "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", 1568 | "keywords": [ 1569 | "phpcs", 1570 | "standards" 1571 | ], 1572 | "time": "2020-01-30T22:20:29+00:00" 1573 | }, 1574 | { 1575 | "name": "symfony/polyfill-ctype", 1576 | "version": "v1.15.0", 1577 | "source": { 1578 | "type": "git", 1579 | "url": "https://github.com/symfony/polyfill-ctype.git", 1580 | "reference": "4719fa9c18b0464d399f1a63bf624b42b6fa8d14" 1581 | }, 1582 | "dist": { 1583 | "type": "zip", 1584 | "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/4719fa9c18b0464d399f1a63bf624b42b6fa8d14", 1585 | "reference": "4719fa9c18b0464d399f1a63bf624b42b6fa8d14", 1586 | "shasum": "" 1587 | }, 1588 | "require": { 1589 | "php": ">=5.3.3" 1590 | }, 1591 | "suggest": { 1592 | "ext-ctype": "For best performance" 1593 | }, 1594 | "type": "library", 1595 | "extra": { 1596 | "branch-alias": { 1597 | "dev-master": "1.15-dev" 1598 | } 1599 | }, 1600 | "autoload": { 1601 | "psr-4": { 1602 | "Symfony\\Polyfill\\Ctype\\": "" 1603 | }, 1604 | "files": [ 1605 | "bootstrap.php" 1606 | ] 1607 | }, 1608 | "notification-url": "https://packagist.org/downloads/", 1609 | "license": [ 1610 | "MIT" 1611 | ], 1612 | "authors": [ 1613 | { 1614 | "name": "Gert de Pagter", 1615 | "email": "BackEndTea@gmail.com" 1616 | }, 1617 | { 1618 | "name": "Symfony Community", 1619 | "homepage": "https://symfony.com/contributors" 1620 | } 1621 | ], 1622 | "description": "Symfony polyfill for ctype functions", 1623 | "homepage": "https://symfony.com", 1624 | "keywords": [ 1625 | "compatibility", 1626 | "ctype", 1627 | "polyfill", 1628 | "portable" 1629 | ], 1630 | "time": "2020-02-27T09:26:54+00:00" 1631 | }, 1632 | { 1633 | "name": "theseer/tokenizer", 1634 | "version": "1.1.3", 1635 | "source": { 1636 | "type": "git", 1637 | "url": "https://github.com/theseer/tokenizer.git", 1638 | "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9" 1639 | }, 1640 | "dist": { 1641 | "type": "zip", 1642 | "url": "https://api.github.com/repos/theseer/tokenizer/zipball/11336f6f84e16a720dae9d8e6ed5019efa85a0f9", 1643 | "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9", 1644 | "shasum": "" 1645 | }, 1646 | "require": { 1647 | "ext-dom": "*", 1648 | "ext-tokenizer": "*", 1649 | "ext-xmlwriter": "*", 1650 | "php": "^7.0" 1651 | }, 1652 | "type": "library", 1653 | "autoload": { 1654 | "classmap": [ 1655 | "src/" 1656 | ] 1657 | }, 1658 | "notification-url": "https://packagist.org/downloads/", 1659 | "license": [ 1660 | "BSD-3-Clause" 1661 | ], 1662 | "authors": [ 1663 | { 1664 | "name": "Arne Blankerts", 1665 | "email": "arne@blankerts.de", 1666 | "role": "Developer" 1667 | } 1668 | ], 1669 | "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", 1670 | "time": "2019-06-13T22:48:21+00:00" 1671 | }, 1672 | { 1673 | "name": "webmozart/assert", 1674 | "version": "1.7.0", 1675 | "source": { 1676 | "type": "git", 1677 | "url": "https://github.com/webmozart/assert.git", 1678 | "reference": "aed98a490f9a8f78468232db345ab9cf606cf598" 1679 | }, 1680 | "dist": { 1681 | "type": "zip", 1682 | "url": "https://api.github.com/repos/webmozart/assert/zipball/aed98a490f9a8f78468232db345ab9cf606cf598", 1683 | "reference": "aed98a490f9a8f78468232db345ab9cf606cf598", 1684 | "shasum": "" 1685 | }, 1686 | "require": { 1687 | "php": "^5.3.3 || ^7.0", 1688 | "symfony/polyfill-ctype": "^1.8" 1689 | }, 1690 | "conflict": { 1691 | "vimeo/psalm": "<3.6.0" 1692 | }, 1693 | "require-dev": { 1694 | "phpunit/phpunit": "^4.8.36 || ^7.5.13" 1695 | }, 1696 | "type": "library", 1697 | "autoload": { 1698 | "psr-4": { 1699 | "Webmozart\\Assert\\": "src/" 1700 | } 1701 | }, 1702 | "notification-url": "https://packagist.org/downloads/", 1703 | "license": [ 1704 | "MIT" 1705 | ], 1706 | "authors": [ 1707 | { 1708 | "name": "Bernhard Schussek", 1709 | "email": "bschussek@gmail.com" 1710 | } 1711 | ], 1712 | "description": "Assertions to validate method input/output with nice error messages.", 1713 | "keywords": [ 1714 | "assert", 1715 | "check", 1716 | "validate" 1717 | ], 1718 | "time": "2020-02-14T12:15:55+00:00" 1719 | } 1720 | ], 1721 | "aliases": [], 1722 | "minimum-stability": "stable", 1723 | "stability-flags": [], 1724 | "prefer-stable": false, 1725 | "prefer-lowest": false, 1726 | "platform": { 1727 | "php": "^7.4|^8.0" 1728 | }, 1729 | "platform-dev": [] 1730 | } 1731 | --------------------------------------------------------------------------------