Failed Conditions
Pull Request — new-parser-ast-metadata (#3)
by
unknown
01:53
created

AnnotationMetadataAssembler::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 6
dl 0
loc 14
ccs 7
cts 7
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 6
crap 1
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\Enum;
9
use Doctrine\Annotations\Annotation\Required as RequiredAnnotation;
10
use Doctrine\Annotations\Annotation\Target as TargetAnnotation;
11
use Doctrine\Annotations\Assembler\Assembler;
12
use Doctrine\Annotations\Metadata\AnnotationMetadata;
13
use Doctrine\Annotations\Metadata\AnnotationTarget;
14
use Doctrine\Annotations\Metadata\Constraint\CompositeConstraint;
15
use Doctrine\Annotations\Metadata\Constraint\Constraint;
16
use Doctrine\Annotations\Metadata\Constraint\EnumConstraint;
17
use Doctrine\Annotations\Metadata\Constraint\RequiredConstraint;
18
use Doctrine\Annotations\Metadata\Constraint\TypeConstraint;
19
use Doctrine\Annotations\Metadata\InternalAnnotations;
20
use Doctrine\Annotations\Metadata\PropertyMetadata;
21
use Doctrine\Annotations\Metadata\Reflection\ClassReflectionProvider;
22
use Doctrine\Annotations\Metadata\ScopeManufacturer;
23
use Doctrine\Annotations\Metadata\Type\MixedType;
24
use Doctrine\Annotations\Metadata\Type\NullType;
25
use Doctrine\Annotations\Metadata\Type\Type;
26
use Doctrine\Annotations\Metadata\Type\UnionType;
27
use Doctrine\Annotations\Parser\Ast\Annotations;
28
use Doctrine\Annotations\Parser\Ast\Reference;
29
use Doctrine\Annotations\Parser\Compiler;
30
use Doctrine\Annotations\Parser\Reference\ReferenceResolver;
31
use Doctrine\Annotations\Parser\Scope;
32
use Doctrine\Annotations\TypeParser\TypeParser;
33
use ReflectionClass;
34
use ReflectionProperty;
35
use function assert;
36
use function count;
37
use function is_array;
38
use function iterator_to_array;
39
use function stripos;
40
41
final class AnnotationMetadataAssembler
42
{
43
    /** @var Compiler */
44
    private $parser;
45
46
    /** @var ReferenceResolver */
47
    private $referenceResolver;
48
49
    /** @var ClassReflectionProvider */
50
    private $classReflectionProvider;
51
52
    /** @var TypeParser */
53
    private $typeParser;
54
55
    /** @var ScopeManufacturer */
56
    private $scopeManufacturer;
57
58
    /** @var Assembler */
59
    private $internalAssembler;
60
61 7
    public function __construct(
62
        Compiler $parser,
63
        ReferenceResolver $referenceResolver,
64
        ClassReflectionProvider $classReflectionProvider,
65
        TypeParser $typeParser,
66
        ScopeManufacturer $scopeManufacturer,
67
        Assembler $internalAssembler
68
    ) {
69 7
        $this->parser                  = $parser;
70 7
        $this->referenceResolver       = $referenceResolver;
71 7
        $this->classReflectionProvider = $classReflectionProvider;
72 7
        $this->typeParser              = $typeParser;
73 7
        $this->scopeManufacturer       = $scopeManufacturer;
74 7
        $this->internalAssembler       = $internalAssembler;
75 7
    }
76
77 7
    public function assemble(Reference $reference, Scope $scope) : AnnotationMetadata
78
    {
79 7
        $realName        = $this->referenceResolver->resolve($reference, $scope);
80 7
        $classReflection = $this->classReflectionProvider->getClassReflection($realName);
81 7
        $docComment      = $classReflection->getDocComment();
82 7
        $hasConstructor  = $classReflection->getConstructor() !== null;
83
84 7
        assert($docComment !== false, 'not an annotation');
85
86 7
        $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

86
        $annotations = $this->parser->compile(/** @scrutinizer ignore-type */ $docComment);
Loading history...
87
88 7
        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

88
        assert(stripos(/** @scrutinizer ignore-type */ $docComment, '@') !== false);
Loading history...
89
90 7
        $hydratedAnnotations = $this->hydrateInternalAnnotations($annotations, $scope);
91
92 7
        assert($this->findAnnotation(AnnotationAnnotation::class, $hydratedAnnotations) !== null, 'not annotated with @Annotation');
93 7
        assert(! $hasConstructor || $classReflection->getConstructor()->isPublic(), 'constructor must be public');
94 7
        assert(! $hasConstructor || $classReflection->getConstructor()->getNumberOfParameters() === 1, 'constructor must accept a single parameter');
95
96 7
        return new AnnotationMetadata(
97 7
            $realName,
98 7
            $this->determineTarget($hydratedAnnotations),
99 7
            $hasConstructor,
100 7
            $this->assembleProperties($classReflection)
101
        );
102
    }
103
104
    /**
105
     * @param object[] $annotations
106
     */
107 7
    private function determineTarget(array $annotations) : AnnotationTarget
108
    {
109
        /** @var TargetAnnotation|null $target */
110 7
        $target = $this->findAnnotation(TargetAnnotation::class, $annotations);
111
112 7
        if ($target === null) {
113
            return new AnnotationTarget(AnnotationTarget::TARGET_ALL);
114
        }
115
116 7
        return new AnnotationTarget($target->targets);
117
    }
118
119
    /**
120
     * @return object[]
121
     */
122 7
    private function hydrateInternalAnnotations(Annotations $annotations, Scope $scope) : array
123
    {
124 7
        $assembled = $this->internalAssembler->collect(
125 7
            $annotations,
126 7
            new Scope($scope->getSubject(), InternalAnnotations::createImports(), clone $scope->getIgnoredAnnotations())
127
        );
128
129 7
        return is_array($assembled) ? $assembled : iterator_to_array($assembled);
130
    }
131
132
    /**
133
     * @param object[] $annotations
134
     */
135 7
    private function findAnnotation(string $name, array $annotations) : ?object
136
    {
137 7
        foreach ($annotations as $annotation) {
138 7
            if (! $annotation instanceof $name) {
139 7
                continue;
140
            }
141
142 7
            return $annotation;
143
        }
144
145 4
        return null;
146
    }
147
148
    /**
149
     * @return PropertyMetadata[]
150
     */
151 7
    private function assembleProperties(ReflectionClass $class) : array
152
    {
153 7
        $metadatas = [];
154
155 7
        foreach ($class->getProperties(ReflectionProperty::IS_PUBLIC) as $i => $property) {
156 6
            $metadatas[] = $this->assembleProperty($property);
157
        }
158
159 7
        return $metadatas;
160
    }
161
162 6
    private function assembleProperty(ReflectionProperty $property) : PropertyMetadata
163
    {
164 6
        $docBlock = $property->getDocComment();
165
166 6
        if ($docBlock === false) {
167 2
            return new PropertyMetadata(
168 2
                $property->getName(),
169 2
                new TypeConstraint(new MixedType())
170
            );
171
        }
172
173 4
        $scope               = $this->scopeManufacturer->manufacturePropertyScope($property);
174 4
        $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

174
        $hydratedAnnotations = $this->hydrateInternalAnnotations($this->parser->compile(/** @scrutinizer ignore-type */ $docBlock), $scope);
Loading history...
175
176
        /** @var RequiredAnnotation|null $required */
177 4
        $required = $this->findAnnotation(RequiredAnnotation::class, $hydratedAnnotations);
178
        /** @var Enum|null $enum */
179 4
        $enum = $this->findAnnotation(Enum::class, $hydratedAnnotations);
180
181 4
        $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

181
        $type = $this->typeParser->parsePropertyType(/** @scrutinizer ignore-type */ $property->getDocComment(), $scope);
Loading history...
182
183 4
        return new PropertyMetadata(
184 4
            $property->getName(),
185 4
            $this->determinePropertyConstraint($type, $required, $enum)
186
        );
187
    }
188
189 4
    private function determinePropertyConstraint(Type $type, ?RequiredAnnotation $required, ?Enum $enum) : Constraint
190
    {
191 4
        if ($required && ! $type->acceptsNull()) {
192
            // TODO: throw deprecated warning?
193 1
            $type = new UnionType($type, new NullType());
194
        }
195
196 4
        $constraints = [new TypeConstraint($type)];
197
198 4
        if ($required) {
199 1
            $constraints[] = new RequiredConstraint();
200
        }
201
202 4
        if ($enum) {
203 1
            $constraints[] = new EnumConstraint($enum->value);
204
        }
205
206 4
        if (count($constraints) === 1) {
207 2
            return $constraints[0];
208
        }
209
210 2
        return new CompositeConstraint(...$constraints);
211
    }
212
}
213