├── .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 | 
6 | [](https://scrutinizer-ci.com/g/dflydev/dflydev-finite-state-machine/?branch=master)
7 | [](https://scrutinizer-ci.com/g/dflydev/dflydev-finite-state-machine/?branch=master)
8 | [](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 |
--------------------------------------------------------------------------------