Failed Conditions
Pull Request — new-parser-ast-metadata (#5)
by
unknown
02:27
created

AnnotationMetadataAssembler::assemble()   A

Complexity

Conditions 3
Paths 1

Size

Total Lines 24
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 16
dl 0
loc 24
ccs 17
cts 17
cp 1
rs 9.7333
c 0
b 0
f 0
cc 3
nc 1
nop 2
crap 3
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\Annotations\Metadata\Assembler;
6
7
use Doctrine\Annotations\Annotation\Annotation as AnnotationAnnotation;
8
use Doctrine\Annotations\Annotation\Required as RequiredAnnotation;
9
use Doctrine\Annotations\Annotation\Target as TargetAnnotation;
10
use Doctrine\Annotations\Assembler\Assembler;
11
use Doctrine\Annotations\Metadata\AnnotationMetadata;
12
use Doctrine\Annotations\Metadata\AnnotationTarget;
13
use Doctrine\Annotations\Metadata\InternalAnnotations;
14
use Doctrine\Annotations\Metadata\PropertyMetadata;
15
use Doctrine\Annotations\Metadata\Reflection\ClassReflectionProvider;
16
use Doctrine\Annotations\Metadata\ScopeManufacturer;
17
use Doctrine\Annotations\Metadata\Type\MixedType;
18
use Doctrine\Annotations\Metadata\Type\NullType;
19
use Doctrine\Annotations\Metadata\Type\UnionType;
20
use Doctrine\Annotations\Parser\Ast\Annotations;
21
use Doctrine\Annotations\Parser\Ast\Reference;
22
use Doctrine\Annotations\Parser\Compiler;
23
use Doctrine\Annotations\Parser\Reference\ReferenceResolver;
24
use Doctrine\Annotations\Parser\Scope;
25
use Doctrine\Annotations\TypeParser\TypeParser;
26
use ReflectionClass;
27
use ReflectionProperty;
28
use function assert;
29
use function is_array;
30
use function iterator_to_array;
31
use function stripos;
32
33
final class AnnotationMetadataAssembler
34
{
35
    /** @var Compiler */
36
    private $parser;
37
38
    /** @var ReferenceResolver */
39
    private $referenceResolver;
40
41
    /** @var ClassReflectionProvider */
42
    private $classReflectionProvider;
43
44
    /** @var TypeParser */
45
    private $typeParser;
46
47
    /** @var ScopeManufacturer */
48
    private $scopeManufacturer;
49
50
    /** @var Assembler */
51
    private $internalAssembler;
52
53 34
    public function __construct(
54
        Compiler $parser,
55
        ReferenceResolver $referenceResolver,
56
        ClassReflectionProvider $classReflectionProvider,
57
        TypeParser $typeParser,
58
        ScopeManufacturer $scopeManufacturer,
59
        Assembler $internalAssembler
60
    ) {
61 34
        $this->parser                  = $parser;
62 34
        $this->referenceResolver       = $referenceResolver;
63 34
        $this->classReflectionProvider = $classReflectionProvider;
64 34
        $this->typeParser              = $typeParser;
65 34
        $this->scopeManufacturer       = $scopeManufacturer;
66 34
        $this->internalAssembler       = $internalAssembler;
67 34
    }
68
69 30
    public function assemble(Reference $reference, Scope $scope) : AnnotationMetadata
70
    {
71 30
        $realName        = $this->referenceResolver->resolve($reference, $scope);
72 30
        $classReflection = $this->classReflectionProvider->getClassReflection($realName);
73 28
        $docComment      = $classReflection->getDocComment();
74 28
        $hasConstructor  = $classReflection->getConstructor() !== null;
75
76 28
        assert($docComment !== false, 'not an annotation');
77
78 28
        $annotations = $this->parser->compile($docComment);
0 ignored issues
show
Bug introduced by
It seems like $docComment can also be of type true; however, parameter $docblock of Doctrine\Annotations\Parser\Compiler::compile() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

78
        $annotations = $this->parser->compile(/** @scrutinizer ignore-type */ $docComment);
Loading history...
79
80 25
        assert(stripos($docComment, '@') !== false);
0 ignored issues
show
Bug introduced by
It seems like $docComment can also be of type true; however, parameter $haystack of stripos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

80
        assert(stripos(/** @scrutinizer ignore-type */ $docComment, '@') !== false);
Loading history...
81
82 25
        $hydratedAnnotations = $this->hydrateInternalAnnotations($annotations, $scope);
83
84 25
        assert($this->findAnnotation(AnnotationAnnotation::class, $hydratedAnnotations) !== null, 'not annotated with @Annotation');
85 25
        assert(! $hasConstructor || $classReflection->getConstructor()->isPublic(), 'constructor must be public');
86 25
        assert(! $hasConstructor || $classReflection->getConstructor()->getNumberOfParameters() === 1, 'constructor must accept a single parameter');
87
88 25
        return new AnnotationMetadata(
89 25
            $realName,
90 25
            $this->determineTarget($hydratedAnnotations),
91 25
            $hasConstructor,
92 25
            $this->assembleProperties($classReflection)
93
        );
94
    }
95
96
    /**
97
     * @param object[] $annotations
98
     */
99 25
    private function determineTarget(array $annotations) : AnnotationTarget
100
    {
101
        /** @var TargetAnnotation|null $target */
102 25
        $target = $this->findAnnotation(TargetAnnotation::class, $annotations);
103
104 25
        if ($target === null) {
105 5
            return new AnnotationTarget(AnnotationTarget::TARGET_ALL);
106
        }
107
108 20
        return new AnnotationTarget($target->targets);
109
    }
110
111
    /**
112
     * @return object[]
113
     */
114 25
    private function hydrateInternalAnnotations(Annotations $annotations, Scope $scope) : array
115
    {
116 25
        $assembled = $this->internalAssembler->collect(
117 25
            $annotations,
118 25
            new Scope($scope->getSubject(), InternalAnnotations::createImports(), clone $scope->getIgnoredAnnotations())
119
        );
120
121 25
        return is_array($assembled) ? $assembled : iterator_to_array($assembled);
122
    }
123
124
    /**
125
     * @param object[] $annotations
126
     */
127 25
    private function findAnnotation(string $name, array $annotations) : ?object
128
    {
129 25
        foreach ($annotations as $annotation) {
130 25
            if (! $annotation instanceof $name) {
131 25
                continue;
132
            }
133
134 25
            return $annotation;
135
        }
136
137 13
        return null;
138
    }
139
140
    /**
141
     * @return PropertyMetadata[]
142
     */
143 25
    private function assembleProperties(ReflectionClass $class) : array
144
    {
145 25
        $metadatas = [];
146
147 25
        foreach ($class->getProperties(ReflectionProperty::IS_PUBLIC) as $i => $property) {
148 24
            $metadatas[] = $this->assembleProperty($property, $i === 0);
149
        }
150
151 25
        return $metadatas;
152
    }
153
154 24
    private function assembleProperty(ReflectionProperty $property, bool $first) : PropertyMetadata
155
    {
156 24
        $docBlock = $property->getDocComment();
157
158 24
        if ($docBlock === false) {
159 16
            return new PropertyMetadata(
160 16
                $property->getName(),
161 16
                new MixedType(),
162 16
                $first
163
            );
164
        }
165
166 14
        $scope               = $this->scopeManufacturer->manufacturePropertyScope($property);
167 14
        $hydratedAnnotations = $this->hydrateInternalAnnotations($this->parser->compile($docBlock), $scope);
0 ignored issues
show
Bug introduced by
It seems like $docBlock can also be of type true; however, parameter $docblock of Doctrine\Annotations\Parser\Compiler::compile() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

167
        $hydratedAnnotations = $this->hydrateInternalAnnotations($this->parser->compile(/** @scrutinizer ignore-type */ $docBlock), $scope);
Loading history...
168
169 14
        $required = $this->findAnnotation(RequiredAnnotation::class, $hydratedAnnotations) !== null;
170
171 14
        $type = $this->typeParser->parsePropertyType($property->getDocComment(), $scope);
0 ignored issues
show
Bug introduced by
It seems like $property->getDocComment() can also be of type true; however, parameter $docBlock of Doctrine\Annotations\Typ...er::parsePropertyType() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

171
        $type = $this->typeParser->parsePropertyType(/** @scrutinizer ignore-type */ $property->getDocComment(), $scope);
Loading history...
172
173 14
        if ($required && ! $type->acceptsNull()) {
174
            // TODO: throw deprecated warning?
175 1
            $type = new UnionType($type, new NullType());
176
        }
177
178 14
        return new PropertyMetadata($property->getName(), $type, $first);
179
    }
180
}
181