Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
100.00% |
1 / 1 |
|
100.00% |
7 / 7 |
CRAP | |
100.00% |
112 / 112 |
AnnotationReader | |
100.00% |
1 / 1 |
|
100.00% |
7 / 7 |
56 | |
100.00% |
112 / 112 |
__construct | |
100.00% |
1 / 1 |
2 | |
100.00% |
3 / 3 |
|||
getStateMachineDefinition | |
100.00% |
1 / 1 |
1 | |
100.00% |
4 / 4 |
|||
processClassReflection | |
100.00% |
1 / 1 |
10 | |
100.00% |
26 / 26 |
|||
processClassAnnotations | |
100.00% |
1 / 1 |
10 | |
100.00% |
17 / 17 |
|||
processConstantAnnotations | |
100.00% |
1 / 1 |
9 | |
100.00% |
17 / 17 |
|||
processMethodAnnotations | |
100.00% |
1 / 1 |
14 | |
100.00% |
27 / 27 |
|||
processPropertyAnnotations | |
100.00% |
1 / 1 |
10 | |
100.00% |
18 / 18 |
<?php declare(strict_types = 1); | |
/* | |
* Copyright (c) 2019, Josef Kufner <josef@kufner.cz> | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
* | |
*/ | |
namespace Smalldb\StateMachine\Definition\AnnotationReader; | |
use ReflectionClass; | |
use ReflectionClassConstant; | |
use ReflectionMethod; | |
use ReflectionProperty; | |
use Smalldb\StateMachine\Annotation\StateMachine; | |
use Smalldb\StateMachine\Annotation\State; | |
use Smalldb\StateMachine\Annotation\Transition; | |
use Smalldb\StateMachine\Definition\Builder\StateMachineDefinitionBuilder; | |
use Smalldb\StateMachine\Definition\Builder\StateMachineDefinitionBuilderFactory; | |
use Smalldb\StateMachine\Definition\StateMachineDefinition; | |
use Smalldb\StateMachine\InvalidArgumentException; | |
use Smalldb\StateMachine\ReferenceTrait; | |
use Smalldb\StateMachine\SourcesExtension\Definition\SourceClassFile; | |
use Smalldb\StateMachine\SourcesExtension\Definition\SourcesExtensionPlaceholder; | |
use Smalldb\StateMachine\Utils\AnnotationReader\AnnotationReaderInterface; | |
use Smalldb\StateMachine\Utils\AnnotationReader\AnnotationReader as Reader; | |
/** | |
* Construct state machine definition from interface annotations. | |
* - The interface with `@StateMachine` annotation represents the machine. | |
* - Methods with a `@Transition` annotation are transitions. | |
* - Constants with a `@State` annotation are states. | |
*/ | |
class AnnotationReader | |
{ | |
private StateMachineDefinitionBuilderFactory $definitionBuilderFactory; | |
private AnnotationReaderInterface $annotationReader; | |
public function __construct(StateMachineDefinitionBuilderFactory $definitionBuilderFactory, | |
?AnnotationReaderInterface $annotationReader = null) | |
{ | |
$this->definitionBuilderFactory = $definitionBuilderFactory; | |
$this->annotationReader = $annotationReader ?? (new Reader()); | |
} | |
public function getStateMachineDefinition(ReflectionClass $reflectionClass): StateMachineDefinition | |
{ | |
$builder = $this->definitionBuilderFactory->createDefinitionBuilder(); | |
$this->processClassReflection($reflectionClass, $this->annotationReader, $builder); | |
$builder->sortPlaceholders(); | |
return $builder->build(); | |
} | |
private function processClassReflection(ReflectionClass $reflectionClass, | |
AnnotationReaderInterface $annotationReader, StateMachineDefinitionBuilder $builder, | |
bool $isSourceClass = true): void | |
{ | |
$filename = $reflectionClass->getFileName(); | |
$classname = $reflectionClass->getName(); | |
// Disallow internal classes | |
if ($filename === false) { | |
throw new InvalidArgumentException("Cannot process PHP core or extension class: $classname"); //@codeCoverageIgnore | |
} | |
// Disallow the use of ReferenceTrait | |
$traits = $reflectionClass->getTraits(); | |
foreach ($traits as $trait) { | |
if ($trait->getName() === ReferenceTrait::class) { | |
throw new InvalidArgumentException("Reference class $classname must not use ReferenceTrait. Use ReferenceProtectedAPI instead."); | |
} | |
} | |
if ($isSourceClass) { | |
$builder->setReferenceClass($classname); | |
$builder->setMTime(filemtime($filename)); | |
} | |
/** @var SourcesExtensionPlaceholder $sourcesPlaceholder */ | |
$sourcesPlaceholder = $builder->getExtensionPlaceholder(SourcesExtensionPlaceholder::class); | |
$sourcesPlaceholder->addSourceFile(new SourceClassFile($reflectionClass)); | |
$classAnnotations = $annotationReader->getClassAnnotations($reflectionClass); | |
$this->processClassAnnotations($reflectionClass, $classAnnotations, $builder, $isSourceClass); | |
foreach ($reflectionClass->getReflectionConstants() as $reflectionConstant) { | |
$constantAnnotations = $annotationReader->getConstantAnnotations($reflectionConstant); | |
$this->processConstantAnnotations($reflectionConstant, $constantAnnotations, $builder); | |
} | |
foreach ($reflectionClass->getMethods() as $reflectionMethod) { | |
if (!$reflectionMethod->isStatic()) { | |
$methodAnnotations = $annotationReader->getMethodAnnotations($reflectionMethod); | |
$this->processMethodAnnotations($reflectionMethod, $methodAnnotations, $builder); | |
} | |
} | |
foreach ($reflectionClass->getProperties() as $reflectionProperty) { | |
if (!$reflectionProperty->isStatic()) { | |
$propertyAnnotations = $annotationReader->getPropertyAnnotations($reflectionProperty); | |
$this->processPropertyAnnotations($reflectionProperty, $propertyAnnotations, $builder); | |
} | |
} | |
} | |
private function processClassAnnotations(ReflectionClass $reflectionClass, array $annotations, | |
StateMachineDefinitionBuilder $builder, bool $isSourceClass = true): void | |
{ | |
$isStateMachine = false; | |
foreach ($annotations as $annotation) { | |
if ($annotation instanceof StateMachine) { | |
$isStateMachine = true; | |
} | |
if ($annotation instanceof ReflectionClassAwareAnnotationInterface) { | |
$annotation->setReflectionClass($reflectionClass); | |
} | |
if ($annotation instanceof ApplyToPlaceholderInterface) { | |
$annotation->applyToPlaceholder($builder); | |
} | |
if ($annotation instanceof ApplyToStateMachineBuilderInterface) { | |
$annotation->applyToBuilder($builder); | |
} | |
if ($annotation instanceof RecursiveAnnotationIncludeInterface) { | |
foreach ($annotation->getIncludedClassNames() as $includedClassName) { | |
$includedClass = new ReflectionClass($includedClassName); | |
$this->processClassReflection($includedClass, $this->annotationReader, $builder, false); | |
} | |
} | |
} | |
if ($isSourceClass && !$isStateMachine) { | |
throw new MissingStateMachineAnnotationException("No @StateMachine annotation found: " . $reflectionClass->getName()); | |
} | |
} | |
private function processConstantAnnotations(ReflectionClassConstant $reflectionConstant, array $annotations, StateMachineDefinitionBuilder $builder): void | |
{ | |
// Find & use @State annotation and make sure there is only one of the kind | |
$placeholder = null; | |
foreach ($annotations as $annotation) { | |
if ($annotation instanceof ReflectionConstantAwareAnnotationInterface) { | |
$annotation->setReflectionConstant($reflectionConstant); | |
} | |
if ($annotation instanceof State) { | |
if ($placeholder) { | |
throw new \InvalidArgumentException("Multiple @State annotations at " . $reflectionConstant->getName() . " constant."); | |
} else { | |
$stateName = $annotation->name ?? (string) $reflectionConstant->getValue(); | |
$placeholder = $builder->addState($stateName); | |
} | |
} | |
} | |
if (!$placeholder) { | |
return; // This is not a state of the state machine. | |
} | |
// Apply all annotations to the state placeholder | |
foreach ($annotations as $annotation) { | |
if ($annotation instanceof ApplyToPlaceholderInterface) { | |
$annotation->applyToPlaceholder($placeholder); | |
} | |
if ($annotation instanceof ApplyToStatePlaceholderInterface) { | |
$annotation->applyToStatePlaceholder($placeholder); | |
} | |
} | |
} | |
private function processMethodAnnotations(ReflectionMethod $reflectionMethod, array $annotations, StateMachineDefinitionBuilder $builder): void | |
{ | |
// Find & use @Transition annotations | |
$transitionPlaceholders = []; | |
$actionPlaceholders = []; | |
$isTransition = false; | |
$transitionName = $reflectionMethod->getName(); | |
foreach ($annotations as $annotation) { | |
if ($annotation instanceof ReflectionMethodAwareAnnotationInterface) { | |
$annotation->setReflectionMethod($reflectionMethod); | |
} | |
if ($annotation instanceof Transition) { | |
$isTransition = true; | |
if ($annotation->definesTransition()) { | |
$transitionPlaceholders[] = $builder->addTransition($transitionName, $annotation->source, $annotation->targets); | |
} else { | |
$actionPlaceholders[] = $builder->addAction($transitionName); | |
} | |
} | |
} | |
if (!$isTransition) { | |
return; // This is not a transition. | |
} | |
foreach ($annotations as $annotation) { | |
// Apply all annotations to both placeholders | |
if ($annotation instanceof ApplyToPlaceholderInterface) { | |
foreach ($actionPlaceholders as $placeholder) { | |
$annotation->applyToPlaceholder($placeholder); | |
} | |
foreach ($transitionPlaceholders as $placeholder) { | |
$annotation->applyToPlaceholder($placeholder); | |
} | |
} | |
// Apply all annotations to the action placeholder | |
if ($annotation instanceof ApplyToActionPlaceholderInterface) { | |
foreach ($actionPlaceholders as $placeholder) { | |
$annotation->applyToActionPlaceholder($placeholder); | |
} | |
} | |
// Apply all annotations to the transition placeholders | |
if ($annotation instanceof ApplyToTransitionPlaceholderInterface) { | |
foreach ($transitionPlaceholders as $placeholder) { | |
$annotation->applyToTransitionPlaceholder($placeholder); | |
} | |
} | |
} | |
} | |
private function processPropertyAnnotations(ReflectionProperty $reflectionProperty, array $annotations, StateMachineDefinitionBuilder $builder): void | |
{ | |
foreach ($annotations as $annotation) { | |
if ($annotation instanceof ReflectionPropertyAwareAnnotationInterface) { | |
$annotation->setReflectionProperty($reflectionProperty); | |
} | |
} | |
$name = $reflectionProperty->getName(); | |
// Get getter type as default property type | |
$getterName = 'get' . ucfirst($name); | |
$classReflection = $reflectionProperty->getDeclaringClass(); | |
if ($classReflection->hasMethod($getterName) && ($type = $classReflection->getMethod($getterName)->getReturnType())) { | |
$typeName = $type instanceof \ReflectionNamedType ? $type->getName() : null; | |
$placeholder = $builder->addProperty($name, $typeName, $type->allowsNull()); | |
} else { | |
$type = $reflectionProperty->getType(); | |
$typeName = $type instanceof \ReflectionNamedType ? $type->getName() : null; | |
$placeholder = $builder->addProperty($name, $typeName, $type->allowsNull()); | |
} | |
foreach ($annotations as $annotation) { | |
if ($annotation instanceof ApplyToPlaceholderInterface) { | |
$annotation->applyToPlaceholder($placeholder); | |
} | |
if ($annotation instanceof ApplyToPropertyPlaceholderInterface) { | |
$annotation->applyToPropertyPlaceholder($placeholder); | |
} | |
} | |
} | |
} |