Builder   D
last analyzed

Complexity

Total Complexity 34

Size/Duplication

Total Lines 350
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 26

Test Coverage

Coverage 99.11%

Importance

Changes 0
Metric Value
wmc 34
lcom 1
cbo 26
dl 0
loc 350
ccs 111
cts 112
cp 0.9911
rs 4.6
c 0
b 0
f 0

15 Methods

Rating   Name   Duplication   Size   Complexity  
A createDefault() 0 8 1
B __construct() 0 27 2
A getPropertyFilter() 0 4 1
A getPropertyMapper() 0 4 1
A getTypeRegistry() 0 4 1
A getBuiltInTypes() 0 4 1
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 getRealDeclaringClass() 0 4 1
A getRealDeclaringClassInternal() 0 13 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
        return new static(
49 43
            new CachedReader(new AnnotationReader(), new ArrayCache()),
50 43
            new DefaultPropertyFilter(),
51 43
            new DefaultPropertyMapper()
52
        );
53
    }
54
55
    /**
56
     * C'tor
57
     *
58
     * @param Reader         $reader
59
     * @param PropertyFilter $propertyFilter
60
     * @param PropertyMapper $propertyMapper
61
     */
62 44
    public function __construct(Reader $reader, PropertyFilter $propertyFilter, PropertyMapper $propertyMapper)
63
    {
64 44
        static $autoload = false;
65
66 44
        if (! $autoload) {
67 1
            $autoload = true;
68
            /** @noinspection ExceptionsAnnotatingAndHandlingInspection */
69 1
            AnnotationRegistry::registerLoader('class_exists');
0 ignored issues
show
Deprecated Code introduced by
The method Doctrine\Common\Annotati...istry::registerLoader() has been deprecated with message: this method is deprecated and will be removed in doctrine/annotations 2.0 autoloading should be deferred to the globally registered autoloader by then. For now, use @example AnnotationRegistry::registerLoader('class_exists')

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

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