Passed
Pull Request — master (#14)
by Pavel
03:23
created

EntityMetadata   D

Complexity

Total Complexity 149

Size/Duplication

Total Lines 837
Duplicated Lines 2.15 %

Coupling/Cohesion

Components 2
Dependencies 4

Test Coverage

Coverage 77.65%

Importance

Changes 0
Metric Value
dl 18
loc 837
c 0
b 0
f 0
wmc 149
lcom 2
cbo 4
ccs 271
cts 349
cp 0.7765
rs 4.4444

71 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A containsForeignIdentifier() 0 4 1
A getReflectionProperties() 0 4 1
A getReflectionProperty() 0 8 2
A getName() 0 4 1
A getMethodContainer() 0 4 1
A getRepositoryClass() 0 4 1
A getIdentifier() 0 4 1
A setIdentifier() 0 5 1
A getReflectionClass() 0 8 2
A isIdentifier() 0 4 1
A hasField() 0 4 1
A getFieldNames() 0 4 1
A hasAssociation() 0 4 1
A getAssociationNames() 0 4 1
A isSingleValuedAssociation() 0 4 2
A isCollectionValuedAssociation() 0 4 2
A getIdentifierFieldNames() 0 4 1
A getTypeOfField() 0 4 1
A getAssociationTargetClass() 0 4 1
A isAssociationInverseSide() 0 6 1
A getAssociationMappedByTargetField() 0 4 1
B getIdentifierValues() 0 21 5
B wakeupReflection() 8 16 6
A initializeReflection() 0 8 2
A getApiFactory() 0 8 2
A getClientName() 0 8 2
A mapField() 0 6 1
A getFieldMapping() 0 8 2
A getFieldOptions() 0 4 1
A getAssociationMapping() 0 8 2
A setCustomRepositoryClass() 0 4 1
A addInheritedFieldMapping() 0 6 1
A getFieldName() 0 4 1
A getApiFieldName() 0 4 1
A hasApiField() 0 4 1
A mapOneToMany() 0 6 1
A mapManyToOne() 0 6 1
A mapOneToOne() 0 6 1
A newInstance() 0 4 1
A isIdentifierComposite() 0 4 1
A getRootEntityName() 0 4 1
A assignIdentifier() 0 6 2
A addInheritedAssociationMapping() 0 6 1
A getSubclasses() 0 4 1
A getAssociationMappings() 0 4 1
A isReadOnly() 0 4 1
A setChangeTrackingPolicy() 0 4 1
A isChangeTrackingDeferredExplicit() 0 4 1
A isChangeTrackingDeferredImplicit() 0 4 1
A isChangeTrackingNotify() 0 4 1
A mapIdentifier() 0 6 1
A setIdGeneratorType() 0 4 1
A isIdentifierNatural() 0 4 1
A isIdentifierRemote() 0 4 1
A setIdentifierValues() 0 6 2
A isRootEntity() 0 4 1
B addDiscriminatorMapClass() 0 17 6
B fullyQualifiedClassName() 0 11 5
A getDiscriminatorField() 0 4 1
B setDiscriminatorField() 0 21 7
A getDiscriminatorMap() 0 4 1
A setDiscriminatorMap() 0 6 2
A getDiscriminatorValue() 0 4 1
F validateAndCompleteAssociationMapping() 10 89 25
A storeMapping() 0 8 1
C validateAndCompleteFieldMapping() 0 28 8
A assertFieldNotMapped() 0 9 4
A validateAndCompleteOneToManyMapping() 0 16 4
A assertMappingOrderBy() 0 8 3
A validateAndCompleteOneToOneMapping() 0 12 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like EntityMetadata often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use EntityMetadata, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Bankiru\Api\Doctrine\Mapping;
4
5
use Bankiru\Api\Doctrine\EntityRepository;
6
use Bankiru\Api\Doctrine\Exception\MappingException;
7
use Bankiru\Api\Doctrine\Rpc\Method\MethodProviderInterface;
8
use Doctrine\Common\Persistence\Mapping\ReflectionService;
9
use Doctrine\Instantiator\Instantiator;
10
use Doctrine\Instantiator\InstantiatorInterface;
11
12
class EntityMetadata implements ApiMetadata
13
{
14
    /**
15
     * The ReflectionProperty instances of the mapped class.
16
     *
17
     * @var \ReflectionProperty[]
18
     */
19
    public $reflFields = [];
20
    /** @var string */
21
    public $name;
22
    /** @var string */
23
    public $namespace;
24
    /** @var string */
25
    public $rootEntityName;
26
    /** @var string[] */
27
    public $identifier = [];
28
    /** @var array */
29
    public $fields = [];
30
    /** @var array */
31
    public $associations = [];
32
    /** @var string */
33
    public $repositoryClass = EntityRepository::class;
34
    /** @var \ReflectionClass */
35
    public $reflClass;
36
    /** @var MethodProviderInterface */
37
    public $methodProvider;
38
    /** @var string */
39
    public $clientName;
40
    /** @var string */
41
    public $apiFactory;
42
    /** @var string[] */
43
    public $apiFieldNames = [];
44
    /** @var string[] */
45
    public $fieldNames = [];
46
    /** @var bool */
47
    public $isMappedSuperclass = false;
48
    /** @var bool */
49
    public $containsForeignIdentifier;
50
    /** @var bool */
51
    public $isIdentifierComposite = false;
52
    /** @var int */
53
    public $generatorType = self::GENERATOR_TYPE_NATURAL;
54
    /** @var string[] */
55
    public $discriminatorField;
56
    /** @var string[] */
57
    public $discriminatorMap;
58
    /** @var string */
59
    public $discriminatorValue;
60
    /** @var string[] */
61
    public $subclasses = [];
62
    /** @var InstantiatorInterface */
63
    private $instantiator;
64
    /** @var int */
65
    private $changeTrackingPolicy = self::CHANGETRACKING_DEFERRED_IMPLICIT;
66
67
    /**
68
     * Initializes a new ClassMetadata instance that will hold the object-relational mapping
69
     * metadata of the class with the given name.
70
     *
71
     * @param string $entityName The name of the entity class the new instance is used for.
72
     */
73 27
    public function __construct($entityName)
74
    {
75 27
        $this->name           = $entityName;
76 27
        $this->rootEntityName = $entityName;
77 27
    }
78
79
    /**
80
     * @return boolean
81
     */
82
    public function containsForeignIdentifier()
83
    {
84
        return $this->containsForeignIdentifier;
85
    }
86
87
    /** {@inheritdoc} */
88 11
    public function getReflectionProperties()
89
    {
90 11
        return $this->reflFields;
91
    }
92
93
    /**
94
     * {@inheritdoc}
95
     */
96 24
    public function getReflectionProperty($name)
97
    {
98 24
        if (!array_key_exists($name, $this->reflFields)) {
99
            throw MappingException::noSuchProperty($name, $this->getName());
100
        }
101
102 24
        return $this->reflFields[$name];
103
    }
104
105
    /** {@inheritdoc} */
106 27
    public function getName()
107
    {
108 27
        return $this->name;
109
    }
110
111
    /** {@inheritdoc} */
112 26
    public function getMethodContainer()
113
    {
114 26
        return $this->methodProvider;
115
    }
116
117
    /** {@inheritdoc} */
118 17
    public function getRepositoryClass()
119
    {
120 17
        return $this->repositoryClass;
121
    }
122
123
    /** {@inheritdoc} */
124 2
    public function getIdentifier()
125
    {
126 2
        return $this->identifier;
127
    }
128
129 14
    public function setIdentifier($identifier)
130
    {
131 14
        $this->identifier            = $identifier;
132 14
        $this->isIdentifierComposite = (count($this->identifier) > 1);
133 14
    }
134
135
    /** {@inheritdoc} */
136 17
    public function getReflectionClass()
137
    {
138 17
        if (null === $this->reflClass) {
139
            $this->reflClass = new \ReflectionClass($this->getName());
140
        }
141
142 17
        return $this->reflClass;
143
    }
144
145
    /** {@inheritdoc} */
146 7
    public function isIdentifier($fieldName)
147
    {
148 7
        return in_array($fieldName, $this->identifier, true);
149
    }
150
151
    /** {@inheritdoc} */
152 2
    public function hasField($fieldName)
153
    {
154 2
        return in_array($fieldName, $this->getFieldNames(), true);
155
    }
156
157
    /** {@inheritdoc} */
158 17
    public function getFieldNames()
159
    {
160 17
        return array_keys($this->fields);
161
    }
162
163
    /** {@inheritdoc} */
164 25
    public function hasAssociation($fieldName)
165
    {
166 25
        return in_array($fieldName, $this->getAssociationNames(), true);
167
    }
168
169
    /** {@inheritdoc} */
170 25
    public function getAssociationNames()
171
    {
172 25
        return array_keys($this->associations);
173
    }
174
175
    /** {@inheritdoc} */
176 11
    public function isSingleValuedAssociation($fieldName)
177
    {
178 11
        return $this->hasAssociation($fieldName) && $this->associations[$fieldName]['type'] & self::TO_ONE;
179
    }
180
181
    /** {@inheritdoc} */
182 15
    public function isCollectionValuedAssociation($fieldName)
183
    {
184 15
        return $this->hasAssociation($fieldName) && $this->associations[$fieldName]['type'] & self::TO_MANY;
185
    }
186
187
    /** {@inheritdoc} */
188 20
    public function getIdentifierFieldNames()
189
    {
190 20
        return $this->identifier;
191
    }
192
193
    /** {@inheritdoc} */
194 25
    public function getTypeOfField($fieldName)
195
    {
196 25
        return $this->fields[$fieldName]['type'];
197
    }
198
199
    /** {@inheritdoc} */
200 11
    public function getAssociationTargetClass($assocName)
201
    {
202 11
        return $this->associations[$assocName]['target'];
203
    }
204
205
    /** {@inheritdoc} */
206
    public function isAssociationInverseSide($assocName)
207
    {
208
        $assoc = $this->associations[$assocName];
209
210
        return array_key_exists('mappedBy', $assoc);
211
    }
212
213
    /** {@inheritdoc} */
214
    public function getAssociationMappedByTargetField($assocName)
215
    {
216
        return $this->associations[$assocName]['mappedBy'];
217
    }
218
219
    /** {@inheritdoc} */
220 21
    public function getIdentifierValues($object)
221
    {
222 21
        if ($this->isIdentifierComposite) {
223 1
            $id = [];
224 1
            foreach ($this->identifier as $idField) {
225 1
                $value = $this->reflFields[$idField]->getValue($object);
226 1
                if ($value !== null) {
227 1
                    $id[$idField] = $value;
228 1
                }
229 1
            }
230
231 1
            return $id;
232
        }
233 20
        $id    = $this->identifier[0];
234 20
        $value = $this->reflFields[$id]->getValue($object);
235 20
        if (null === $value) {
236
            return [];
237
        }
238
239 20
        return [$id => $value];
240
    }
241
242
    /** {@inheritdoc} */
243 27
    public function wakeupReflection(ReflectionService $reflService)
244
    {
245
        // Restore ReflectionClass and properties
246 27
        $this->reflClass    = $reflService->getClass($this->name);
247 27
        $this->instantiator = $this->instantiator ?: new Instantiator();
248
249 27 View Code Duplication
        foreach ($this->fields as $field => $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...
250 27
            $class                    = array_key_exists('declared', $mapping) ? $mapping['declared'] : $this->name;
251 27
            $this->reflFields[$field] = $reflService->getAccessibleProperty($class, $field);
252 27
        }
253
254 27 View Code Duplication
        foreach ($this->associations as $field => $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...
255 17
            $class                    = array_key_exists('declared', $mapping) ? $mapping['declared'] : $this->name;
256 17
            $this->reflFields[$field] = $reflService->getAccessibleProperty($class, $field);
257 27
        }
258 27
    }
259
260
    /** {@inheritdoc} */
261 27
    public function initializeReflection(ReflectionService $reflService)
262
    {
263 27
        $this->reflClass = $reflService->getClass($this->name);
264 27
        $this->namespace = $reflService->getClassNamespace($this->name);
265 27
        if ($this->reflClass) {
266 27
            $this->name = $this->rootEntityName = $this->reflClass->getName();
0 ignored issues
show
Bug introduced by
Consider using $this->reflClass->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
267 27
        }
268 27
    }
269
270
    /** {@inheritdoc} */
271 25
    public function getApiFactory()
272
    {
273 25
        if (null === $this->apiFactory) {
274
            throw MappingException::noApiSpecified($this->getName());
275
        }
276
277 25
        return $this->apiFactory;
278
    }
279
280
    /** {@inheritdoc} */
281 26
    public function getClientName()
282
    {
283 26
        if (null === $this->clientName) {
284
            throw MappingException::noClientSpecified($this->getName());
285
        }
286
287 26
        return $this->clientName;
288
    }
289
290 27
    public function mapField(array $mapping)
291
    {
292 27
        $this->validateAndCompleteFieldMapping($mapping);
293 27
        $this->assertFieldNotMapped($mapping['field']);
294 27
        $this->fields[$mapping['field']] = $mapping;
295 27
    }
296
297
    /** {@inheritdoc} */
298 25
    public function getFieldMapping($fieldName)
299
    {
300 25
        if (!isset($this->fields[$fieldName])) {
301
            throw MappingException::unknownField($fieldName, $this->getName());
302
        }
303
304 25
        return $this->fields[$fieldName];
305
    }
306
307
    /** {@inheritdoc} */
308 25
    public function getFieldOptions($fieldName)
309
    {
310 25
        return $this->getFieldMapping($fieldName)['options'];
311
    }
312
313
    /** {@inheritdoc} */
314 16
    public function getAssociationMapping($fieldName)
315
    {
316 16
        if (!isset($this->associations[$fieldName])) {
317
            throw MappingException::unknownAssociation($fieldName, $this->getName());
318
        }
319
320 16
        return $this->associations[$fieldName];
321
    }
322
323 2
    public function setCustomRepositoryClass($customRepositoryClassName)
324
    {
325 2
        $this->repositoryClass = $customRepositoryClassName;
326 2
    }
327
328
    /**
329
     * @internal
330
     *
331
     * @param array $mapping
332
     *
333
     * @return void
334
     */
335 14
    public function addInheritedFieldMapping(array $mapping)
336
    {
337 14
        $this->fields[$mapping['field']]         = $mapping;
338 14
        $this->apiFieldNames[$mapping['field']]  = $mapping['api_field'];
339 14
        $this->fieldNames[$mapping['api_field']] = $mapping['field'];
340 14
    }
341
342
    /** {@inheritdoc} */
343 7
    public function getFieldName($apiFieldName)
344
    {
345 7
        return $this->fieldNames[$apiFieldName];
346
    }
347
348
    /** {@inheritdoc} */
349 25
    public function getApiFieldName($fieldName)
350
    {
351 25
        return $this->apiFieldNames[$fieldName];
352
    }
353
354
    public function hasApiField($apiFieldName)
355
    {
356
        return array_key_exists($apiFieldName, $this->fieldNames);
357
    }
358
359 17
    public function mapOneToMany(array $mapping)
360
    {
361 17
        $mapping = $this->validateAndCompleteOneToManyMapping($mapping);
362
363 17
        $this->storeMapping($mapping);
364 17
    }
365
366 17
    public function mapManyToOne(array $mapping)
367
    {
368 17
        $mapping = $this->validateAndCompleteOneToOneMapping($mapping);
369
370 17
        $this->storeMapping($mapping);
371 17
    }
372
373
    public function mapOneToOne(array $mapping)
374
    {
375
        $mapping = $this->validateAndCompleteOneToOneMapping($mapping);
376
377
        $this->storeMapping($mapping);
378
    }
379
380
    /** {@inheritdoc} */
381 16
    public function newInstance()
382
    {
383 16
        return $this->instantiator->instantiate($this->name);
384
    }
385
386 7
    public function isIdentifierComposite()
387
    {
388 7
        return $this->isIdentifierComposite;
389
    }
390
391
    /** {@inheritdoc} */
392 27
    public function getRootEntityName()
393
    {
394 27
        return $this->rootEntityName;
395
    }
396
397
    /**
398
     * Populates the entity identifier of an entity.
399
     *
400
     * @param object $entity
401
     * @param array  $id
402
     *
403
     * @return void
404
     */
405
    public function assignIdentifier($entity, array $id)
406
    {
407
        foreach ($id as $idField => $idValue) {
408
            $this->reflFields[$idField]->setValue($entity, $idValue);
409
        }
410
    }
411
412 10
    public function addInheritedAssociationMapping(array $mapping)
413
    {
414 10
        $this->associations[$mapping['field']]   = $mapping;
415 10
        $this->apiFieldNames[$mapping['field']]  = $mapping['api_field'];
416 10
        $this->fieldNames[$mapping['api_field']] = $mapping['field'];
417 10
    }
418
419
    /** {@inheritdoc} */
420 7
    public function getSubclasses()
421
    {
422 7
        return $this->subclasses;
423
    }
424
425
    /** {@inheritdoc} */
426 7
    public function getAssociationMappings()
427
    {
428 7
        return $this->associations;
429
    }
430
431 3
    public function isReadOnly()
432
    {
433 3
        return false;
434
    }
435
436
    /**
437
     * Sets the change tracking policy used by this class.
438
     *
439
     * @param integer $policy
440
     *
441
     * @return void
442
     */
443
    public function setChangeTrackingPolicy($policy)
444
    {
445
        $this->changeTrackingPolicy = $policy;
446
    }
447
448
    /**
449
     * Whether the change tracking policy of this class is "deferred explicit".
450
     *
451
     * @return boolean
452
     */
453
    public function isChangeTrackingDeferredExplicit()
454
    {
455
        return $this->changeTrackingPolicy === self::CHANGETRACKING_DEFERRED_EXPLICIT;
456
    }
457
458
    /**
459
     * Whether the change tracking policy of this class is "deferred implicit".
460
     *
461
     * @return boolean
462
     */
463 3
    public function isChangeTrackingDeferredImplicit()
464
    {
465 3
        return $this->changeTrackingPolicy === self::CHANGETRACKING_DEFERRED_IMPLICIT;
466
    }
467
468
    /**
469
     * Whether the change tracking policy of this class is "notify".
470
     *
471
     * @return boolean
472
     */
473
    public function isChangeTrackingNotify()
474
    {
475
        return $this->changeTrackingPolicy === self::CHANGETRACKING_NOTIFY;
476
    }
477
478 27
    public function mapIdentifier(array $mapping)
479
    {
480 27
        $this->setIdGeneratorType($mapping['generator']['strategy']);
481
482 27
        $this->mapField($mapping);
483 27
    }
484
485
    /**
486
     * Sets the type of Id generator to use for the mapped class.
487
     *
488
     * @param int $generatorType
489
     *
490
     * @return void
491
     */
492 27
    public function setIdGeneratorType($generatorType)
493
    {
494 27
        $this->generatorType = $generatorType;
495 27
    }
496
497 7
    public function isIdentifierNatural()
498
    {
499 7
        return $this->generatorType === self::GENERATOR_TYPE_NATURAL;
500
    }
501
502 7
    public function isIdentifierRemote()
503
    {
504 7
        return $this->generatorType === self::GENERATOR_TYPE_REMOTE;
505
    }
506
507
    /**
508
     * Populates the entity identifier of an entity.
509
     *
510
     * @param object $entity
511
     * @param array  $id
512
     *
513
     * @return void
514
     *
515
     * @todo Rename to assignIdentifier()
516
     */
517
    public function setIdentifierValues($entity, array $id)
518
    {
519
        foreach ($id as $idField => $idValue) {
520
            $this->reflFields[$idField]->setValue($entity, $idValue);
521
        }
522
    }
523
524 27
    public function isRootEntity()
525
    {
526 27
        return $this->getRootEntityName() === $this->getName();
527
    }
528
529
    /**
530
     * Adds one entry of the discriminator map with a new class and corresponding name.
531
     *
532
     * @param string $name
533
     * @param string $className
534
     *
535
     * @return void
536
     *
537
     * @throws MappingException
538
     */
539 27
    public function addDiscriminatorMapClass($name, $className)
540
    {
541 27
        $className                     = $this->fullyQualifiedClassName($className);
542 27
        $className                     = ltrim($className, '\\');
543 27
        $this->discriminatorMap[$name] = $className;
544 27
        if ($this->name === $className) {
545 27
            $this->discriminatorValue = $name;
546
547 27
            return;
548
        }
549 20
        if (!(class_exists($className) || interface_exists($className))) {
550
            throw MappingException::invalidClassInDiscriminatorMap($className, $this->name);
551
        }
552 20
        if (is_subclass_of($className, $this->name) && !in_array($className, $this->subclasses)) {
1 ignored issue
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if $this->name can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
553 20
            $this->subclasses[] = $className;
554 20
        }
555 20
    }
556
557
    /**
558
     * @param  string|null $className
559
     *
560
     * @return string|null null if the input value is null
561
     */
562 27
    public function fullyQualifiedClassName($className)
563
    {
564 27
        if (empty($className)) {
565
            return $className;
566
        }
567 27
        if ($className !== null && strpos($className, '\\') === false && $this->namespace) {
568
            return $this->namespace . '\\' . $className;
569
        }
570
571 27
        return $className;
572
    }
573
574
    /** {@inheritdoc} */
575 25
    public function getDiscriminatorField()
576
    {
577 25
        return $this->discriminatorField;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->discriminatorField; (string[]) is incompatible with the return type declared by the interface Bankiru\Api\Doctrine\Map...::getDiscriminatorField of type string[]|null.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
578
    }
579
580
    /**
581
     * Sets the discriminator column definition.
582
     *
583
     * @param array $columnDef
584
     *
585
     * @return void
586
     *
587
     * @throws MappingException
588
     *
589
     * @see getDiscriminatorColumn()
590
     */
591 14
    public function setDiscriminatorField(array $columnDef = null)
592
    {
593 14
        if ($columnDef !== null) {
594 4
            if (!isset($columnDef['name'])) {
595
                throw MappingException::nameIsMandatoryForDiscriminatorColumns($this->name);
596
            }
597 4
            if (isset($this->fieldNames[$columnDef['name']])) {
598
                throw MappingException::duplicateColumnName($this->name, $columnDef['name']);
599
            }
600 4
            if (!isset($columnDef['fieldName'])) {
601 4
                $columnDef['fieldName'] = $columnDef['name'];
602 4
            }
603 4
            if (!isset($columnDef['type'])) {
604
                $columnDef['type'] = 'string';
605
            }
606 4
            if (in_array($columnDef['type'], ['boolean', 'array', 'object', 'datetime', 'time', 'date'], true)) {
607
                throw MappingException::invalidDiscriminatorColumnType($this->name, $columnDef['type']);
608
            }
609 4
            $this->discriminatorField = $columnDef;
0 ignored issues
show
Documentation Bug introduced by
It seems like $columnDef of type array<string,?,{"type":"string"}> is incompatible with the declared type array<integer,string> of property $discriminatorField.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
610 4
        }
611 14
    }
612
613
    /** {@inheritdoc} */
614 17
    public function getDiscriminatorMap()
615
    {
616 17
        return $this->discriminatorMap;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->discriminatorMap; (string[]) is incompatible with the return type declared by the interface Bankiru\Api\Doctrine\Map...ta::getDiscriminatorMap of type string[].

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
617
    }
618
619
    /**
620
     * Sets the discriminator values used by this class.
621
     * Used for JOINED and SINGLE_TABLE inheritance mapping strategies.
622
     *
623
     * @param array $map
624
     *
625
     * @return void
626
     */
627 27
    public function setDiscriminatorMap(array $map)
628
    {
629 27
        foreach ($map as $value => $className) {
630 27
            $this->addDiscriminatorMapClass($value, $className);
631 27
        }
632 27
    }
633
634
    /** {@inheritdoc} */
635 18
    public function getDiscriminatorValue()
636
    {
637 18
        return $this->discriminatorValue;
638
    }
639
640
    /**
641
     * Validates & completes the basic mapping information that is common to all
642
     * association mappings (one-to-one, many-ot-one, one-to-many, many-to-many).
643
     *
644
     * @param array $mapping The mapping.
645
     *
646
     * @return array The updated mapping.
647
     *
648
     * @throws MappingException If something is wrong with the mapping.
649
     */
650 17
    protected function validateAndCompleteAssociationMapping(array $mapping)
651
    {
652 17
        if (!array_key_exists('api_field', $mapping)) {
653 17
            $mapping['api_field'] = $mapping['field'];
654 17
        }
655
656 17
        if (!isset($mapping['mappedBy'])) {
657 17
            $mapping['mappedBy'] = null;
658 17
        }
659
660 17
        if (!isset($mapping['inversedBy'])) {
661 17
            $mapping['inversedBy'] = null;
662 17
        }
663
664 17
        $mapping['isOwningSide'] = true; // assume owning side until we hit mappedBy
665
666
        // unset optional indexBy attribute if its empty
667 17
        if (!isset($mapping['indexBy']) || !$mapping['indexBy']) {
668 17
            unset($mapping['indexBy']);
669 17
        }
670
671
        // If targetEntity is unqualified, assume it is in the same namespace as
672
        // the sourceEntity.
673 17
        $mapping['source'] = $this->name;
674 17
        if (isset($mapping['target'])) {
675 17
            $mapping['target'] = ltrim($mapping['target'], '\\');
676 17
        }
677
678 17
        if (($mapping['type'] & self::MANY_TO_ONE) > 0 &&
679 17
            isset($mapping['orphanRemoval']) &&
680
            $mapping['orphanRemoval'] == true
681 17
        ) {
682
            throw new MappingException(
683
                sprintf('Illegal orphanRemoval %s for %s', $mapping['field'], $this->name)
684
            );
685
        }
686
687
        // Complete id mapping
688 17
        if (isset($mapping['id']) && $mapping['id'] === true) {
689 View Code Duplication
            if (isset($mapping['orphanRemoval']) && $mapping['orphanRemoval'] == true) {
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...
690
                throw new MappingException(
691
                    sprintf('Illegal orphanRemoval on identifier association %s for %s', $mapping['field'], $this->name)
692
                );
693
            }
694
695
            if (!in_array($mapping['field'], $this->identifier, true)) {
696
                $this->identifier[]              = $mapping['field'];
697
                $this->containsForeignIdentifier = true;
698
            }
699
700
            // Check for composite key
701
            if (!$this->isIdentifierComposite && count($this->identifier) > 1) {
702
                $this->isIdentifierComposite = true;
703
            }
704
        }
705
706
        // Mandatory and optional attributes for either side
707 17
        if (null !== $mapping['mappedBy']) {
708 17
            $mapping['isOwningSide'] = false;
709 17
        }
710
711 17 View Code Duplication
        if (isset($mapping['id']) && $mapping['id'] === true && $mapping['type'] & self::TO_MANY) {
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...
712
            throw new MappingException(
713
                sprintf('Illegal toMany identifier association %s for %s', $mapping['field'], $this->name)
714
            );
715
        }
716
717
        // Fetch mode. Default fetch mode to LAZY, if not set.
718 17
        if (!isset($mapping['fetch'])) {
719 17
            $mapping['fetch'] = self::FETCH_LAZY;
720 17
        }
721
722
        // Cascades
723 17
        $cascades    = isset($mapping['cascade']) ? array_map('strtolower', $mapping['cascade']) : [];
724 17
        $allCascades = ['remove', 'persist', 'refresh', 'merge', 'detach'];
725 17
        if (in_array('all', $cascades, true)) {
726
            $cascades = $allCascades;
727 17
        } elseif (count($cascades) !== count(array_intersect($cascades, $allCascades))) {
728
            throw new MappingException('Invalid cascades: ' . implode(', ', $cascades));
729
        }
730 17
        $mapping['cascade']          = $cascades;
731 17
        $mapping['isCascadeRemove']  = in_array('remove', $cascades, true);
732 17
        $mapping['isCascadePersist'] = in_array('persist', $cascades, true);
733 17
        $mapping['isCascadeRefresh'] = in_array('refresh', $cascades, true);
734 17
        $mapping['isCascadeMerge']   = in_array('merge', $cascades, true);
735 17
        $mapping['isCascadeDetach']  = in_array('detach', $cascades, true);
736
737 17
        return $mapping;
738
    }
739
740 17
    private function storeMapping(array $mapping)
741
    {
742 17
        $this->assertFieldNotMapped($mapping['field']);
743
744 17
        $this->apiFieldNames[$mapping['field']]  = $mapping['api_field'];
745 17
        $this->fieldNames[$mapping['api_field']] = $mapping['field'];
746 17
        $this->associations[$mapping['field']]   = $mapping;
747 17
    }
748
749 27
    private function validateAndCompleteFieldMapping(array &$mapping)
750
    {
751 27
        if (!array_key_exists('api_field', $mapping)) {
752 26
            $mapping['api_field'] = $mapping['field']; //todo: invent naming strategy
753 26
        }
754
755 27
        if (!array_key_exists('options', $mapping)) {
756
            $mapping['options'] = [];
757
        }
758
759 27
        $this->apiFieldNames[$mapping['field']]  = $mapping['api_field'];
760 27
        $this->fieldNames[$mapping['api_field']] = $mapping['field'];
761
762
        //        if (isset($this->fieldNames[$mapping['columnName']]) || ($this->discriminatorField && $this->discriminatorField['name'] === $mapping['api_field'])) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
70% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
763
        //            throw MappingException::duplicateColumnName($this->name, $mapping['columnName']);
0 ignored issues
show
Unused Code Comprehensibility introduced by
67% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
764
        //        }
765
766
        // Complete id mapping
767 27
        if (isset($mapping['id']) && $mapping['id'] === true) {
768 27
            if (!in_array($mapping['field'], $this->identifier, true)) {
769 27
                $this->identifier[] = $mapping['field'];
770 27
            }
771
            // Check for composite key
772 27
            if (!$this->isIdentifierComposite && count($this->identifier) > 1) {
773 1
                $this->isIdentifierComposite = true;
774 1
            }
775 27
        }
776 27
    }
777
778
    /**
779
     * @param string $fieldName
780
     *
781
     * @throws MappingException
782
     */
783 27
    private function assertFieldNotMapped($fieldName)
784
    {
785 27
        if (array_key_exists($fieldName, $this->fields) ||
786 27
            array_key_exists($fieldName, $this->associations) ||
787 27
            array_key_exists($fieldName, $this->identifier)
788 27
        ) {
789
            throw new MappingException('Field already mapped');
790
        }
791 27
    }
792
793
    /**
794
     * @param array $mapping
795
     *
796
     * @return array
797
     * @throws MappingException
798
     * @throws \InvalidArgumentException
799
     */
800 17
    private function validateAndCompleteOneToManyMapping(array $mapping)
801
    {
802 17
        $mapping = $this->validateAndCompleteAssociationMapping($mapping);
803
804
        // OneToMany-side MUST be inverse (must have mappedBy)
805 17
        if (!isset($mapping['mappedBy'])) {
806
            throw new MappingException(
807
                sprintf('Many to many requires mapped by: %s', $mapping['field'])
808
            );
809
        }
810 17
        $mapping['orphanRemoval']   = isset($mapping['orphanRemoval']) && $mapping['orphanRemoval'];
811 17
        $mapping['isCascadeRemove'] = $mapping['orphanRemoval'] || $mapping['isCascadeRemove'];
812 17
        $this->assertMappingOrderBy($mapping);
813
814 17
        return $mapping;
815
    }
816
817
    /**
818
     * @param array $mapping
819
     *
820
     * @throws \InvalidArgumentException
821
     */
822 17
    private function assertMappingOrderBy(array $mapping)
823
    {
824 17
        if (array_key_exists('orderBy', $mapping) && !is_array($mapping['orderBy'])) {
825
            throw new \InvalidArgumentException(
826
                "'orderBy' is expected to be an array, not " . gettype($mapping['orderBy'])
827
            );
828
        }
829 17
    }
830
831
    /**
832
     * @param array $mapping
833
     *
834
     * @return array
835
     */
836 17
    private function validateAndCompleteOneToOneMapping(array $mapping)
837
    {
838 17
        $mapping = $this->validateAndCompleteAssociationMapping($mapping);
839
840 17
        $mapping['orphanRemoval']   = isset($mapping['orphanRemoval']) && $mapping['orphanRemoval'];
841 17
        $mapping['isCascadeRemove'] = $mapping['orphanRemoval'] || $mapping['isCascadeRemove'];
842 17
        if ($mapping['orphanRemoval']) {
843
            unset($mapping['unique']);
844
        }
845
846 17
        return $mapping;
847
    }
848
}
849