Completed
Pull Request — master (#14)
by Pavel
29:07 queued 23:49
created

Mapping/EntityMetadata.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
    public function __construct($entityName)
64
    {
65
        $this->name           = $entityName;
66
        $this->rootEntityName = $entityName;
67
    }
68
69
    /**
70
     * @return boolean
71
     */
72
    public function containsForeignIdentifier()
73
    {
74
        return $this->containsForeignIdentifier;
75
    }
76
77
    /** {@inheritdoc} */
78
    public function getReflectionProperties()
79
    {
80
        return $this->reflFields;
81
    }
82
83
    /**
84
     * {@inheritdoc}
85
     */
86
    public function getReflectionProperty($name)
87
    {
88
        if (!array_key_exists($name, $this->reflFields)) {
89
            throw MappingException::noSuchProperty($name, $this->getName());
90
        }
91
92
        return $this->reflFields[$name];
93
    }
94
95
    /** {@inheritdoc} */
96
    public function getName()
97
    {
98
        return $this->name;
99
    }
100
101
    /** {@inheritdoc} */
102
    public function getMethodContainer()
103
    {
104
        return $this->methodProvider;
105
    }
106
107
    /** {@inheritdoc} */
108
    public function getRepositoryClass()
109
    {
110
        return $this->repositoryClass;
111
    }
112
113
    /** {@inheritdoc} */
114
    public function getIdentifier()
115
    {
116
        return $this->identifier;
117
    }
118
119
    public function setIdentifier($identifier)
120
    {
121
        $this->identifier            = $identifier;
122
        $this->isIdentifierComposite = (count($this->identifier) > 1);
123
    }
124
125
    /** {@inheritdoc} */
126
    public function getReflectionClass()
127
    {
128
        if (null === $this->reflClass) {
129
            $this->reflClass = new \ReflectionClass($this->getName());
130
        }
131
132
        return $this->reflClass;
133
    }
134
135
    /** {@inheritdoc} */
136
    public function isIdentifier($fieldName)
137
    {
138
        return in_array($fieldName, $this->identifier, true);
139
    }
140
141
    /** {@inheritdoc} */
142
    public function hasField($fieldName)
143
    {
144
        return in_array($fieldName, $this->getFieldNames(), true);
145
    }
146
147
    /** {@inheritdoc} */
148
    public function getFieldNames()
149
    {
150
        return array_keys($this->fields);
151
    }
152
153
    /** {@inheritdoc} */
154
    public function hasAssociation($fieldName)
155
    {
156
        return in_array($fieldName, $this->getAssociationNames(), true);
157
    }
158
159
    /** {@inheritdoc} */
160
    public function getAssociationNames()
161
    {
162
        return array_keys($this->associations);
163
    }
164
165
    /** {@inheritdoc} */
166
    public function isSingleValuedAssociation($fieldName)
167
    {
168
        return $this->hasAssociation($fieldName) && $this->associations[$fieldName]['type'] & self::TO_ONE;
169
    }
170
171
    /** {@inheritdoc} */
172
    public function isCollectionValuedAssociation($fieldName)
173
    {
174
        return $this->hasAssociation($fieldName) && $this->associations[$fieldName]['type'] & self::TO_MANY;
175
    }
176
177
    /** {@inheritdoc} */
178
    public function getIdentifierFieldNames()
179
    {
180
        return $this->identifier;
181
    }
182
183
    /** {@inheritdoc} */
184
    public function getTypeOfField($fieldName)
185
    {
186
        return $this->fields[$fieldName]['type'];
187
    }
188
189
    /** {@inheritdoc} */
190
    public function getAssociationTargetClass($assocName)
191
    {
192
        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
    public function getIdentifierValues($object)
211
    {
212
        if ($this->isIdentifierComposite) {
213
            $id = [];
214
            foreach ($this->identifier as $idField) {
215
                $value = $this->reflFields[$idField]->getValue($object);
216
                if ($value !== null) {
217
                    $id[$idField] = $value;
218
                }
219
            }
220
221
            return $id;
222
        }
223
        $id    = $this->identifier[0];
224
        $value = $this->reflFields[$id]->getValue($object);
225
        if (null === $value) {
226
            return [];
227
        }
228
229
        return [$id => $value];
230
    }
231
232
    /** {@inheritdoc} */
233
    public function wakeupReflection(ReflectionService $reflService)
234
    {
235
        // Restore ReflectionClass and properties
236
        $this->reflClass    = $reflService->getClass($this->name);
237
        $this->instantiator = $this->instantiator ?: new Instantiator();
238
239 View Code Duplication
        foreach ($this->fields as $field => $mapping) {
240
            $class                    = array_key_exists('declared', $mapping) ? $mapping['declared'] : $this->name;
241
            $this->reflFields[$field] = $reflService->getAccessibleProperty($class, $field);
242
        }
243
244 View Code Duplication
        foreach ($this->associations as $field => $mapping) {
245
            $class                    = array_key_exists('declared', $mapping) ? $mapping['declared'] : $this->name;
246
            $this->reflFields[$field] = $reflService->getAccessibleProperty($class, $field);
247
        }
248
    }
249
250
    /** {@inheritdoc} */
251
    public function initializeReflection(ReflectionService $reflService)
252
    {
253
        $this->reflClass = $reflService->getClass($this->name);
254
        $this->namespace = $reflService->getClassNamespace($this->name);
255
        if ($this->reflClass) {
256
            $this->name = $this->rootEntityName = $this->reflClass->getName();
257
        }
258
    }
259
260
    /** {@inheritdoc} */
261
    public function getApiName()
262
    {
263
        if (null === $this->apiName) {
264
            throw MappingException::noApiSpecified($this->getName());
265
        }
266
267
        return $this->apiName;
268
    }
269
270
    /** {@inheritdoc} */
271
    public function getClientName()
272
    {
273
        if (null === $this->clientName) {
274
            throw MappingException::noClientSpecified($this->getName());
275
        }
276
277
        return $this->clientName;
278
    }
279
280
    public function mapField(array $mapping)
281
    {
282
        $this->validateAndCompleteFieldMapping($mapping);
283
        $this->assertFieldNotMapped($mapping['field']);
284
        $this->fields[$mapping['field']] = $mapping;
285
    }
286
287
    /** {@inheritdoc} */
288
    public function getFieldMapping($fieldName)
289
    {
290
        if (!isset($this->fields[$fieldName])) {
291
            throw MappingException::unknownField($fieldName, $this->getName());
292
        }
293
294
        return $this->fields[$fieldName];
295
    }
296
297
    /** {@inheritdoc} */
298
    public function getAssociationMapping($fieldName)
299
    {
300
        if (!isset($this->associations[$fieldName])) {
301
            throw MappingException::unknownAssociation($fieldName, $this->getName());
302
        }
303
304
        return $this->associations[$fieldName];
305
    }
306
307
    public function setCustomRepositoryClass($customRepositoryClassName)
308
    {
309
        $this->repositoryClass = $customRepositoryClassName;
310
    }
311
312
    /**
313
     * @internal
314
     *
315
     * @param array $mapping
316
     *
317
     * @return void
318
     */
319
    public function addInheritedFieldMapping(array $mapping)
320
    {
321
        $this->fields[$mapping['field']]         = $mapping;
322
        $this->apiFieldNames[$mapping['field']]  = $mapping['api_field'];
323
        $this->fieldNames[$mapping['api_field']] = $mapping['field'];
324
    }
325
326
    /** {@inheritdoc} */
327
    public function getFieldName($apiFieldName)
328
    {
329
        return $this->fieldNames[$apiFieldName];
330
    }
331
332
    /** {@inheritdoc} */
333
    public function getApiFieldName($fieldName)
334
    {
335
        return $this->apiFieldNames[$fieldName];
336
    }
337
338
    public function hasApiField($apiFieldName)
339
    {
340
        return array_key_exists($apiFieldName, $this->fieldNames);
341
    }
342
343
    public function mapOneToMany(array $mapping)
344
    {
345
        $mapping = $this->validateAndCompleteOneToManyMapping($mapping);
346
347
        $this->storeMapping($mapping);
348
    }
349
350
    public function mapManyToOne(array $mapping)
351
    {
352
        $mapping = $this->validateAndCompleteOneToOneMapping($mapping);
353
354
        $this->storeMapping($mapping);
355
    }
356
357
    public function mapOneToOne(array $mapping)
358
    {
359
        $mapping = $this->validateAndCompleteOneToOneMapping($mapping);
360
361
        $this->storeMapping($mapping);
362
    }
363
364
    /** {@inheritdoc} */
365
    public function newInstance()
366
    {
367
        return $this->instantiator->instantiate($this->name);
368
    }
369
370
    public function isIdentifierComposite()
371
    {
372
        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
    public function addInheritedAssociationMapping(array $mapping)
397
    {
398
        $this->associations[$mapping['field']]   = $mapping;
399
        $this->apiFieldNames[$mapping['field']]  = $mapping['api_field'];
400
        $this->fieldNames[$mapping['api_field']] = $mapping['field'];
401
    }
402
403
    /** {@inheritdoc} */
404
    public function getSubclasses()
405
    {
406
        //fixme
407
        return [];
408
    }
409
410
    /** {@inheritdoc} */
411
    public function getAssociationMappings()
412
    {
413
        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
    protected function validateAndCompleteAssociationMapping(array $mapping)
427
    {
428
        if (!array_key_exists('api_field', $mapping)) {
429
            $mapping['api_field'] = $mapping['field'];
430
        }
431
432
        if (!isset($mapping['mappedBy'])) {
433
            $mapping['mappedBy'] = null;
434
        }
435
436
        if (!isset($mapping['inversedBy'])) {
437
            $mapping['inversedBy'] = null;
438
        }
439
440
        $mapping['isOwningSide'] = true; // assume owning side until we hit mappedBy
441
442
        // unset optional indexBy attribute if its empty
443
        if (!isset($mapping['indexBy']) || !$mapping['indexBy']) {
444
            unset($mapping['indexBy']);
445
        }
446
447
        // If targetEntity is unqualified, assume it is in the same namespace as
448
        // the sourceEntity.
449
        $mapping['source'] = $this->name;
450
        if (isset($mapping['target'])) {
451
            $mapping['target'] = ltrim($mapping['target'], '\\');
452
        }
453
454
        if (($mapping['type'] & self::MANY_TO_ONE) > 0 &&
455
            isset($mapping['orphanRemoval']) &&
456
            $mapping['orphanRemoval'] == true
457
        ) {
458
            throw new MappingException(
459
                sprintf('Illegal orphanRemoval %s for %s', $mapping['field'], $this->name)
460
            );
461
        }
462
463
        // Complete id mapping
464
        if (isset($mapping['id']) && $mapping['id'] === true) {
465 View Code Duplication
            if (isset($mapping['orphanRemoval']) && $mapping['orphanRemoval'] == true) {
0 ignored issues
show
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
        if (null !== $mapping['mappedBy']) {
484
            $mapping['isOwningSide'] = false;
485
        }
486
487 View Code Duplication
        if (isset($mapping['id']) && $mapping['id'] === true && $mapping['type'] & self::TO_MANY) {
0 ignored issues
show
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
        if ( ! isset($mapping['fetch'])) {
495
            $mapping['fetch'] = self::FETCH_LAZY;
496
        }
497
498
        // Cascades
499
        $cascades    = isset($mapping['cascade']) ? array_map('strtolower', $mapping['cascade']) : [];
500
        $allCascades = ['remove', 'persist', 'refresh', 'merge', 'detach'];
501
        if (in_array('all', $cascades, true)) {
502
            $cascades = $allCascades;
503
        } elseif (count($cascades) !== count(array_intersect($cascades, $allCascades))) {
504
            throw new MappingException('Invalid cascades: ' . implode(', ', $cascades));
505
        }
506
        $mapping['cascade']          = $cascades;
507
        $mapping['isCascadeRemove']  = in_array('remove', $cascades, true);
508
        $mapping['isCascadePersist'] = in_array('persist', $cascades, true);
509
        $mapping['isCascadeRefresh'] = in_array('refresh', $cascades, true);
510
        $mapping['isCascadeMerge']   = in_array('merge', $cascades, true);
511
        $mapping['isCascadeDetach']  = in_array('detach', $cascades, true);
512
513
        return $mapping;
514
    }
515
516
    private function storeMapping(array $mapping)
517
    {
518
        $this->assertFieldNotMapped($mapping['field']);
519
520
        $this->apiFieldNames[$mapping['field']]  = $mapping['api_field'];
521
        $this->fieldNames[$mapping['api_field']] = $mapping['field'];
522
        $this->associations[$mapping['field']]   = $mapping;
523
    }
524
525
    private function validateAndCompleteFieldMapping(array &$mapping)
526
    {
527
        if (!array_key_exists('api_field', $mapping)) {
528
            $mapping['api_field'] = $mapping['field']; //todo: invent naming strategy
529
        }
530
531
        $this->apiFieldNames[$mapping['field']]  = $mapping['api_field'];
532
        $this->fieldNames[$mapping['api_field']] = $mapping['field'];
533
534
        // Complete id mapping
535
        if (isset($mapping['id']) && $mapping['id'] === true) {
536
            if (!in_array($mapping['field'], $this->identifier, true)) {
537
                $this->identifier[] = $mapping['field'];
538
            }
539
            // Check for composite key
540
            if (!$this->isIdentifierComposite && count($this->identifier) > 1) {
541
                $this->isIdentifierComposite = true;
542
            }
543
        }
544
    }
545
546
    /**
547
     * @param string $fieldName
548
     *
549
     * @throws MappingException
550
     */
551
    private function assertFieldNotMapped($fieldName)
552
    {
553
        if (array_key_exists($fieldName, $this->fields) ||
554
            array_key_exists($fieldName, $this->associations) ||
555
            array_key_exists($fieldName, $this->identifier)
556
        ) {
557
            throw new MappingException('Field already mapped');
558
        }
559
    }
560
561
    /**
562
     * @param array $mapping
563
     *
564
     * @return array
565
     * @throws MappingException
566
     * @throws \InvalidArgumentException
567
     */
568
    private function validateAndCompleteOneToManyMapping(array $mapping)
569
    {
570
        $mapping = $this->validateAndCompleteAssociationMapping($mapping);
571
572
        // OneToMany-side MUST be inverse (must have mappedBy)
573
        if (!isset($mapping['mappedBy'])) {
574
            throw new MappingException(
575
                sprintf('Many to many requires mapped by: %s', $mapping['field'])
576
            );
577
        }
578
        $mapping['orphanRemoval']   = isset($mapping['orphanRemoval']) && $mapping['orphanRemoval'];
579
        $mapping['isCascadeRemove'] = $mapping['orphanRemoval'] || $mapping['isCascadeRemove'];
580
        $this->assertMappingOrderBy($mapping);
581
582
        return $mapping;
583
    }
584
585
    /**
586
     * @param array $mapping
587
     *
588
     * @throws \InvalidArgumentException
589
     */
590
    private function assertMappingOrderBy(array $mapping)
591
    {
592
        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
    }
598
599
    /**
600
     * @param array $mapping
601
     *
602
     * @return array
603
     */
604
    private function validateAndCompleteOneToOneMapping(array $mapping)
605
    {
606
        $mapping = $this->validateAndCompleteAssociationMapping($mapping);
607
608
        $mapping['orphanRemoval']   = isset($mapping['orphanRemoval']) && $mapping['orphanRemoval'];
609
        $mapping['isCascadeRemove'] = $mapping['orphanRemoval'] || $mapping['isCascadeRemove'];
610
        if ($mapping['orphanRemoval']) {
611
            unset($mapping['unique']);
612
        }
613
614
        return $mapping;
615
    }
616
617
    public function isReadOnly()
618
    {
619
        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
    public function isChangeTrackingDeferredImplicit()
648
    {
649
        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