Passed
Push — master ( 2182b2...873fd5 )
by Pavel
05:21
created

EntityMetadataFactory::doLoadMetadata()   B

Complexity

Conditions 6
Paths 9

Size

Total Lines 32
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 6.0913

Importance

Changes 0
Metric Value
dl 0
loc 32
ccs 19
cts 22
cp 0.8636
rs 8.439
c 0
b 0
f 0
cc 6
eloc 19
nc 9
nop 4
crap 6.0913
1
<?php
2
3
namespace Bankiru\Api\Doctrine;
4
5
use Bankiru\Api\Doctrine\Exception\MappingException;
6
use Bankiru\Api\Doctrine\Mapping\EntityMetadata;
7
use Doctrine\Common\Persistence\Mapping\AbstractClassMetadataFactory;
8
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
9
use Doctrine\Common\Persistence\Mapping\Driver\MappingDriver;
10
use Doctrine\Common\Persistence\Mapping\ReflectionService;
11
use ReflectionException;
12
13
class EntityMetadataFactory extends AbstractClassMetadataFactory
14
{
15
    /** @var  EntityManager */
16
    private $manager;
17
    /** @var  MappingDriver */
18
    private $driver;
19
20
    /** @var string[] */
21
    private $aliases = [];
22
23
    public function registerAlias($namespaceAlias, $namespace)
24
    {
25
        if (array_key_exists($namespaceAlias, $this->aliases)) {
26
            throw new \LogicException(sprintf('Alias "%s" is already registered', $namespaceAlias));
27
        }
28
29
        $this->aliases[$namespaceAlias] = rtrim($namespace, '\\');
30
    }
31
32
    /**
33
     * @param EntityManager $manager
34
     */
35 28
    public function setEntityManager($manager)
36
    {
37 28
        $this->manager = $manager;
38 28
    }
39
40
    /**
41
     * {@inheritDoc}
42
     */
43 28
    protected function loadMetadata($name)
44
    {
45 28
        $loaded = parent::loadMetadata($name);
46 28
        array_map([$this, 'resolveDiscriminatorValue'], array_map([$this, 'getMetadataFor'], $loaded));
47
48 28
        return $loaded;
49
    }
50
51
    /** {@inheritdoc} */
52 28
    protected function initialize()
53
    {
54 28
        $this->driver      = $this->manager->getConfiguration()->getDriver();
55 28
        $this->initialized = true;
56 28
    }
57
58
    /**
59
     * {@inheritdoc}
60
     * @throws MappingException
61
     */
62
    protected function getFqcnFromAlias($namespaceAlias, $simpleClassName)
63
    {
64
        if (!array_key_exists($namespaceAlias, $this->aliases)) {
65
            throw MappingException::unknownAlias($namespaceAlias);
66
        }
67
68
        return $this->aliases[$namespaceAlias] . $simpleClassName;
69
    }
70
71
    /** {@inheritdoc} */
72 28
    protected function wakeupReflection(ClassMetadata $class, ReflectionService $reflService)
73
    {
74 28
        if (!($class instanceof EntityMetadata)) {
75
            throw new \LogicException('Metadata is not supported');
76
        }
77
78
        /** @var EntityMetadata $class */
79 28
        $class->wakeupReflection($reflService);
80 28
    }
81
82
    /**
83
     * Initializes Reflection after ClassMetadata was constructed.
84
     *
85
     * @param ClassMetadata     $class
86
     * @param ReflectionService $reflService
87
     *
88
     * @return void
89
     */
90 28
    protected function initializeReflection(ClassMetadata $class, ReflectionService $reflService)
91
    {
92 28
        if (!($class instanceof EntityMetadata)) {
93
            throw new \LogicException('Metadata is not supported');
94
        }
95
96
        /** @var EntityMetadata $class */
97 28
        $class->initializeReflection($reflService);
98 28
    }
99
100
    /**
101
     * Checks whether the class metadata is an entity.
102
     *
103
     * This method should return false for mapped superclasses or embedded classes.
104
     *
105
     * @param ClassMetadata $class
106
     *
107
     * @return boolean
108
     */
109 28
    protected function isEntity(ClassMetadata $class)
110
    {
111 28
        return true;
112
    }
113
114
    /** {@inheritdoc} */
115 28
    protected function doLoadMetadata($class, $parent, $rootEntityFound, array $nonSuperclassParents)
116
    {
117
        /* @var $class EntityMetadata */
118
        /* @var $parent EntityMetadata */
119 28
        if ($parent) {
120 15
            $this->addInheritedFields($class, $parent);
0 ignored issues
show
Compatibility introduced by
$class of type object<Doctrine\Common\P...\Mapping\ClassMetadata> is not a sub-type of object<Bankiru\Api\Doctr...Mapping\EntityMetadata>. It seems like you assume a concrete implementation of the interface Doctrine\Common\Persistence\Mapping\ClassMetadata to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
121 15
            $this->addInheritedRelations($class, $parent);
0 ignored issues
show
Compatibility introduced by
$class of type object<Doctrine\Common\P...\Mapping\ClassMetadata> is not a sub-type of object<Bankiru\Api\Doctr...Mapping\EntityMetadata>. It seems like you assume a concrete implementation of the interface Doctrine\Common\Persistence\Mapping\ClassMetadata to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
122 15
            $class->setIdentifier($parent->identifier);
123 15
            $class->apiFactory     = $parent->apiFactory;
0 ignored issues
show
Bug introduced by
Accessing apiFactory on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
124 15
            $class->clientName     = $parent->clientName;
0 ignored issues
show
Bug introduced by
Accessing clientName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
125 15
            $class->methodProvider = $parent->methodProvider;
0 ignored issues
show
Bug introduced by
Accessing methodProvider on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
126
127 15
            $class->setIdGeneratorType($parent->generatorType);
128 15
            $class->setDiscriminatorField($parent->discriminatorField);
129 15
            $class->setDiscriminatorMap($parent->discriminatorMap);
130
131 15
            if ($parent->isMappedSuperclass) {
132
                $class->setCustomRepositoryClass($parent->repositoryClass);
133
            }
134 15
        }
135
136
        // Invoke driver
137
        try {
138 28
            $this->getDriver()->loadMetadataForClass($class->getName(), $class);
139 28
        } catch (ReflectionException $e) {
140
            throw MappingException::nonExistingClass($class->getName());
141
        }
142
143 28
        if ($class->isRootEntity() && !$class->discriminatorMap) {
0 ignored issues
show
Bug introduced by
Accessing discriminatorMap on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
144 28
            $this->addDefaultDiscriminatorMap($class);
0 ignored issues
show
Compatibility introduced by
$class of type object<Doctrine\Common\P...\Mapping\ClassMetadata> is not a sub-type of object<Bankiru\Api\Doctr...Mapping\EntityMetadata>. It seems like you assume a concrete implementation of the interface Doctrine\Common\Persistence\Mapping\ClassMetadata to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
145 28
        }
146 28
    }
147
148
    /**
149
     * Returns the mapping driver implementation.
150
     *
151
     * @return \Doctrine\Common\Persistence\Mapping\Driver\MappingDriver
152
     */
153 28
    protected function getDriver()
154
    {
155 28
        return $this->driver;
156
    }
157
158
    /**
159
     * Creates a new ClassMetadata instance for the given class name.
160
     *
161
     * @param string $className
162
     *
163
     * @return ClassMetadata
164
     */
165 28
    protected function newClassMetadataInstance($className)
166
    {
167 28
        return new EntityMetadata($className);
168
    }
169
170
    /**
171
     * Populates the discriminator value of the given metadata (if not set) by iterating over discriminator
172
     * map classes and looking for a fitting one.
173
     *
174
     * @param EntityMetadata $metadata
175
     *
176
     * @return void
177
     *
178
     * @throws MappingException
179
     */
180 28
    private function resolveDiscriminatorValue(EntityMetadata $metadata)
181
    {
182 28
        if ($metadata->discriminatorValue
183 4
            || !$metadata->discriminatorMap
0 ignored issues
show
Bug Best Practice introduced by
The expression $metadata->discriminatorMap of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
184 4
            || $metadata->isMappedSuperclass
185 4
            || !$metadata->reflClass
186 4
            || $metadata->reflClass->isAbstract()
187 28
        ) {
188 28
            return;
189
        }
190
        // minor optimization: avoid loading related metadata when not needed
191
        foreach ($metadata->discriminatorMap as $discriminatorValue => $discriminatorClass) {
192
            if ($discriminatorClass === $metadata->name) {
193
                $metadata->discriminatorValue = $discriminatorValue;
0 ignored issues
show
Documentation Bug introduced by
The property $discriminatorValue was declared of type string, but $discriminatorValue is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
194
195
                return;
196
            }
197
        }
198
        // iterate over discriminator mappings and resolve actual referenced classes according to existing metadata
199
        foreach ($metadata->discriminatorMap as $discriminatorValue => $discriminatorClass) {
200
            if ($metadata->name === $this->getMetadataFor($discriminatorClass)->getName()) {
201
                $metadata->discriminatorValue = $discriminatorValue;
202
203
                return;
204
            }
205
        }
206
207
        throw MappingException::mappedClassNotPartOfDiscriminatorMap($metadata->name, $metadata->rootEntityName);
208
    }
209
210
    /**
211
     * Adds a default discriminator map if no one is given
212
     *
213
     * If an entity is of any inheritance type and does not contain a
214
     * discriminator map, then the map is generated automatically. This process
215
     * is expensive computation wise.
216
     *
217
     * The automatically generated discriminator map contains the lowercase short name of
218
     * each class as key.
219
     *
220
     * @param EntityMetadata $class
221
     *
222
     * @throws MappingException
223
     */
224 28
    private function addDefaultDiscriminatorMap(EntityMetadata $class)
225
    {
226 28
        $allClasses = $this->driver->getAllClassNames();
227 28
        $fqcn       = $class->getName();
228 28
        $map        = [$this->getShortName($class->name) => $fqcn];
229 28
        $duplicates = [];
230 28
        foreach ($allClasses as $subClassCandidate) {
231 28
            if (is_subclass_of($subClassCandidate, $fqcn)) {
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if $fqcn can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
232 22
                $shortName = $this->getShortName($subClassCandidate);
233 22
                if (isset($map[$shortName])) {
234
                    $duplicates[] = $shortName;
235
                }
236 22
                $map[$shortName] = $subClassCandidate;
237 22
            }
238 28
        }
239 28
        if ($duplicates) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $duplicates of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
240
            throw MappingException::duplicateDiscriminatorEntry($class->name, $duplicates, $map);
241
        }
242 28
        $class->setDiscriminatorMap($map);
243 28
    }
244
245
    /**
246
     * Gets the lower-case short name of a class.
247
     *
248
     * @param string $className
249
     *
250
     * @return string
251
     */
252 28
    private function getShortName($className)
253
    {
254 28
        if (strpos($className, "\\") === false) {
255
            return strtolower($className);
256
        }
257 28
        $parts = explode("\\", $className);
258
259 28
        return strtolower(end($parts));
260
    }
261
262
    /**
263
     * Adds inherited fields to the subclass mapping.
264
     *
265
     * @param EntityMetadata $subClass
266
     * @param EntityMetadata $parentClass
267
     *
268
     * @return void
269
     */
270 15
    private function addInheritedFields(EntityMetadata $subClass, EntityMetadata $parentClass)
271
    {
272 15 View Code Duplication
        foreach ($parentClass->fields as $mapping) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
273 15
            if (!isset($mapping['inherited']) && !$parentClass->isMappedSuperclass) {
274 15
                $mapping['inherited'] = $parentClass->name;
275 15
            }
276 15
            if (!isset($mapping['declared'])) {
277 15
                $mapping['declared'] = $parentClass->name;
278 15
            }
279 15
            $subClass->addInheritedFieldMapping($mapping);
280 15
        }
281 15
        foreach ($parentClass->reflFields as $name => $field) {
282 15
            $subClass->reflFields[$name] = $field;
283 15
        }
284 15
    }
285
286
    /**
287
     * Adds inherited association mappings to the subclass mapping.
288
     *
289
     * @param EntityMetadata $subClass
290
     * @param EntityMetadata $parentClass
291
     *
292
     * @return void
293
     *
294
     * @throws MappingException
295
     */
296 15
    private function addInheritedRelations(EntityMetadata $subClass, EntityMetadata $parentClass)
297
    {
298 15 View Code Duplication
        foreach ($parentClass->associations as $mapping) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
299 10
            if (!isset($mapping['inherited']) && !$parentClass->isMappedSuperclass) {
300 10
                $mapping['inherited'] = $parentClass->name;
301 10
            }
302 10
            if (!isset($mapping['declared'])) {
303 10
                $mapping['declared'] = $parentClass->name;
304 10
            }
305 10
            $subClass->addInheritedAssociationMapping($mapping);
306 15
        }
307 15
    }
308
}
309