Completed
Push — master ( ce1bfb...dabc7b )
by Tom
02:08
created

DoctrineObject::hydrateValue()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 3
crap 1
1
<?php
2
3
namespace DoctrineModule\Stdlib\Hydrator;
4
5
use DateTime;
6
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
7
use Doctrine\Common\Persistence\ObjectManager;
8
use Doctrine\Common\Inflector\Inflector;
9
use InvalidArgumentException;
10
use RuntimeException;
11
use Traversable;
12
use Zend\Stdlib\ArrayUtils;
13
use Zend\Hydrator\AbstractHydrator;
14
use Zend\Hydrator\Filter\FilterProviderInterface;
15
16
/**
17
 * This hydrator has been completely refactored for DoctrineModule 0.7.0. It provides an easy and powerful way
18
 * of extracting/hydrator objects in Doctrine, by handling most associations types.
19
 *
20
 * Starting from DoctrineModule 0.8.0, the hydrator can be used multiple times with different objects
21
 *
22
 * @license MIT
23
 * @link    http://www.doctrine-project.org/
24
 * @since   0.7.0
25
 * @author  Michael Gallego <[email protected]>
26
 */
27
class DoctrineObject extends AbstractHydrator
28
{
29
    /**
30
     * @var ObjectManager
31
     */
32
    protected $objectManager;
33
34
    /**
35
     * @var ClassMetadata
36
     */
37
    protected $metadata;
38
39
    /**
40
     * @var bool
41
     */
42
    protected $byValue = true;
43
44
    /**
45
     * @var string
46
     */
47
    protected $defaultByValueStrategy = __NAMESPACE__ . '\Strategy\AllowRemoveByValue';
48
49
    /**
50
     * @var string
51
     */
52
    protected $defaultByReferenceStrategy = __NAMESPACE__ . '\Strategy\AllowRemoveByReference';
53
54
55
    /**
56
     * Constructor
57
     *
58
     * @param ObjectManager $objectManager The ObjectManager to use
59
     * @param bool          $byValue       If set to true, hydrator will always use entity's public API
60
     */
61 71
    public function __construct(ObjectManager $objectManager, $byValue = true)
62
    {
63 71
        parent::__construct();
64
65 71
        $this->objectManager = $objectManager;
66 71
        $this->byValue       = (bool) $byValue;
67 71
    }
68
69
    /**
70
     * @return string
71
     */
72 14
    public function getDefaultByValueStrategy()
73
    {
74 14
        return $this->defaultByValueStrategy;
75
    }
76
77
    /**
78
     * @param string $defaultByValueStrategy
79
     * @return DoctrineObject
80
     */
81 1
    public function setDefaultByValueStrategy($defaultByValueStrategy)
82
    {
83 1
        $this->defaultByValueStrategy = $defaultByValueStrategy;
84 1
        return $this;
85
    }
86
87
    /**
88
     * @return string
89
     */
90 9
    public function getDefaultByReferenceStrategy()
91
    {
92 9
        return $this->defaultByReferenceStrategy;
93
    }
94
95
    /**
96
     * @param string $defaultByReferenceStrategy
97
     * @return DoctrineObject
98
     */
99 1
    public function setDefaultByReferenceStrategy($defaultByReferenceStrategy)
100
    {
101 1
        $this->defaultByReferenceStrategy = $defaultByReferenceStrategy;
102 1
        return $this;
103
    }
104
105
    /**
106
     * Extract values from an object
107
     *
108
     * @param  object $object
109
     * @return array
110
     */
111 17
    public function extract($object)
112
    {
113 17
        $this->prepare($object);
114
115 17
        if ($this->byValue) {
116 10
            return $this->extractByValue($object);
117
        }
118
119 7
        return $this->extractByReference($object);
120
    }
121
122
    /**
123
     * Hydrate $object with the provided $data.
124
     *
125
     * @param  array  $data
126
     * @param  object $object
127
     * @return object
128
     */
129 55
    public function hydrate(array $data, $object)
130
    {
131 55
        $this->prepare($object);
132
133 55
        if ($this->byValue) {
134 42
            return $this->hydrateByValue($data, $object);
135
        }
136
137 27
        return $this->hydrateByReference($data, $object);
138
    }
139
140
    /**
141
     * Prepare the hydrator by adding strategies to every collection valued associations
142
     *
143
     * @param  object $object
144
     * @return void
145
     */
146 71
    protected function prepare($object)
147
    {
148 71
        $this->metadata = $this->objectManager->getClassMetadata(get_class($object));
149 71
        $this->prepareStrategies();
150 71
    }
151
152
    /**
153
     * Prepare strategies before the hydrator is used
154
     *
155
     * @throws \InvalidArgumentException
156
     * @return void
157
     */
158 71
    protected function prepareStrategies()
159
    {
160 71
        $associations = $this->metadata->getAssociationNames();
161
162 71
        foreach ($associations as $association) {
163 36
            if ($this->metadata->isCollectionValuedAssociation($association)) {
164
                // Add a strategy if the association has none set by user
165 23
                if (! $this->hasStrategy($association)) {
166 21
                    if ($this->byValue) {
167 14
                        $strategyClassName = $this->getDefaultByValueStrategy();
168
                    } else {
169 9
                        $strategyClassName = $this->getDefaultByReferenceStrategy();
170
                    }
171
172 21
                    $this->addStrategy($association, new $strategyClassName());
173
                }
174
175 23
                $strategy = $this->getStrategy($association);
176
177 23
                if (! $strategy instanceof Strategy\AbstractCollectionStrategy) {
178
                    throw new InvalidArgumentException(
179
                        sprintf(
180
                            'Strategies used for collections valued associations must inherit from '
181
                            . 'Strategy\AbstractCollectionStrategy, %s given',
182
                            get_class($strategy)
183
                        )
184
                    );
185
                }
186
187 23
                $strategy->setCollectionName($association)
188 36
                         ->setClassMetadata($this->metadata);
189
            }
190
        }
191 71
    }
192
193
    /**
194
     * Extract values from an object using a by-value logic (this means that it uses the entity
195
     * API, in this case, getters)
196
     *
197
     * @param  object $object
198
     * @throws RuntimeException
199
     * @return array
200
     */
201 10
    protected function extractByValue($object)
202
    {
203 10
        $fieldNames = array_merge($this->metadata->getFieldNames(), $this->metadata->getAssociationNames());
204 10
        $methods    = get_class_methods($object);
205 10
        $filter     = $object instanceof FilterProviderInterface
206
            ? $object->getFilter()
207 10
            : $this->filterComposite;
208
209 10
        $data = [];
210 10
        foreach ($fieldNames as $fieldName) {
211 10
            if ($filter && ! $filter->filter($fieldName)) {
212 1
                continue;
213
            }
214
215 10
            $getter = 'get' . Inflector::classify($fieldName);
216 10
            $isser  = 'is' . Inflector::classify($fieldName);
217
218 10
            $dataFieldName = $this->computeExtractFieldName($fieldName);
219 10
            if (in_array($getter, $methods)) {
220 10
                $data[$dataFieldName] = $this->extractValue($fieldName, $object->$getter(), $object);
221 1
            } elseif (in_array($isser, $methods)) {
222 1
                $data[$dataFieldName] = $this->extractValue($fieldName, $object->$isser(), $object);
223
            } elseif (substr($fieldName, 0, 2) === 'is'
224
                && ctype_upper(substr($fieldName, 2, 1))
225
                && in_array($fieldName, $methods)) {
226 10
                $data[$dataFieldName] = $this->extractValue($fieldName, $object->$fieldName(), $object);
227
            }
228
229
            // Unknown fields are ignored
230
        }
231
232 10
        return $data;
233
    }
234
235
    /**
236
     * Extract values from an object using a by-reference logic (this means that values are
237
     * directly fetched without using the public API of the entity, in this case, getters)
238
     *
239
     * @param  object $object
240
     * @return array
241
     */
242 7
    protected function extractByReference($object)
243
    {
244 7
        $fieldNames = array_merge($this->metadata->getFieldNames(), $this->metadata->getAssociationNames());
245 7
        $refl       = $this->metadata->getReflectionClass();
246 7
        $filter     = $object instanceof FilterProviderInterface
247
            ? $object->getFilter()
248 7
            : $this->filterComposite;
249
250 7
        $data = [];
251 7
        foreach ($fieldNames as $fieldName) {
252 7
            if ($filter && ! $filter->filter($fieldName)) {
253 1
                continue;
254
            }
255 7
            $reflProperty = $refl->getProperty($fieldName);
256 7
            $reflProperty->setAccessible(true);
257
258 7
            $dataFieldName        = $this->computeExtractFieldName($fieldName);
259 7
            $data[$dataFieldName] = $this->extractValue($fieldName, $reflProperty->getValue($object), $object);
260
        }
261
262 7
        return $data;
263
    }
264
265
    /**
266
     * Converts a value for hydration
267
     * Apply strategies first, then the type conversions
268
     *
269
     * @inheritdoc
270
     */
271 52
    public function hydrateValue($name, $value, $data = null)
272
    {
273 52
        $value = parent::hydrateValue($name, $value, $data);
274
275 52
        return $this->handleTypeConversions($value, $this->metadata->getTypeOfField($name));
276
    }
277
278
    /**
279
     * Hydrate the object using a by-value logic (this means that it uses the entity API, in this
280
     * case, setters)
281
     *
282
     * @param  array  $data
283
     * @param  object $object
284
     * @throws RuntimeException
285
     * @return object
286
     */
287 43
    protected function hydrateByValue(array $data, $object)
288
    {
289 43
        $tryObject = $this->tryConvertArrayToObject($data, $object);
290 43
        $metadata  = $this->metadata;
291
292 43
        if (is_object($tryObject)) {
293 40
            $object = $tryObject;
294
        }
295
296 43
        foreach ($data as $field => $value) {
297 41
            $field  = $this->computeHydrateFieldName($field);
298 41
            $setter = 'set' . Inflector::classify($field);
299
300 41
            if ($metadata->hasAssociation($field)) {
301 18
                $target = $metadata->getAssociationTargetClass($field);
302
303 18
                if ($metadata->isSingleValuedAssociation($field)) {
304 7
                    if (! is_callable([$object, $setter])) {
305
                        continue;
306
                    }
307
308 7
                    $value = $this->toOne($target, $this->hydrateValue($field, $value, $data));
309
310 7
                    if (null === $value
311 7
                        && ! current($metadata->getReflectionClass()->getMethod($setter)->getParameters())->allowsNull()
312
                    ) {
313 1
                        continue;
314
                    }
315
316 6
                    $object->$setter($value);
317 11
                } elseif ($metadata->isCollectionValuedAssociation($field)) {
318 17
                    $this->toMany($object, $field, $target, $value);
319
                }
320
            } else {
321 26
                if (! is_callable([$object, $setter])) {
322 1
                    continue;
323
                }
324
325 39
                $object->$setter($this->hydrateValue($field, $value, $data));
326
            }
327
        }
328
329 43
        return $object;
330
    }
331
332
    /**
333
     * Hydrate the object using a by-reference logic (this means that values are modified directly without
334
     * using the public API, in this case setters, and hence override any logic that could be done in those
335
     * setters)
336
     *
337
     * @param  array  $data
338
     * @param  object $object
339
     * @return object
340
     */
341 28
    protected function hydrateByReference(array $data, $object)
342
    {
343 28
        $tryObject = $this->tryConvertArrayToObject($data, $object);
344 28
        $metadata  = $this->metadata;
345 28
        $refl      = $metadata->getReflectionClass();
346
347 28
        if (is_object($tryObject)) {
348 27
            $object = $tryObject;
349
        }
350
351 28
        foreach ($data as $field => $value) {
352 26
            $field = $this->computeHydrateFieldName($field);
353
354
            // Ignore unknown fields
355 26
            if (! $refl->hasProperty($field)) {
356
                continue;
357
            }
358
359 26
            $reflProperty = $refl->getProperty($field);
360 26
            $reflProperty->setAccessible(true);
361
362 26
            if ($metadata->hasAssociation($field)) {
363 10
                $target = $metadata->getAssociationTargetClass($field);
364
365 10
                if ($metadata->isSingleValuedAssociation($field)) {
366 4
                    $value = $this->toOne($target, $this->hydrateValue($field, $value, $data));
367 4
                    $reflProperty->setValue($object, $value);
368 6
                } elseif ($metadata->isCollectionValuedAssociation($field)) {
369 10
                    $this->toMany($object, $field, $target, $value);
370
                }
371
            } else {
372 26
                $reflProperty->setValue($object, $this->hydrateValue($field, $value, $data));
373
            }
374
        }
375
376 28
        return $object;
377
    }
378
379
    /**
380
     * This function tries, given an array of data, to convert it to an object if the given array contains
381
     * an identifier for the object. This is useful in a context of updating existing entities, without ugly
382
     * tricks like setting manually the existing id directly into the entity
383
     *
384
     * @param  array  $data   The data that may contain identifiers keys
385
     * @param  object $object
386
     * @return object
387
     */
388 55
    protected function tryConvertArrayToObject($data, $object)
389
    {
390 55
        $metadata         = $this->metadata;
391 55
        $identifierNames  = $metadata->getIdentifierFieldNames($object);
0 ignored issues
show
Unused Code introduced by
The call to ClassMetadata::getIdentifierFieldNames() has too many arguments starting with $object.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
392 55
        $identifierValues = [];
393
394 55
        if (empty($identifierNames)) {
395 30
            return $object;
396
        }
397
398 25
        foreach ($identifierNames as $identifierName) {
399 25
            if (! isset($data[$identifierName])) {
400 21
                return $object;
401
            }
402
403 4
            $identifierValues[$identifierName] = $data[$identifierName];
404
        }
405
406 4
        return $this->find($identifierValues, $metadata->getName());
407
    }
408
409
    /**
410
     * Handle ToOne associations
411
     *
412
     * When $value is an array but is not the $target's identifiers, $value is
413
     * most likely an array of fieldset data. The identifiers will be determined
414
     * and a target instance will be initialized and then hydrated. The hydrated
415
     * target will be returned.
416
     *
417
     * @param  string $target
418
     * @param  mixed  $value
419
     * @return object
420
     */
421 11
    protected function toOne($target, $value)
422
    {
423 11
        $metadata = $this->objectManager->getClassMetadata($target);
424
425 11
        if (is_array($value) && array_keys($value) != $metadata->getIdentifier()) {
426
            // $value is most likely an array of fieldset data
427 1
            $identifiers = array_intersect_key(
428 1
                $value,
429 1
                array_flip($metadata->getIdentifier())
430
            );
431 1
            $object      = $this->find($identifiers, $target) ?: new $target;
432 1
            return $this->hydrate($value, $object);
433
        }
434
435 10
        return $this->find($value, $target);
436
    }
437
438
    /**
439
     * Handle ToMany associations. In proper Doctrine design, Collections should not be swapped, so
440
     * collections are always handled by reference. Internally, every collection is handled using specials
441
     * strategies that inherit from AbstractCollectionStrategy class, and that add or remove elements but without
442
     * changing the collection of the object
443
     *
444
     * @param  object $object
445
     * @param  mixed  $collectionName
446
     * @param  string $target
447
     * @param  mixed  $values
448
     *
449
     * @throws \InvalidArgumentException
450
     *
451
     * @return void
452
     */
453 17
    protected function toMany($object, $collectionName, $target, $values)
454
    {
455 17
        $metadata   = $this->objectManager->getClassMetadata(ltrim($target, '\\'));
456 17
        $identifier = $metadata->getIdentifier();
457
458 17
        if (! is_array($values) && ! $values instanceof Traversable) {
459
            $values = (array)$values;
460
        }
461
462 17
        $collection = [];
463
464
        // If the collection contains identifiers, fetch the objects from database
465 17
        foreach ($values as $value) {
466 17
            if ($value instanceof $target) {
467
                // assumes modifications have already taken place in object
468 7
                $collection[] = $value;
469 7
                continue;
470
            } elseif (empty($value)) {
471
                // assumes no id and retrieves new $target
472 1
                $collection[] = $this->find($value, $target);
473 1
                continue;
474
            }
475
476 9
            $find = [];
477 9
            if (is_array($identifier)) {
478 9
                foreach ($identifier as $field) {
479 9
                    switch (gettype($value)) {
480 9
                        case 'object':
481 1
                            $getter = 'get' . Inflector::classify($field);
482
483 1
                            if (is_callable([$value, $getter])) {
484
                                $find[$field] = $value->$getter();
485 1
                            } elseif (property_exists($value, $field)) {
486 1
                                $find[$field] = $value->$field;
487
                            }
488 1
                            break;
489 8
                        case 'array':
490 5
                            if (array_key_exists($field, $value) && $value[$field] != null) {
491 5
                                $find[$field] = $value[$field];
492 5
                                unset($value[$field]); // removed identifier from persistable data
493
                            }
494 5
                            break;
495
                        default:
496 3
                            $find[$field] = $value;
497 9
                            break;
498
                    }
499
                }
500
            }
501
502 9
            if (! empty($find) && $found = $this->find($find, $target)) {
503 9
                $collection[] = (is_array($value)) ? $this->hydrate($value, $found) : $found;
504
            } else {
505 9
                $collection[] = (is_array($value)) ? $this->hydrate($value, new $target) : new $target;
506
            }
507
        }
508
509 17
        $collection = array_filter(
510 17
            $collection,
511
            function ($item) {
512 17
                return null !== $item;
513 17
            }
514
        );
515
516
        // Set the object so that the strategy can extract the Collection from it
517
518
        /** @var \DoctrineModule\Stdlib\Hydrator\Strategy\AbstractCollectionStrategy $collectionStrategy */
519 17
        $collectionStrategy = $this->getStrategy($collectionName);
520 17
        $collectionStrategy->setObject($object);
521
522
        // We could directly call hydrate method from the strategy, but if people want to override
523
        // hydrateValue function, they can do it and do their own stuff
524 17
        $this->hydrateValue($collectionName, $collection, $values);
525 17
    }
526
527
    /**
528
     * Handle various type conversions that should be supported natively by Doctrine (like DateTime)
529
     * See Documentation of Doctrine Mapping Types for defaults
530
     *
531
     * @link http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/basic-mapping.html#doctrine-mapping-types
532
     * @param  mixed  $value
533
     * @param  string $typeOfField
534
     * @return DateTime
535
     */
536 52
    protected function handleTypeConversions($value, $typeOfField)
537
    {
538 52
        switch ($typeOfField) {
539 52
            case 'boolean':
540 1
                $value = (bool)$value;
541 1
                break;
542 51
            case 'string':
543 42
            case 'text':
544 41
            case 'bigint':
545 40
            case 'decimal':
546 16
                $value = (string)$value;
547 16
                break;
548 39
            case 'integer':
549 36
            case 'smallint':
550 5
                $value = (int)$value;
551 5
                break;
552 35
            case 'float':
553 1
                $value = (double)$value;
554 1
                break;
555 34
            case 'datetimetz':
556 33
            case 'datetime':
557 30
            case 'time':
558 29
            case 'date':
559 6
                if ($value === '') {
560 1
                    return null;
561
                }
562
563 5
                if ($value instanceof Datetime) {
564 4
                    return $value;
565
                }
566
567 5
                if (is_int($value)) {
568 5
                    $dateTime = new DateTime();
569 5
                    $dateTime->setTimestamp($value);
570 5
                    return $dateTime;
571
                }
572
573 4
                if (is_string($value)) {
574 4
                    return new DateTime($value);
575
                }
576
577
                break;
578
            default:
579 28
                break;
580
        }
581
582 46
        return $value;
583
    }
584
585
    /**
586
     * Find an object by a given target class and identifier
587
     *
588
     * @param  mixed   $identifiers
589
     * @param  string  $targetClass
590
     *
591
     * @return object|null
592
     */
593 25
    protected function find($identifiers, $targetClass)
594
    {
595 25
        if ($identifiers instanceof $targetClass) {
596 2
            return $identifiers;
597
        }
598
599 23
        if ($this->isNullIdentifier($identifiers)) {
600 4
            return null;
601
        }
602
603 19
        return $this->objectManager->find($targetClass, $identifiers);
604
    }
605
606
    /**
607
     * Verifies if a provided identifier is to be considered null
608
     *
609
     * @param  mixed $identifier
610
     *
611
     * @return bool
612
     */
613 23
    private function isNullIdentifier($identifier)
614
    {
615 23
        if (null === $identifier) {
616 4
            return true;
617
        }
618
619 19
        if ($identifier instanceof Traversable || is_array($identifier)) {
620 16
            $nonNullIdentifiers = array_filter(
621 16
                ArrayUtils::iteratorToArray($identifier),
622
                function ($value) {
623 16
                    return null !== $value;
624 16
                }
625
            );
626
627 16
            return empty($nonNullIdentifiers);
628
        }
629
630 3
        return false;
631
    }
632
633
    /**
634
     * Applies the naming strategy if there is one set
635
     *
636
     * @param string $field
637
     *
638
     * @return string
639
     */
640 53
    protected function computeHydrateFieldName($field)
641
    {
642 53
        if ($this->hasNamingStrategy()) {
643 2
            $field = $this->getNamingStrategy()->hydrate($field);
644
        }
645 53
        return $field;
646
    }
647
648
    /**
649
     * Applies the naming strategy if there is one set
650
     *
651
     * @param string $field
652
     *
653
     * @return string
654
     */
655 17
    protected function computeExtractFieldName($field)
656
    {
657 17
        if ($this->hasNamingStrategy()) {
658 2
            $field = $this->getNamingStrategy()->extract($field);
659
        }
660 17
        return $field;
661
    }
662
}
663