Completed
Pull Request — master (#14)
by Pavel
03:24
created

EntityMetadata::getIdentifierValues()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 21
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 5

Importance

Changes 0
Metric Value
dl 0
loc 21
c 0
b 0
f 0
ccs 15
cts 15
cp 1
rs 8.7624
cc 5
eloc 13
nc 5
nop 1
crap 5
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 $apiName;
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 InstantiatorInterface */
53
    private $instantiator;
54
    /** @var  int */
55
    private $changeTrackingPolicy = self::CHANGETRACKING_DEFERRED_IMPLICIT;
56
57
    /**
58
     * Initializes a new ClassMetadata instance that will hold the object-relational mapping
59
     * metadata of the class with the given name.
60
     *
61
     * @param string $entityName The name of the entity class the new instance is used for.
62
     */
63 18
    public function __construct($entityName)
64
    {
65 18
        $this->name           = $entityName;
66 18
        $this->rootEntityName = $entityName;
67 18
    }
68
69
    /**
70
     * @return boolean
71
     */
72
    public function containsForeignIdentifier()
73
    {
74
        return $this->containsForeignIdentifier;
75
    }
76
77
    /** {@inheritdoc} */
78 7
    public function getReflectionProperties()
79
    {
80 7
        return $this->reflFields;
81
    }
82
83
    /**
84
     * {@inheritdoc}
85
     */
86 16
    public function getReflectionProperty($name)
87
    {
88 16
        if (!array_key_exists($name, $this->reflFields)) {
89
            throw MappingException::noSuchProperty($name, $this->getName());
90
        }
91
92 16
        return $this->reflFields[$name];
93
    }
94
95
    /** {@inheritdoc} */
96 18
    public function getName()
97
    {
98 18
        return $this->name;
99
    }
100
101
    /** {@inheritdoc} */
102 1
    public function getMethodContainer()
103
    {
104 1
        return $this->methodProvider;
105
    }
106
107
    /** {@inheritdoc} */
108 12
    public function getRepositoryClass()
109
    {
110 12
        return $this->repositoryClass;
111
    }
112
113
    /** {@inheritdoc} */
114 2
    public function getIdentifier()
115
    {
116 2
        return $this->identifier;
117
    }
118
119 5
    public function setIdentifier($identifier)
120
    {
121 5
        $this->identifier            = $identifier;
122 5
        $this->isIdentifierComposite = (count($this->identifier) > 1);
123 5
    }
124
125
    /** {@inheritdoc} */
126 12
    public function getReflectionClass()
127
    {
128 12
        if (null === $this->reflClass) {
129
            $this->reflClass = new \ReflectionClass($this->getName());
130
        }
131
132 12
        return $this->reflClass;
133
    }
134
135
    /** {@inheritdoc} */
136 4
    public function isIdentifier($fieldName)
137
    {
138 4
        return in_array($fieldName, $this->identifier, true);
139
    }
140
141
    /** {@inheritdoc} */
142 2
    public function hasField($fieldName)
143
    {
144 2
        return in_array($fieldName, $this->getFieldNames(), true);
145
    }
146
147
    /** {@inheritdoc} */
148 12
    public function getFieldNames()
149
    {
150 12
        return array_keys($this->fields);
151
    }
152
153
    /** {@inheritdoc} */
154 17
    public function hasAssociation($fieldName)
155
    {
156 17
        return in_array($fieldName, $this->getAssociationNames(), true);
157
    }
158
159
    /** {@inheritdoc} */
160 17
    public function getAssociationNames()
161
    {
162 17
        return array_keys($this->associations);
163
    }
164
165
    /** {@inheritdoc} */
166 9
    public function isSingleValuedAssociation($fieldName)
167
    {
168 9
        return $this->hasAssociation($fieldName) && $this->associations[$fieldName]['type'] & self::TO_ONE;
169
    }
170
171
    /** {@inheritdoc} */
172 13
    public function isCollectionValuedAssociation($fieldName)
173
    {
174 13
        return $this->hasAssociation($fieldName) && $this->associations[$fieldName]['type'] & self::TO_MANY;
175
    }
176
177
    /** {@inheritdoc} */
178 16
    public function getIdentifierFieldNames()
179
    {
180 16
        return $this->identifier;
181
    }
182
183
    /** {@inheritdoc} */
184 17
    public function getTypeOfField($fieldName)
185
    {
186 17
        return $this->fields[$fieldName]['type'];
187
    }
188
189
    /** {@inheritdoc} */
190 9
    public function getAssociationTargetClass($assocName)
191
    {
192 9
        return $this->associations[$assocName]['target'];
193
    }
194
195
    /** {@inheritdoc} */
196
    public function isAssociationInverseSide($assocName)
197
    {
198
        $assoc = $this->associations[$assocName];
199
200
        return array_key_exists('mappedBy', $assoc);
201
    }
202
203
    /** {@inheritdoc} */
204
    public function getAssociationMappedByTargetField($assocName)
205
    {
206
        return $this->associations[$assocName]['mappedBy'];
207
    }
208
209
    /** {@inheritdoc} */
210 15
    public function getIdentifierValues($object)
211
    {
212 15
        if ($this->isIdentifierComposite) {
213 1
            $id = [];
214 1
            foreach ($this->identifier as $idField) {
215 1
                $value = $this->reflFields[$idField]->getValue($object);
216 1
                if ($value !== null) {
217 1
                    $id[$idField] = $value;
218 1
                }
219 1
            }
220
221 1
            return $id;
222
        }
223 14
        $id    = $this->identifier[0];
224 14
        $value = $this->reflFields[$id]->getValue($object);
225 14
        if (null === $value) {
226 2
            return [];
227
        }
228
229 14
        return [$id => $value];
230
    }
231
232
    /** {@inheritdoc} */
233 18
    public function wakeupReflection(ReflectionService $reflService)
234
    {
235
        // Restore ReflectionClass and properties
236 18
        $this->reflClass    = $reflService->getClass($this->name);
237 18
        $this->instantiator = $this->instantiator ?: new Instantiator();
238
239 18 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...
240 18
            $class                    = array_key_exists('declared', $mapping) ? $mapping['declared'] : $this->name;
241 18
            $this->reflFields[$field] = $reflService->getAccessibleProperty($class, $field);
242 18
        }
243
244 18 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...
245 14
            $class                    = array_key_exists('declared', $mapping) ? $mapping['declared'] : $this->name;
246 14
            $this->reflFields[$field] = $reflService->getAccessibleProperty($class, $field);
247 18
        }
248 18
    }
249
250
    /** {@inheritdoc} */
251 18
    public function initializeReflection(ReflectionService $reflService)
252
    {
253 18
        $this->reflClass = $reflService->getClass($this->name);
254 18
        $this->namespace = $reflService->getClassNamespace($this->name);
255 18
        if ($this->reflClass) {
256 18
            $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...
257 18
        }
258 18
    }
259
260
    /** {@inheritdoc} */
261 17
    public function getApiName()
262
    {
263 17
        if (null === $this->apiName) {
264
            throw MappingException::noApiSpecified($this->getName());
265
        }
266
267 17
        return $this->apiName;
268
    }
269
270
    /** {@inheritdoc} */
271 18
    public function getClientName()
272
    {
273 18
        if (null === $this->clientName) {
274
            throw MappingException::noClientSpecified($this->getName());
275
        }
276
277 18
        return $this->clientName;
278
    }
279
280 18
    public function mapField(array $mapping)
281
    {
282 18
        $this->validateAndCompleteFieldMapping($mapping);
283 18
        $this->assertFieldNotMapped($mapping['field']);
284 18
        $this->fields[$mapping['field']] = $mapping;
285 18
    }
286
287
    /** {@inheritdoc} */
288 4
    public function getFieldMapping($fieldName)
289
    {
290 4
        if (!isset($this->fields[$fieldName])) {
291
            throw MappingException::unknownField($fieldName, $this->getName());
292
        }
293
294 4
        return $this->fields[$fieldName];
295
    }
296
297
    /** {@inheritdoc} */
298 13
    public function getAssociationMapping($fieldName)
299
    {
300 13
        if (!isset($this->associations[$fieldName])) {
301
            throw MappingException::unknownAssociation($fieldName, $this->getName());
302
        }
303
304 13
        return $this->associations[$fieldName];
305
    }
306
307 2
    public function setCustomRepositoryClass($customRepositoryClassName)
308
    {
309 2
        $this->repositoryClass = $customRepositoryClassName;
310 2
    }
311
312
    /**
313
     * @internal
314
     *
315
     * @param array $mapping
316
     *
317
     * @return void
318
     */
319 5
    public function addInheritedFieldMapping(array $mapping)
320
    {
321 5
        $this->fields[$mapping['field']]         = $mapping;
322 5
        $this->apiFieldNames[$mapping['field']]  = $mapping['api_field'];
323 5
        $this->fieldNames[$mapping['api_field']] = $mapping['field'];
324 5
    }
325
326
    /** {@inheritdoc} */
327
    public function getFieldName($apiFieldName)
328
    {
329
        return $this->fieldNames[$apiFieldName];
330
    }
331
332
    /** {@inheritdoc} */
333 17
    public function getApiFieldName($fieldName)
334
    {
335 17
        return $this->apiFieldNames[$fieldName];
336
    }
337
338
    public function hasApiField($apiFieldName)
339
    {
340
        return array_key_exists($apiFieldName, $this->fieldNames);
341
    }
342
343 14
    public function mapOneToMany(array $mapping)
344
    {
345 14
        $mapping = $this->validateAndCompleteOneToManyMapping($mapping);
346
347 14
        $this->storeMapping($mapping);
348 14
    }
349
350 14
    public function mapManyToOne(array $mapping)
351
    {
352 14
        $mapping = $this->validateAndCompleteOneToOneMapping($mapping);
353
354 14
        $this->storeMapping($mapping);
355 14
    }
356
357
    public function mapOneToOne(array $mapping)
358
    {
359
        $mapping = $this->validateAndCompleteOneToOneMapping($mapping);
360
361
        $this->storeMapping($mapping);
362
    }
363
364
    /** {@inheritdoc} */
365 11
    public function newInstance()
366
    {
367 11
        return $this->instantiator->instantiate($this->name);
368
    }
369
370 4
    public function isIdentifierComposite()
371
    {
372 4
        return $this->isIdentifierComposite;
373
    }
374
375
    /** {@inheritdoc} */
376
    public function getRootEntityName()
377
    {
378
        return $this->rootEntityName;
379
    }
380
381
    /**
382
     * Populates the entity identifier of an entity.
383
     *
384
     * @param object $entity
385
     * @param array  $id
386
     *
387
     * @return void
388
     */
389
    public function assignIdentifier($entity, array $id)
390
    {
391
        foreach ($id as $idField => $idValue) {
392
            $this->reflFields[$idField]->setValue($entity, $idValue);
393
        }
394
    }
395
396 5
    public function addInheritedAssociationMapping(array $mapping)
397
    {
398 5
        $this->associations[$mapping['field']]   = $mapping;
399 5
        $this->apiFieldNames[$mapping['field']]  = $mapping['api_field'];
400 5
        $this->fieldNames[$mapping['api_field']] = $mapping['field'];
401 5
    }
402
403
    /** {@inheritdoc} */
404 4
    public function getSubclasses()
405
    {
406
        //fixme
407 4
        return [];
408
    }
409
410
    /** {@inheritdoc} */
411 4
    public function getAssociationMappings()
412
    {
413 4
        return $this->associations;
414
    }
415
416
    /**
417
     * Validates & completes the basic mapping information that is common to all
418
     * association mappings (one-to-one, many-ot-one, one-to-many, many-to-many).
419
     *
420
     * @param array $mapping The mapping.
421
     *
422
     * @return array The updated mapping.
423
     *
424
     * @throws MappingException If something is wrong with the mapping.
425
     */
426 14
    protected function validateAndCompleteAssociationMapping(array $mapping)
427
    {
428 14
        if (!array_key_exists('api_field', $mapping)) {
429 14
            $mapping['api_field'] = $mapping['field'];
430 14
        }
431
432 14
        if (!isset($mapping['mappedBy'])) {
433 14
            $mapping['mappedBy'] = null;
434 14
        }
435
436 14
        if (!isset($mapping['inversedBy'])) {
437 14
            $mapping['inversedBy'] = null;
438 14
        }
439
440 14
        $mapping['isOwningSide'] = true; // assume owning side until we hit mappedBy
441
442
        // unset optional indexBy attribute if its empty
443 14
        if (!isset($mapping['indexBy']) || !$mapping['indexBy']) {
444 14
            unset($mapping['indexBy']);
445 14
        }
446
447
        // If targetEntity is unqualified, assume it is in the same namespace as
448
        // the sourceEntity.
449 14
        $mapping['source'] = $this->name;
450 14
        if (isset($mapping['target'])) {
451 14
            $mapping['target'] = ltrim($mapping['target'], '\\');
452 14
        }
453
454 14
        if (($mapping['type'] & self::MANY_TO_ONE) > 0 &&
455 14
            isset($mapping['orphanRemoval']) &&
456
            $mapping['orphanRemoval'] == true
457 14
        ) {
458
            throw new MappingException(
459
                sprintf('Illegal orphanRemoval %s for %s', $mapping['field'], $this->name)
460
            );
461
        }
462
463
        // Complete id mapping
464 14
        if (isset($mapping['id']) && $mapping['id'] === true) {
465 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...
466
                throw new MappingException(
467
                    sprintf('Illegal orphanRemoval on identifier association %s for %s', $mapping['field'], $this->name)
468
                );
469
            }
470
471
            if (!in_array($mapping['field'], $this->identifier, true)) {
472
                $this->identifier[]              = $mapping['field'];
473
                $this->containsForeignIdentifier = true;
474
            }
475
476
            // Check for composite key
477
            if (!$this->isIdentifierComposite && count($this->identifier) > 1) {
478
                $this->isIdentifierComposite = true;
479
            }
480
        }
481
482
        // Mandatory and optional attributes for either side
483 14
        if (null !== $mapping['mappedBy']) {
484 14
            $mapping['isOwningSide'] = false;
485 14
        }
486
487 14 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...
488
            throw new MappingException(
489
                sprintf('Illegal toMany identifier association %s for %s', $mapping['field'], $this->name)
490
            );
491
        }
492
493
        // Fetch mode. Default fetch mode to LAZY, if not set.
494 14
        if ( ! isset($mapping['fetch'])) {
495 14
            $mapping['fetch'] = self::FETCH_LAZY;
496 14
        }
497
498
        // Cascades
499 14
        $cascades    = isset($mapping['cascade']) ? array_map('strtolower', $mapping['cascade']) : [];
500 14
        $allCascades = ['remove', 'persist', 'refresh', 'merge', 'detach'];
501 14
        if (in_array('all', $cascades, true)) {
502
            $cascades = $allCascades;
503 14
        } elseif (count($cascades) !== count(array_intersect($cascades, $allCascades))) {
504
            throw new MappingException('Invalid cascades: ' . implode(', ', $cascades));
505
        }
506 14
        $mapping['cascade']          = $cascades;
507 14
        $mapping['isCascadeRemove']  = in_array('remove', $cascades, true);
508 14
        $mapping['isCascadePersist'] = in_array('persist', $cascades, true);
509 14
        $mapping['isCascadeRefresh'] = in_array('refresh', $cascades, true);
510 14
        $mapping['isCascadeMerge']   = in_array('merge', $cascades, true);
511 14
        $mapping['isCascadeDetach']  = in_array('detach', $cascades, true);
512
513 14
        return $mapping;
514
    }
515
516 14
    private function storeMapping(array $mapping)
517
    {
518 14
        $this->assertFieldNotMapped($mapping['field']);
519
520 14
        $this->apiFieldNames[$mapping['field']]  = $mapping['api_field'];
521 14
        $this->fieldNames[$mapping['api_field']] = $mapping['field'];
522 14
        $this->associations[$mapping['field']]   = $mapping;
523 14
    }
524
525 18
    private function validateAndCompleteFieldMapping(array &$mapping)
526
    {
527 18
        if (!array_key_exists('api_field', $mapping)) {
528 18
            $mapping['api_field'] = $mapping['field']; //todo: invent naming strategy
529 18
        }
530
531 18
        $this->apiFieldNames[$mapping['field']]  = $mapping['api_field'];
532 18
        $this->fieldNames[$mapping['api_field']] = $mapping['field'];
533
534
        // Complete id mapping
535 18
        if (isset($mapping['id']) && $mapping['id'] === true) {
536 18
            if (!in_array($mapping['field'], $this->identifier, true)) {
537 18
                $this->identifier[] = $mapping['field'];
538 18
            }
539
            // Check for composite key
540 18
            if (!$this->isIdentifierComposite && count($this->identifier) > 1) {
541 1
                $this->isIdentifierComposite = true;
542 1
            }
543 18
        }
544 18
    }
545
546
    /**
547
     * @param string $fieldName
548
     *
549
     * @throws MappingException
550
     */
551 18
    private function assertFieldNotMapped($fieldName)
552
    {
553 18
        if (array_key_exists($fieldName, $this->fields) ||
554 18
            array_key_exists($fieldName, $this->associations) ||
555 18
            array_key_exists($fieldName, $this->identifier)
556 18
        ) {
557
            throw new MappingException('Field already mapped');
558
        }
559 18
    }
560
561
    /**
562
     * @param array $mapping
563
     *
564
     * @return array
565
     * @throws MappingException
566
     * @throws \InvalidArgumentException
567
     */
568 14
    private function validateAndCompleteOneToManyMapping(array $mapping)
569
    {
570 14
        $mapping = $this->validateAndCompleteAssociationMapping($mapping);
571
572
        // OneToMany-side MUST be inverse (must have mappedBy)
573 14
        if (!isset($mapping['mappedBy'])) {
574
            throw new MappingException(
575
                sprintf('Many to many requires mapped by: %s', $mapping['field'])
576
            );
577
        }
578 14
        $mapping['orphanRemoval']   = isset($mapping['orphanRemoval']) && $mapping['orphanRemoval'];
579 14
        $mapping['isCascadeRemove'] = $mapping['orphanRemoval'] || $mapping['isCascadeRemove'];
580 14
        $this->assertMappingOrderBy($mapping);
581
582 14
        return $mapping;
583
    }
584
585
    /**
586
     * @param array $mapping
587
     *
588
     * @throws \InvalidArgumentException
589
     */
590 14
    private function assertMappingOrderBy(array $mapping)
591
    {
592 14
        if (array_key_exists('orderBy', $mapping) && !is_array($mapping['orderBy'])) {
593
            throw new \InvalidArgumentException(
594
                "'orderBy' is expected to be an array, not " . gettype($mapping['orderBy'])
595
            );
596
        }
597 14
    }
598
599
    /**
600
     * @param array $mapping
601
     *
602
     * @return array
603
     */
604 14
    private function validateAndCompleteOneToOneMapping(array $mapping)
605
    {
606 14
        $mapping = $this->validateAndCompleteAssociationMapping($mapping);
607
608 14
        $mapping['orphanRemoval']   = isset($mapping['orphanRemoval']) && $mapping['orphanRemoval'];
609 14
        $mapping['isCascadeRemove'] = $mapping['orphanRemoval'] || $mapping['isCascadeRemove'];
610 14
        if ($mapping['orphanRemoval']) {
611
            unset($mapping['unique']);
612
        }
613
614 14
        return $mapping;
615
    }
616
617 2
    public function isReadOnly()
618
    {
619 2
        return false;
620
    }
621
622
    /**
623
     * Sets the change tracking policy used by this class.
624
     *
625
     * @param integer $policy
626
     *
627
     * @return void
628
     */
629
    public function setChangeTrackingPolicy($policy)
630
    {
631
        $this->changeTrackingPolicy = $policy;
632
    }
633
    /**
634
     * Whether the change tracking policy of this class is "deferred explicit".
635
     *
636
     * @return boolean
637
     */
638
    public function isChangeTrackingDeferredExplicit()
639
    {
640
        return $this->changeTrackingPolicy == self::CHANGETRACKING_DEFERRED_EXPLICIT;
641
    }
642
    /**
643
     * Whether the change tracking policy of this class is "deferred implicit".
644
     *
645
     * @return boolean
646
     */
647 2
    public function isChangeTrackingDeferredImplicit()
648
    {
649 2
        return $this->changeTrackingPolicy == self::CHANGETRACKING_DEFERRED_IMPLICIT;
650
    }
651
    /**
652
     * Whether the change tracking policy of this class is "notify".
653
     *
654
     * @return boolean
655
     */
656
    public function isChangeTrackingNotify()
657
    {
658
        return $this->changeTrackingPolicy == self::CHANGETRACKING_NOTIFY;
659
    }
660
661
}
662