Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
7 / 7
CRAP
100.00% covered (success)
100.00%
112 / 112
AnnotationReader
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
7 / 7
56
100.00% covered (success)
100.00%
112 / 112
 __construct
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
3 / 3
 getStateMachineDefinition
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
4 / 4
 processClassReflection
100.00% covered (success)
100.00%
1 / 1
10
100.00% covered (success)
100.00%
26 / 26
 processClassAnnotations
100.00% covered (success)
100.00%
1 / 1
10
100.00% covered (success)
100.00%
17 / 17
 processConstantAnnotations
100.00% covered (success)
100.00%
1 / 1
9
100.00% covered (success)
100.00%
17 / 17
 processMethodAnnotations
100.00% covered (success)
100.00%
1 / 1
14
100.00% covered (success)
100.00%
27 / 27
 processPropertyAnnotations
100.00% covered (success)
100.00%
1 / 1
10
100.00% covered (success)
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);
            }
        }
    }
}