Completed
Push — master ( 4b8596...171883 )
by Karsten
02:14
created

Builder::getDeclaringClassInInheritanceChain()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 10
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 10
ccs 4
cts 4
cp 1
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 4
nc 2
nop 2
crap 3
1
<?php
2
/**
3
 * Created by gerk on 29.09.16 10:21
4
 */
5
6
namespace PeekAndPoke\Component\MetaCore;
7
8
use Doctrine\Common\Annotations\AnnotationReader;
9
use Doctrine\Common\Annotations\AnnotationRegistry;
10
use Doctrine\Common\Annotations\CachedReader;
11
use Doctrine\Common\Annotations\Reader;
12
use Doctrine\Common\Cache\ArrayCache;
13
use PeekAndPoke\Component\MetaCore\DomainModel\Property;
14
use PeekAndPoke\Component\MetaCore\DomainModel\Type;
15
use PeekAndPoke\Component\MetaCore\DomainModel\Type\ObjectType;
16
use PeekAndPoke\Component\MetaCore\DomainModel\TypeRef;
17
use PeekAndPoke\Component\MetaCore\DomainModel\TypeRegistry;
18
use PeekAndPoke\Component\MetaCore\Exception\MetaCoreRuntimeException;
19
use PeekAndPoke\Component\Psi\Psi;
20
use PeekAndPoke\Component\Slumber\Annotation\Slumber\Alias;
21
use PeekAndPoke\Types\Enumerated;
22
23
24
/**
25
 * Builder
26
 *
27
 * @author Karsten J. Gerber <[email protected]>
28
 */
29
class Builder
30
{
31
    /** @var Reader */
32
    private $reader;
33
    /** @var PropertyFilter */
34
    private $propertyFilter;
35
    /** @var PropertyMapper */
36
    private $propertyMapper;
37
38
    /** @var TypeRegistry */
39
    private $typeRegistry;
40
    /** @var array|Type[] Built in type, key being the type name, value a blueprint instance */
41
    private $builtInTypes;
42
43
    /**
44
     * @return Builder
45
     */
46 43
    public static function createDefault()
47
    {
48 43
        static $autoloaderRegistered = false;
49
50 43
        if (!$autoloaderRegistered) {
51 1
            $autoloaderRegistered = true;
52
53
            /** @noinspection ExceptionsAnnotatingAndHandlingInspection */
54 1
            AnnotationRegistry::registerLoader(
55
                function ($class) {
56 15
                    return class_exists($class) || interface_exists($class) || trait_exists($class);
57 1
                }
58
            );
59
        }
60
61 43
        return new static(
62 43
            new CachedReader(new AnnotationReader(), new ArrayCache()),
63 43
            new DefaultPropertyFilter(),
64 43
            new DefaultPropertyMapper()
65
        );
66
    }
67
68
    /**
69
     * C'tor
70
     *
71
     * @param Reader         $reader
72
     * @param PropertyFilter $propertyFilter
73
     * @param PropertyMapper $propertyMapper
74
     */
75 44
    public function __construct(Reader $reader, PropertyFilter $propertyFilter, PropertyMapper $propertyMapper)
76
    {
77 44
        $this->reader         = $reader;
78 44
        $this->propertyFilter = $propertyFilter;
79 44
        $this->propertyMapper = $propertyMapper;
80
81 44
        $this->typeRegistry    = new TypeRegistry();
82
83 44
        $this->builtInTypes = [
84 44
            DomainModel\Type\AnyType::TYPE => new DomainModel\Type\AnyType(),
85 44
            DomainModel\Type\BooleanType::TYPE => new DomainModel\Type\BooleanType(),
86 44
            DomainModel\Type\DateTimeType::TYPE => new DomainModel\Type\DateTimeType(),
87 44
            DomainModel\Type\DoubleType::TYPE => new DomainModel\Type\DoubleType(),
88 44
            DomainModel\Type\FloatType::TYPE => new DomainModel\Type\FloatType(),
89 44
            DomainModel\Type\IntType::TYPE => new DomainModel\Type\IntType(),
90 44
            DomainModel\Type\LocalDateTimeType::TYPE => new DomainModel\Type\LocalDateTimeType(),
91 44
            DomainModel\Type\StringType::TYPE => new DomainModel\Type\StringType(),
92
        ];
93 44
    }
94
95
    /**
96
     * @return PropertyFilter
97
     */
98 1
    public function getPropertyFilter()
99
    {
100 1
        return $this->propertyFilter;
101
    }
102
103
    /**
104
     * @return PropertyMapper
105
     */
106 1
    public function getPropertyMapper()
107
    {
108 1
        return $this->propertyMapper;
109
    }
110
111
    /**
112
     * @return TypeRegistry
113
     */
114 1
    public function getTypeRegistry()
115
    {
116 1
        return $this->typeRegistry;
117
    }
118
119
    /**
120
     * @return Type[]
121
     */
122 1
    public function getBuiltInTypes(): array
123
    {
124 1
        return $this->builtInTypes;
125
    }
126
127
    /**
128
     * Build the full name for a type.
129
     *
130
     * For simple types this will return "String", "Int", etc.
131
     *
132
     * For generic type this will return e.g. Map<String,List<Int>>
133
     *
134
     * @param TypeRef $ref
135
     *
136
     * @return string
137
     */
138 62
    public function buildFullName(TypeRef $ref): string
139
    {
140 62
        if (count($ref->getParams()) === 0) {
141 62
            return $ref->getId();
142
        }
143
144 12
        $params = [];
145
146 12
        foreach ($ref->getParams() as $param) {
147 12
            $params[] = $this->buildFullName(
148 12
                $param
149
            );
150
        }
151
152 12
        return $ref->getId() . '<' . implode(',', $params) . '>';
153
    }
154
155
    /**
156
     * Build a type from a given TypeRef.
157
     *
158
     * This method is omnipotent, meaning that when ever we request a type that is already
159
     * known, we will return the same instance of the Type.
160
     *
161
     * A type is considered to be known when the fullName
162
     * a) matches a built in type like "String", "DateTime", "Int"
163
     * b) is already present in the type registry
164
     *
165
     * @param TypeRef $ref
166
     *
167
     * @return Type
168
     */
169 34
    public function buildForRef(TypeRef $ref)
170
    {
171 34
        $fullName = $this->buildFullName($ref);
172
173 34
        if (isset($this->builtInTypes[$fullName])) {
174 21
            return $this->builtInTypes[$fullName];
175
        }
176
177 13
        if ($this->typeRegistry->hasById($fullName)) {
178 9
            return $this->typeRegistry->getById($fullName);
179
        }
180
181 11
        if ($ref->getId() === Type\MapType::type()) {
182 6
            $type = Type::map(
183 6
                $ref->getParams()[0] ?? Type::any()->ref(),
184 6
                $ref->getParams()[1] ?? Type::any()->ref()
185
            );
186
187 6
            $this->typeRegistry->add($fullName, $type);
188
189 6
            return $type;
190
        }
191
192 5
        if ($ref->getId() === Type\ListType::type()) {
193 2
            $type = Type::list_($ref->getParams()[0] ?? Type::any()->ref());
194
195 2
            $this->typeRegistry->add($fullName, $type);
196
197 2
            return $type;
198
        }
199
200 3
        return $this->buildForClass(new \ReflectionClass($ref->getId()));
201
    }
202
203
    /**
204
     * Build the type from a given reflection class.
205
     *
206
     * @param \ReflectionClass $class
207
     *
208
     * @return Type
209
     */
210 15
    public function buildForClass(\ReflectionClass $class)
211
    {
212 15
        $id = $class->name;
213
214
        // is there an alias defined on the class using the Slumber\Alias annotation
215 15
        if (class_exists(Alias::class)) {
216
            /** @var Alias|null $aliasAnnotation */
217 15
            $aliasAnnotation = $this->reader->getClassAnnotation($class, Alias::class);
218
            // do we have an alias ?
219 15
            $id = $aliasAnnotation ? $aliasAnnotation->value : $id;
220
        }
221
222 15
        if ($this->typeRegistry->hasById($id)) {
223
            return $this->typeRegistry->getById($id);
224
        }
225
226 15
        if (is_a($class->name, Enumerated::class, true)) {
227
228
            /** @var Enumerated $enum */
229 4
            $enum = $class->name;
230 4
            $type = Type::enum($enum::void());
231
232 4
            $this->typeRegistry->add($this->buildFullName($type->ref()), $type);
233
234
        } else {
235
            // IMPORTANT: we initiate and register an "empty" type in order to break cyclic references.
236 11
            $type = new ObjectType($id);
237 11
            $this->typeRegistry->add($this->buildFullName($type->ref()), $type);
238
239
            // populate the type
240 11
            $this->populateObjectType($type, $class);
241
        }
242
243 12
        return $type;
244
    }
245
246
    /**
247
     * @param ObjectType $type
248
     *
249
     * @return Property[]
250
     */
251 1
    public function getAllProperties(ObjectType $type)
252
    {
253 1
        $result = $type->getProperties();
254
255 1
        foreach ($type->getExtends() as $extend) {
256
257 1
            $extendType = $this->buildForRef($extend);
258
259 1
            if ($extendType instanceof ObjectType) {
260
                /** @noinspection SlowArrayOperationsInLoopInspection */
261 1
                $result = array_merge($result, $this->getAllProperties($extendType));
262
            }
263
        }
264
265 1
        return $result;
266
    }
267
268
    /**
269
     * @param ObjectType       $type
270
     * @param \ReflectionClass $class
271
     */
272 11
    private function populateObjectType(ObjectType $type, \ReflectionClass $class)
273
    {
274
        /** @var Property[] $properties */
275 11
        $properties = Psi::it($class->getProperties())
276
            // is it a non-static property
277 11
            ->filter(
278
                function (\ReflectionProperty $p) { return $p->isStatic() === false; }
279
            )
280
            // is the property declared by the Class or one of the Traits it uses ?
281 11
            ->filter(
282
                function (\ReflectionProperty $p) use ($class) {
283 10
                    return self::getDeclaringClassInInheritanceChain($class, $p) === $class;
284 11
                }
285
            )
286
            // filter properties
287 11
            ->filter(
288
                function (\ReflectionProperty $p) { return $this->propertyFilter->filterProperty($p); }
289
            )
290
            // map properties
291 11
            ->map(
292
                function (\ReflectionProperty $p) { return $this->buildProperty($p); }
293
            )
294
            // collect
295 11
            ->toArray();
296
297
        // add all the found properties
298 8
        foreach ($properties as $property) {
299 7
            $type->addProperty($property);
300
        }
301
302
        // do we inherit ?
303 8
        if ($class->getParentClass()) {
304 3
            $type->addExtends(
305 3
                $this->buildForClass($class->getParentClass())->ref()
306
            );
307
        }
308 8
    }
309
310
    /**
311
     * @param \ReflectionProperty $property
312
     *
313
     * @return Property
314
     */
315 10
    private function buildProperty(\ReflectionProperty $property)
316
    {
317
        try {
318 10
            return $this->propertyMapper->mapProperty($this, $property);
319
320 3
        } catch (\Exception $e) {
321 3
            throw new MetaCoreRuntimeException(
322 3
                'Error in context of ' . self::getRealDeclaringClass($property)->name . '::' . $property->getName(), 0, $e
323
            );
324
        }
325
    }
326
327
    /**
328
     * TODO: should we extract extended reflection functionality into the Mirror ?
329
     *
330
     * @param \ReflectionProperty $prop
331
     *
332
     * @return \ReflectionClass
333
     */
334 10
    public static function getRealDeclaringClass(\ReflectionProperty $prop)
335
    {
336 10
        return self::getRealDeclaringClassInternal($prop->getDeclaringClass(), $prop);
337
    }
338
339
    /**
340
     * Get the exact class or trait that defines a property.
341
     *
342
     * TODO: should we extract extended reflection functionality into the Mirror ?
343
     *
344
     * @param \ReflectionClass    $class
345
     * @param \ReflectionProperty $prop
346
     *
347
     * @return \ReflectionClass
348
     */
349 10
    private static function getRealDeclaringClassInternal(\ReflectionClass $class, \ReflectionProperty $prop)
350
    {
351 10
        $declaringTrait = Psi::it($class->getTraits())
352
            ->filter(function (\ReflectionClass $r) use ($prop) { return $r->hasProperty($prop->getName()); })
353 10
            ->getFirst(null);
354
355
        // We found it on the traits. No we need to recurse on traits to find exactly the one that was defining it
356 10
        if ($declaringTrait !== null) {
357 1
            return self::getRealDeclaringClassInternal($declaringTrait, $prop);
358
        }
359
360 10
        return self::getDeclaringClassInInheritanceChain($class, $prop);
361
    }
362
363
    /**
364
     * Get the class declaring the property after traits are resolved.
365
     *
366
     * TODO: should we extract extended reflection functionality into the Mirror ?
367
     *
368
     * @param \ReflectionClass    $class
369
     * @param \ReflectionProperty $prop
370
     *
371
     * @return \ReflectionClass
372
     */
373 10
    public static function getDeclaringClassInInheritanceChain(\ReflectionClass $class, \ReflectionProperty $prop)
374
    {
375
        // climb up the inheritance tree
376 10
        while ($class->getParentClass() && $class->getParentClass()->hasProperty($prop->getName())) {
377 3
            $class = $class->getParentClass();
378
        }
379
380
        // this is what is left
381 10
        return $class;
382
    }
383
}
384