Completed
Push — master ( 985fb3...ed8706 )
by Karsten
01:50
created

Builder   D

Complexity

Total Complexity 36

Size/Duplication

Total Lines 355
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 28

Test Coverage

Coverage 99.22%

Importance

Changes 0
Metric Value
wmc 36
lcom 1
cbo 28
dl 0
loc 355
ccs 128
cts 129
cp 0.9922
rs 4.4
c 0
b 0
f 0

15 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 19 1
A getPropertyFilter() 0 4 1
A getPropertyMapper() 0 4 1
A getTypeRegistry() 0 4 1
A getBuiltInTypes() 0 4 1
A getRealDeclaringClass() 0 4 1
A getRealDeclaringClassInternal() 0 13 2
A createDefault() 0 21 4
A buildFullName() 0 16 3
B buildForRef() 0 33 5
B buildForClass() 0 35 5
A getAllProperties() 0 16 3
B populateObjectType() 0 37 3
A buildProperty() 0 11 2
A getDeclaringClassInInheritanceChain() 0 10 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
                }
58 8
            );
59 1
        }
60
61 43
        return new static(
62 43
            new CachedReader(new AnnotationReader(), new ArrayCache()),
63 43
            new DefaultPropertyFilter(),
64 43
            new DefaultPropertyMapper()
65 43
        );
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()
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)
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
                $param
149 12
            );
150 12
        }
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->getParamAt(0),
0 ignored issues
show
Bug introduced by
It seems like $ref->getParamAt(0) can be null; however, map() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
184 6
                $ref->getParamAt(1)
0 ignored issues
show
Bug introduced by
It seems like $ref->getParamAt(1) can be null; however, map() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
185 6
            );
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->getParamAt(0));
0 ignored issues
show
Bug introduced by
It seems like $ref->getParamAt(0) can be null; however, list_() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
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 15
        }
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 4
        } 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 1
            }
263 1
        }
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 11
            )
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
                }
285 11
            )
286
            // filter properties
287 11
            ->filter(
288
                function (\ReflectionProperty $p) { return $this->propertyFilter->filterProperty($p); }
289 11
            )
290
            // map properties
291 11
            ->map(
292
                function (\ReflectionProperty $p) { return $this->buildProperty($p); }
293 11
            )
294
            // collect
295 11
            ->toArray();
296
297
        // add all the found properties
298 8
        foreach ($properties as $property) {
299 7
            $type->addProperty($property);
300 8
        }
301
302
        // do we inherit ?
303 8
        if ($class->getParentClass()) {
304 3
            $type->addExtends(
305 3
                $this->buildForClass($class->getParentClass())->ref()
306 3
            );
307 3
        }
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 3
            );
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 2
            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 3
        }
379
380
        // this is what is left
381 10
        return $class;
382
    }
383
}
384