Completed
Push — master ( a7e0f3...57cf4b )
by Tom
13s
created

DoctrineObject::handleTypeConversions()   D

Complexity

Conditions 18
Paths 30

Size

Total Lines 52

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 36
CRAP Score 18.0063

Importance

Changes 0
Metric Value
dl 0
loc 52
ccs 36
cts 37
cp 0.973
rs 4.8666
c 0
b 0
f 0
cc 18
nc 30
nop 2
crap 18.0063

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 72
    public function __construct(ObjectManager $objectManager, $byValue = true)
62
    {
63 72
        parent::__construct();
64
65 72
        $this->objectManager = $objectManager;
66 72
        $this->byValue       = (bool) $byValue;
67 72
    }
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 56
    public function hydrate(array $data, $object)
130
    {
131 56
        $this->prepare($object);
132
133 56
        if ($this->byValue) {
134 43
            return $this->hydrateByValue($data, $object);
135
        }
136
137 28
        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 72
    protected function prepare($object)
147
    {
148 72
        $this->metadata = $this->objectManager->getClassMetadata(get_class($object));
149 72
        $this->prepareStrategies();
150 72
    }
151
152
    /**
153
     * Prepare strategies before the hydrator is used
154
     *
155
     * @throws \InvalidArgumentException
156
     * @return void
157
     */
158 72
    protected function prepareStrategies()
159
    {
160 72
        $associations = $this->metadata->getAssociationNames();
161
162 72
        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 72
    }
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 53
    public function hydrateValue($name, $value, $data = null)
272
    {
273 53
        $value = parent::hydrateValue($name, $value, $data);
274
275 53
        if (is_null($value) && method_exists($this->metadata, 'isNullable') && $this->metadata->isNullable($name)) {
0 ignored issues
show
Bug introduced by
The method isNullable() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
276
            return null;
277
        }
278
279 53
        return $this->handleTypeConversions($value, $this->metadata->getTypeOfField($name));
280
    }
281
282
    /**
283
     * Hydrate the object using a by-value logic (this means that it uses the entity API, in this
284
     * case, setters)
285
     *
286
     * @param  array  $data
287
     * @param  object $object
288
     * @throws RuntimeException
289
     * @return object
290
     */
291 44
    protected function hydrateByValue(array $data, $object)
292
    {
293 44
        $tryObject = $this->tryConvertArrayToObject($data, $object);
294 44
        $metadata  = $this->metadata;
295
296 44
        if (is_object($tryObject)) {
297 41
            $object = $tryObject;
298
        }
299
300 44
        foreach ($data as $field => $value) {
301 42
            $field  = $this->computeHydrateFieldName($field);
302 42
            $setter = 'set' . Inflector::classify($field);
303
304 42
            if ($metadata->hasAssociation($field)) {
305 18
                $target = $metadata->getAssociationTargetClass($field);
306
307 18
                if ($metadata->isSingleValuedAssociation($field)) {
308 7
                    if (! is_callable([$object, $setter])) {
309
                        continue;
310
                    }
311
312 7
                    $value = $this->toOne($target, $this->hydrateValue($field, $value, $data));
313
314 7
                    if (null === $value
315 7
                        && ! current($metadata->getReflectionClass()->getMethod($setter)->getParameters())->allowsNull()
316
                    ) {
317 1
                        continue;
318
                    }
319
320 6
                    $object->$setter($value);
321 11
                } elseif ($metadata->isCollectionValuedAssociation($field)) {
322 17
                    $this->toMany($object, $field, $target, $value);
323
                }
324
            } else {
325 27
                if (! is_callable([$object, $setter])) {
326 1
                    continue;
327
                }
328
329 40
                $object->$setter($this->hydrateValue($field, $value, $data));
330
            }
331
        }
332
333 44
        return $object;
334
    }
335
336
    /**
337
     * Hydrate the object using a by-reference logic (this means that values are modified directly without
338
     * using the public API, in this case setters, and hence override any logic that could be done in those
339
     * setters)
340
     *
341
     * @param  array  $data
342
     * @param  object $object
343
     * @return object
344
     */
345 29
    protected function hydrateByReference(array $data, $object)
346
    {
347 29
        $tryObject = $this->tryConvertArrayToObject($data, $object);
348 29
        $metadata  = $this->metadata;
349 29
        $refl      = $metadata->getReflectionClass();
350
351 29
        if (is_object($tryObject)) {
352 28
            $object = $tryObject;
353
        }
354
355 29
        foreach ($data as $field => $value) {
356 27
            $field = $this->computeHydrateFieldName($field);
357
358
            // Ignore unknown fields
359 27
            if (! $refl->hasProperty($field)) {
360
                continue;
361
            }
362
363 27
            $reflProperty = $refl->getProperty($field);
364 27
            $reflProperty->setAccessible(true);
365
366 27
            if ($metadata->hasAssociation($field)) {
367 10
                $target = $metadata->getAssociationTargetClass($field);
368
369 10
                if ($metadata->isSingleValuedAssociation($field)) {
370 4
                    $value = $this->toOne($target, $this->hydrateValue($field, $value, $data));
371 4
                    $reflProperty->setValue($object, $value);
372 6
                } elseif ($metadata->isCollectionValuedAssociation($field)) {
373 10
                    $this->toMany($object, $field, $target, $value);
374
                }
375
            } else {
376 27
                $reflProperty->setValue($object, $this->hydrateValue($field, $value, $data));
377
            }
378
        }
379
380 29
        return $object;
381
    }
382
383
    /**
384
     * This function tries, given an array of data, to convert it to an object if the given array contains
385
     * an identifier for the object. This is useful in a context of updating existing entities, without ugly
386
     * tricks like setting manually the existing id directly into the entity
387
     *
388
     * @param  array  $data   The data that may contain identifiers keys
389
     * @param  object $object
390
     * @return object
391
     */
392 56
    protected function tryConvertArrayToObject($data, $object)
393
    {
394 56
        $metadata         = $this->metadata;
395 56
        $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...
396 56
        $identifierValues = [];
397
398 56
        if (empty($identifierNames)) {
399 30
            return $object;
400
        }
401
402 26
        foreach ($identifierNames as $identifierName) {
403 26
            if (! isset($data[$identifierName])) {
404 22
                return $object;
405
            }
406
407 4
            $identifierValues[$identifierName] = $data[$identifierName];
408
        }
409
410 4
        return $this->find($identifierValues, $metadata->getName());
411
    }
412
413
    /**
414
     * Handle ToOne associations
415
     *
416
     * When $value is an array but is not the $target's identifiers, $value is
417
     * most likely an array of fieldset data. The identifiers will be determined
418
     * and a target instance will be initialized and then hydrated. The hydrated
419
     * target will be returned.
420
     *
421
     * @param  string $target
422
     * @param  mixed  $value
423
     * @return object
424
     */
425 11
    protected function toOne($target, $value)
426
    {
427 11
        $metadata = $this->objectManager->getClassMetadata($target);
428
429 11
        if (is_array($value) && array_keys($value) != $metadata->getIdentifier()) {
430
            // $value is most likely an array of fieldset data
431 1
            $identifiers = array_intersect_key(
432 1
                $value,
433 1
                array_flip($metadata->getIdentifier())
434
            );
435 1
            $object      = $this->find($identifiers, $target) ?: new $target;
436 1
            return $this->hydrate($value, $object);
437
        }
438
439 10
        return $this->find($value, $target);
440
    }
441
442
    /**
443
     * Handle ToMany associations. In proper Doctrine design, Collections should not be swapped, so
444
     * collections are always handled by reference. Internally, every collection is handled using specials
445
     * strategies that inherit from AbstractCollectionStrategy class, and that add or remove elements but without
446
     * changing the collection of the object
447
     *
448
     * @param  object $object
449
     * @param  mixed  $collectionName
450
     * @param  string $target
451
     * @param  mixed  $values
452
     *
453
     * @throws \InvalidArgumentException
454
     *
455
     * @return void
456
     */
457 17
    protected function toMany($object, $collectionName, $target, $values)
458
    {
459 17
        $metadata   = $this->objectManager->getClassMetadata(ltrim($target, '\\'));
460 17
        $identifier = $metadata->getIdentifier();
461
462 17
        if (! is_array($values) && ! $values instanceof Traversable) {
463
            $values = (array)$values;
464
        }
465
466 17
        $collection = [];
467
468
        // If the collection contains identifiers, fetch the objects from database
469 17
        foreach ($values as $value) {
470 17
            if ($value instanceof $target) {
471
                // assumes modifications have already taken place in object
472 7
                $collection[] = $value;
473 7
                continue;
474
            } elseif (empty($value)) {
475
                // assumes no id and retrieves new $target
476 1
                $collection[] = $this->find($value, $target);
477 1
                continue;
478
            }
479
480 9
            $find = [];
481 9
            if (is_array($identifier)) {
482 9
                foreach ($identifier as $field) {
483 9
                    switch (gettype($value)) {
484 9
                        case 'object':
485 1
                            $getter = 'get' . Inflector::classify($field);
486
487 1
                            if (is_callable([$value, $getter])) {
488
                                $find[$field] = $value->$getter();
489 1
                            } elseif (property_exists($value, $field)) {
490 1
                                $find[$field] = $value->$field;
491
                            }
492 1
                            break;
493 8
                        case 'array':
494 5
                            if (array_key_exists($field, $value) && $value[$field] != null) {
495 5
                                $find[$field] = $value[$field];
496 5
                                unset($value[$field]); // removed identifier from persistable data
497
                            }
498 5
                            break;
499
                        default:
500 3
                            $find[$field] = $value;
501 9
                            break;
502
                    }
503
                }
504
            }
505
506 9
            if (! empty($find) && $found = $this->find($find, $target)) {
507 9
                $collection[] = (is_array($value)) ? $this->hydrate($value, $found) : $found;
508
            } else {
509 9
                $collection[] = (is_array($value)) ? $this->hydrate($value, new $target) : new $target;
510
            }
511
        }
512
513 17
        $collection = array_filter(
514 17
            $collection,
515
            function ($item) {
516 17
                return null !== $item;
517 17
            }
518
        );
519
520
        // Set the object so that the strategy can extract the Collection from it
521
522
        /** @var \DoctrineModule\Stdlib\Hydrator\Strategy\AbstractCollectionStrategy $collectionStrategy */
523 17
        $collectionStrategy = $this->getStrategy($collectionName);
524 17
        $collectionStrategy->setObject($object);
525
526
        // We could directly call hydrate method from the strategy, but if people want to override
527
        // hydrateValue function, they can do it and do their own stuff
528 17
        $this->hydrateValue($collectionName, $collection, $values);
529 17
    }
530
531
    /**
532
     * Handle various type conversions that should be supported natively by Doctrine (like DateTime)
533
     * See Documentation of Doctrine Mapping Types for defaults
534
     *
535
     * @link http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/basic-mapping.html#doctrine-mapping-types
536
     * @param  mixed  $value
537
     * @param  string $typeOfField
538
     * @return DateTime
539
     */
540 53
    protected function handleTypeConversions($value, $typeOfField)
541
    {
542 53
        if (is_null($value)) {
543 5
            return null;
544
        }
545
546 48
        switch ($typeOfField) {
547 48
            case 'boolean':
548 1
                $value = (bool)$value;
549 1
                break;
550 47
            case 'string':
551 38
            case 'text':
552 37
            case 'bigint':
553 36
            case 'decimal':
554 16
                $value = (string)$value;
555 16
                break;
556 35
            case 'integer':
557 32
            case 'smallint':
558 5
                $value = (int)$value;
559 5
                break;
560 31
            case 'float':
561 1
                $value = (double)$value;
562 1
                break;
563 30
            case 'datetimetz':
564 29
            case 'datetime':
565 26
            case 'time':
566 25
            case 'date':
567 6
                if ($value === '') {
568 1
                    return null;
569
                }
570
571 5
                if ($value instanceof Datetime) {
572 4
                    return $value;
573
                }
574
575 5
                if (is_int($value)) {
576 5
                    $dateTime = new DateTime();
577 5
                    $dateTime->setTimestamp($value);
578 5
                    return $dateTime;
579
                }
580
581 4
                if (is_string($value)) {
582 4
                    return new DateTime($value);
583
                }
584
585
                break;
586
            default:
587 24
                break;
588
        }
589
590 42
        return $value;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $value; (boolean|string|integer|double|object|array) is incompatible with the return type documented by DoctrineModule\Stdlib\Hy...::handleTypeConversions of type DateTime|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...
591
    }
592
593
    /**
594
     * Find an object by a given target class and identifier
595
     *
596
     * @param  mixed   $identifiers
597
     * @param  string  $targetClass
598
     *
599
     * @return object|null
600
     */
601 25
    protected function find($identifiers, $targetClass)
602
    {
603 25
        if ($identifiers instanceof $targetClass) {
604 2
            return $identifiers;
605
        }
606
607 23
        if ($this->isNullIdentifier($identifiers)) {
608 4
            return null;
609
        }
610
611 19
        return $this->objectManager->find($targetClass, $identifiers);
612
    }
613
614
    /**
615
     * Verifies if a provided identifier is to be considered null
616
     *
617
     * @param  mixed $identifier
618
     *
619
     * @return bool
620
     */
621 23
    private function isNullIdentifier($identifier)
622
    {
623 23
        if (null === $identifier) {
624 4
            return true;
625
        }
626
627 19
        if ($identifier instanceof Traversable || is_array($identifier)) {
628 16
            $nonNullIdentifiers = array_filter(
629 16
                ArrayUtils::iteratorToArray($identifier),
630
                function ($value) {
631 16
                    return null !== $value;
632 16
                }
633
            );
634
635 16
            return empty($nonNullIdentifiers);
636
        }
637
638 3
        return false;
639
    }
640
641
    /**
642
     * Applies the naming strategy if there is one set
643
     *
644
     * @param string $field
645
     *
646
     * @return string
647
     */
648 54
    protected function computeHydrateFieldName($field)
649
    {
650 54
        if ($this->hasNamingStrategy()) {
651 2
            $field = $this->getNamingStrategy()->hydrate($field);
652
        }
653 54
        return $field;
654
    }
655
656
    /**
657
     * Applies the naming strategy if there is one set
658
     *
659
     * @param string $field
660
     *
661
     * @return string
662
     */
663 17
    protected function computeExtractFieldName($field)
664
    {
665 17
        if ($this->hasNamingStrategy()) {
666 2
            $field = $this->getNamingStrategy()->extract($field);
667
        }
668 17
        return $field;
669
    }
670
}
671