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

EntityMetadata::storeMapping()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 8
c 0
b 0
f 0
ccs 6
cts 6
cp 1
rs 9.4285
cc 1
eloc 5
nc 1
nop 1
crap 1
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 28
    public function __construct($entityName)
74
    {
75 28
        $this->name           = $entityName;
76 28
        $this->rootEntityName = $entityName;
77 28
    }
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 25
    public function getReflectionProperty($name)
97
    {
98 25
        if (!array_key_exists($name, $this->reflFields)) {
99
            throw MappingException::noSuchProperty($name, $this->getName());
100
        }
101
102 25
        return $this->reflFields[$name];
103
    }
104
105
    /** {@inheritdoc} */
106 28
    public function getName()
107
    {
108 28
        return $this->name;
109
    }
110
111
    /** {@inheritdoc} */
112 27
    public function getMethodContainer()
113
    {
114 27
        return $this->methodProvider;
115
    }
116
117
    /** {@inheritdoc} */
118 18
    public function getRepositoryClass()
119
    {
120 18
        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 18
    public function getReflectionClass()
137
    {
138 18
        if (null === $this->reflClass) {
139
            $this->reflClass = new \ReflectionClass($this->getName());
140
        }
141
142 18
        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 18
    public function getFieldNames()
159
    {
160 18
        return array_keys($this->fields);
161
    }
162
163
    /** {@inheritdoc} */
164 26
    public function hasAssociation($fieldName)
165
    {
166 26
        return in_array($fieldName, $this->getAssociationNames(), true);
167
    }
168
169
    /** {@inheritdoc} */
170 26
    public function getAssociationNames()
171
    {
172 26
        return array_keys($this->associations);
173
    }
174
175
    /** {@inheritdoc} */
176 12
    public function isSingleValuedAssociation($fieldName)
177
    {
178 12
        return $this->hasAssociation($fieldName) && $this->associations[$fieldName]['type'] & self::TO_ONE;
179
    }
180
181
    /** {@inheritdoc} */
182 16
    public function isCollectionValuedAssociation($fieldName)
183
    {
184 16
        return $this->hasAssociation($fieldName) && $this->associations[$fieldName]['type'] & self::TO_MANY;
185
    }
186
187
    /** {@inheritdoc} */
188 21
    public function getIdentifierFieldNames()
189
    {
190 21
        return $this->identifier;
191
    }
192
193
    /** {@inheritdoc} */
194 26
    public function getTypeOfField($fieldName)
195
    {
196 26
        return $this->fields[$fieldName]['type'];
197
    }
198
199
    /** {@inheritdoc} */
200 12
    public function getAssociationTargetClass($assocName)
201
    {
202 12
        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 22
    public function getIdentifierValues($object)
221
    {
222 22
        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 21
        $id    = $this->identifier[0];
234 21
        $value = $this->reflFields[$id]->getValue($object);
235 21
        if (null === $value) {
236
            return [];
237
        }
238
239 21
        return [$id => $value];
240
    }
241
242
    /** {@inheritdoc} */
243 28
    public function wakeupReflection(ReflectionService $reflService)
244
    {
245
        // Restore ReflectionClass and properties
246 28
        $this->reflClass    = $reflService->getClass($this->name);
247 28
        $this->instantiator = $this->instantiator ?: new Instantiator();
248
249 28 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 28
            $class                    = array_key_exists('declared', $mapping) ? $mapping['declared'] : $this->name;
251 28
            $this->reflFields[$field] = $reflService->getAccessibleProperty($class, $field);
252 28
        }
253
254 28 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 18
            $class                    = array_key_exists('declared', $mapping) ? $mapping['declared'] : $this->name;
256 18
            $this->reflFields[$field] = $reflService->getAccessibleProperty($class, $field);
257 28
        }
258 28
    }
259
260
    /** {@inheritdoc} */
261 28
    public function initializeReflection(ReflectionService $reflService)
262
    {
263 28
        $this->reflClass = $reflService->getClass($this->name);
264 28
        $this->namespace = $reflService->getClassNamespace($this->name);
265 28
        if ($this->reflClass) {
266 28
            $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 28
        }
268 28
    }
269
270
    /** {@inheritdoc} */
271 26
    public function getApiFactory()
272
    {
273 26
        if (null === $this->apiFactory) {
274
            throw MappingException::noApiSpecified($this->getName());
275
        }
276
277 26
        return $this->apiFactory;
278
    }
279
280
    /** {@inheritdoc} */
281 27
    public function getClientName()
282
    {
283 27
        if (null === $this->clientName) {
284
            throw MappingException::noClientSpecified($this->getName());
285
        }
286
287 27
        return $this->clientName;
288
    }
289
290 28
    public function mapField(array $mapping)
291
    {
292 28
        $this->validateAndCompleteFieldMapping($mapping);
293 28
        $this->assertFieldNotMapped($mapping['field']);
294 28
        $this->fields[$mapping['field']] = $mapping;
295 28
    }
296
297
    /** {@inheritdoc} */
298 26
    public function getFieldMapping($fieldName)
299
    {
300 26
        if (!isset($this->fields[$fieldName])) {
301
            throw MappingException::unknownField($fieldName, $this->getName());
302
        }
303
304 26
        return $this->fields[$fieldName];
305
    }
306
307
    /** {@inheritdoc} */
308 26
    public function getFieldOptions($fieldName)
309
    {
310 26
        return $this->getFieldMapping($fieldName)['options'];
311
    }
312
313
    /** {@inheritdoc} */
314 17
    public function getAssociationMapping($fieldName)
315
    {
316 17
        if (!isset($this->associations[$fieldName])) {
317
            throw MappingException::unknownAssociation($fieldName, $this->getName());
318
        }
319
320 17
        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 26
    public function getApiFieldName($fieldName)
350
    {
351 26
        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 17
    public function newInstance()
382
    {
383 17
        return $this->instantiator->instantiate($this->name);
384
    }
385
386 8
    public function isIdentifierComposite()
387
    {
388 8
        return $this->isIdentifierComposite;
389
    }
390
391
    /** {@inheritdoc} */
392 28
    public function getRootEntityName()
393
    {
394 28
        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 28
    public function mapIdentifier(array $mapping)
479
    {
480 28
        $this->setIdGeneratorType($mapping['generator']['strategy']);
481
482 28
        $this->mapField($mapping);
483 28
    }
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 28
    public function setIdGeneratorType($generatorType)
493
    {
494 28
        $this->generatorType = $generatorType;
495 28
    }
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 28
    public function isRootEntity()
525
    {
526 28
        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 28
    public function addDiscriminatorMapClass($name, $className)
540
    {
541 28
        $className                     = $this->fullyQualifiedClassName($className);
542 28
        $className                     = ltrim($className, '\\');
543 28
        $this->discriminatorMap[$name] = $className;
544 28
        if ($this->name === $className) {
545 28
            $this->discriminatorValue = $name;
546
547 28
            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 28
    public function fullyQualifiedClassName($className)
563
    {
564 28
        if (empty($className)) {
565
            return $className;
566
        }
567 28
        if ($className !== null && strpos($className, '\\') === false && $this->namespace) {
568
            return $this->namespace . '\\' . $className;
569
        }
570
571 28
        return $className;
572
    }
573
574
    /** {@inheritdoc} */
575 26
    public function getDiscriminatorField()
576
    {
577 26
        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 18
    public function getDiscriminatorMap()
615
    {
616 18
        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 28
    public function setDiscriminatorMap(array $map)
628
    {
629 28
        foreach ($map as $value => $className) {
630 28
            $this->addDiscriminatorMapClass($value, $className);
631 28
        }
632 28
    }
633
634
    /** {@inheritdoc} */
635 19
    public function getDiscriminatorValue()
636
    {
637 19
        return $this->discriminatorValue;
638
    }
639
640 1
    public function mapManyToMany($mapping)
641
    {
642 1
        $mapping = $this->validateAndCompleteManyToManyMapping($mapping);
643
644 1
        $this->storeMapping($mapping);
645 1
    }
646
647
    /**
648
     * Validates & completes the basic mapping information that is common to all
649
     * association mappings (one-to-one, many-ot-one, one-to-many, many-to-many).
650
     *
651
     * @param array $mapping The mapping.
652
     *
653
     * @return array The updated mapping.
654
     *
655
     * @throws MappingException If something is wrong with the mapping.
656
     */
657 18
    protected function validateAndCompleteAssociationMapping(array $mapping)
658
    {
659 18
        if (!array_key_exists('api_field', $mapping)) {
660 17
            $mapping['api_field'] = $mapping['field'];
661 17
        }
662
663 18
        if (!isset($mapping['mappedBy'])) {
664 18
            $mapping['mappedBy'] = null;
665 18
        }
666
667 18
        if (!isset($mapping['inversedBy'])) {
668 18
            $mapping['inversedBy'] = null;
669 18
        }
670
671 18
        if (!isset($mapping['orderBy'])) {
672 18
            $mapping['orderBy'] = [];
673 18
        }
674
675 18
        $mapping['isOwningSide'] = true; // assume owning side until we hit mappedBy
676
677
        // unset optional indexBy attribute if its empty
678 18
        if (!isset($mapping['indexBy']) || !$mapping['indexBy']) {
679 18
            unset($mapping['indexBy']);
680 18
        }
681
682
        // If targetEntity is unqualified, assume it is in the same namespace as
683
        // the sourceEntity.
684 18
        $mapping['source'] = $this->name;
685 18
        if (isset($mapping['target'])) {
686 18
            $mapping['target'] = ltrim($mapping['target'], '\\');
687 18
        }
688
689 18
        if (($mapping['type'] & self::MANY_TO_ONE) > 0 &&
690 18
            isset($mapping['orphanRemoval']) &&
691
            $mapping['orphanRemoval'] == true
692 18
        ) {
693
            throw new MappingException(
694
                sprintf('Illegal orphanRemoval %s for %s', $mapping['field'], $this->name)
695
            );
696
        }
697
698
        // Complete id mapping
699 18
        if (isset($mapping['id']) && $mapping['id'] === true) {
700 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...
701
                throw new MappingException(
702
                    sprintf('Illegal orphanRemoval on identifier association %s for %s', $mapping['field'], $this->name)
703
                );
704
            }
705
706
            if (!in_array($mapping['field'], $this->identifier, true)) {
707
                $this->identifier[]              = $mapping['field'];
708
                $this->containsForeignIdentifier = true;
709
            }
710
711
            // Check for composite key
712
            if (!$this->isIdentifierComposite && count($this->identifier) > 1) {
713
                $this->isIdentifierComposite = true;
714
            }
715
        }
716
717
        // Mandatory and optional attributes for either side
718 18
        if (null !== $mapping['mappedBy']) {
719 17
            $mapping['isOwningSide'] = false;
720 17
        }
721
722 18 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...
723
            throw new MappingException(
724
                sprintf('Illegal toMany identifier association %s for %s', $mapping['field'], $this->name)
725
            );
726
        }
727
728
        // Fetch mode. Default fetch mode to LAZY, if not set.
729 18
        if (!isset($mapping['fetch'])) {
730 18
            $mapping['fetch'] = self::FETCH_LAZY;
731 18
        }
732
733
        // Cascades
734 18
        $cascades    = isset($mapping['cascade']) ? array_map('strtolower', $mapping['cascade']) : [];
735 18
        $allCascades = ['remove', 'persist', 'refresh', 'merge', 'detach'];
736 18
        if (in_array('all', $cascades, true)) {
737
            $cascades = $allCascades;
738 18
        } elseif (count($cascades) !== count(array_intersect($cascades, $allCascades))) {
739
            throw new MappingException('Invalid cascades: ' . implode(', ', $cascades));
740
        }
741 18
        $mapping['cascade']          = $cascades;
742 18
        $mapping['isCascadeRemove']  = in_array('remove', $cascades, true);
743 18
        $mapping['isCascadePersist'] = in_array('persist', $cascades, true);
744 18
        $mapping['isCascadeRefresh'] = in_array('refresh', $cascades, true);
745 18
        $mapping['isCascadeMerge']   = in_array('merge', $cascades, true);
746 18
        $mapping['isCascadeDetach']  = in_array('detach', $cascades, true);
747
748 18
        return $mapping;
749
    }
750
751 18
    private function storeMapping(array $mapping)
752
    {
753 18
        $this->assertFieldNotMapped($mapping['field']);
754
755 18
        $this->apiFieldNames[$mapping['field']]  = $mapping['api_field'];
756 18
        $this->fieldNames[$mapping['api_field']] = $mapping['field'];
757 18
        $this->associations[$mapping['field']]   = $mapping;
758 18
    }
759
760 28
    private function validateAndCompleteFieldMapping(array &$mapping)
761
    {
762 28
        if (!array_key_exists('api_field', $mapping)) {
763 27
            $mapping['api_field'] = $mapping['field']; //todo: invent naming strategy
764 27
        }
765
766 28
        if (!array_key_exists('options', $mapping)) {
767
            $mapping['options'] = [];
768
        }
769
770 28
        $this->apiFieldNames[$mapping['field']]  = $mapping['api_field'];
771 28
        $this->fieldNames[$mapping['api_field']] = $mapping['field'];
772
773
        //        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...
774
        //            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...
775
        //        }
776
777
        // Complete id mapping
778 28
        if (isset($mapping['id']) && $mapping['id'] === true) {
779 28
            if (!in_array($mapping['field'], $this->identifier, true)) {
780 28
                $this->identifier[] = $mapping['field'];
781 28
            }
782
            // Check for composite key
783 28
            if (!$this->isIdentifierComposite && count($this->identifier) > 1) {
784 1
                $this->isIdentifierComposite = true;
785 1
            }
786 28
        }
787 28
    }
788
789
    /**
790
     * @param string $fieldName
791
     *
792
     * @throws MappingException
793
     */
794 28
    private function assertFieldNotMapped($fieldName)
795
    {
796 28
        if (array_key_exists($fieldName, $this->fields) ||
797 28
            array_key_exists($fieldName, $this->associations) ||
798 28
            array_key_exists($fieldName, $this->identifier)
799 28
        ) {
800
            throw new MappingException('Field already mapped');
801
        }
802 28
    }
803
804
    /**
805
     * @param array $mapping
806
     *
807
     * @return array
808
     * @throws MappingException
809
     * @throws \InvalidArgumentException
810
     */
811 17
    private function validateAndCompleteOneToManyMapping(array $mapping)
812
    {
813 17
        $mapping = $this->validateAndCompleteAssociationMapping($mapping);
814
815
        // OneToMany-side MUST be inverse (must have mappedBy)
816 17
        if (!isset($mapping['mappedBy'])) {
817
            throw new MappingException(
818
                sprintf('Many to many requires mapped by: %s', $mapping['field'])
819
            );
820
        }
821 17
        $mapping['orphanRemoval']   = isset($mapping['orphanRemoval']) && $mapping['orphanRemoval'];
822 17
        $mapping['isCascadeRemove'] = $mapping['orphanRemoval'] || $mapping['isCascadeRemove'];
823 17
        $this->assertMappingOrderBy($mapping);
824
825 17
        return $mapping;
826
    }
827
828
    /**
829
     * @param array $mapping
830
     *
831
     * @return array
832
     * @throws MappingException
833
     * @throws \InvalidArgumentException
834
     */
835 1
    private function validateAndCompleteManyToManyMapping(array $mapping)
836
    {
837 1
        $mapping = $this->validateAndCompleteAssociationMapping($mapping);
838
839 1
        $this->assertMappingOrderBy($mapping);
840
841 1
        return $mapping;
842
    }
843
844
    /**
845
     * @param array $mapping
846
     *
847
     * @throws \InvalidArgumentException
848
     */
849 18
    private function assertMappingOrderBy(array $mapping)
850
    {
851 18
        if (array_key_exists('orderBy', $mapping) && !is_array($mapping['orderBy'])) {
852
            throw new \InvalidArgumentException(
853
                "'orderBy' is expected to be an array, not " . gettype($mapping['orderBy'])
854
            );
855
        }
856 18
    }
857
858
    /**
859
     * @param array $mapping
860
     *
861
     * @return array
862
     */
863 17
    private function validateAndCompleteOneToOneMapping(array $mapping)
864
    {
865 17
        $mapping = $this->validateAndCompleteAssociationMapping($mapping);
866
867 17
        $mapping['orphanRemoval']   = isset($mapping['orphanRemoval']) && $mapping['orphanRemoval'];
868 17
        $mapping['isCascadeRemove'] = $mapping['orphanRemoval'] || $mapping['isCascadeRemove'];
869 17
        if ($mapping['orphanRemoval']) {
870
            unset($mapping['unique']);
871
        }
872
873 17
        return $mapping;
874
    }
875
}
876