Completed
Push — master ( eb6033...4329f6 )
by Gianluca
10s
created

DoctrineObject::extract()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 10
ccs 5
cts 5
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 5
nc 2
nop 1
crap 2
1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace DoctrineModule\Stdlib\Hydrator;
21
22
use DateTime;
23
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
24
use Doctrine\Common\Persistence\ObjectManager;
25
use Doctrine\Common\Util\Inflector;
26
use InvalidArgumentException;
27
use RuntimeException;
28
use Traversable;
29
use Zend\Stdlib\ArrayUtils;
30
use Zend\Hydrator\AbstractHydrator;
31
use Zend\Hydrator\Filter\FilterProviderInterface;
32
33
/**
34
 * This hydrator has been completely refactored for DoctrineModule 0.7.0. It provides an easy and powerful way
35
 * of extracting/hydrator objects in Doctrine, by handling most associations types.
36
 *
37
 * Starting from DoctrineModule 0.8.0, the hydrator can be used multiple times with different objects
38
 *
39
 * @license MIT
40
 * @link    http://www.doctrine-project.org/
41
 * @since   0.7.0
42
 * @author  Michael Gallego <[email protected]>
43
 */
44
class DoctrineObject extends AbstractHydrator
45
{
46
    /**
47
     * @var ObjectManager
48
     */
49
    protected $objectManager;
50
51
    /**
52
     * @var ClassMetadata
53
     */
54
    protected $metadata;
55
56
    /**
57
     * @var bool
58
     */
59
    protected $byValue = true;
60
61
62
    /**
63
     * Constructor
64
     *
65
     * @param ObjectManager $objectManager The ObjectManager to use
66
     * @param bool          $byValue       If set to true, hydrator will always use entity's public API
67
     */
68 55
    public function __construct(ObjectManager $objectManager, $byValue = true)
69
    {
70 55
        parent::__construct();
71
72 55
        $this->objectManager = $objectManager;
73 55
        $this->byValue       = (bool) $byValue;
74 55
    }
75
76
    /**
77
     * Extract values from an object
78
     *
79
     * @param  object $object
80
     * @return array
81
     */
82 17
    public function extract($object)
83
    {
84 17
        $this->prepare($object);
85
86 17
        if ($this->byValue) {
87 10
            return $this->extractByValue($object);
88
        }
89
90 7
        return $this->extractByReference($object);
91
    }
92
93
    /**
94
     * Hydrate $object with the provided $data.
95
     *
96
     * @param  array  $data
97
     * @param  object $object
98
     * @return object
99
     */
100 39
    public function hydrate(array $data, $object)
101
    {
102 39
        $this->prepare($object);
103
104 39
        if ($this->byValue) {
105 26
            return $this->hydrateByValue($data, $object);
106
        }
107
108 13
        return $this->hydrateByReference($data, $object);
109
    }
110
111
    /**
112
     * Prepare the hydrator by adding strategies to every collection valued associations
113
     *
114
     * @param  object $object
115
     * @return void
116
     */
117 55
    protected function prepare($object)
118
    {
119 55
        $this->metadata = $this->objectManager->getClassMetadata(get_class($object));
120 55
        $this->prepareStrategies();
121 55
    }
122
123
    /**
124
     * Prepare strategies before the hydrator is used
125
     *
126
     * @throws \InvalidArgumentException
127
     * @return void
128
     */
129 55
    protected function prepareStrategies()
130
    {
131 55
        $associations = $this->metadata->getAssociationNames();
132
133 55
        foreach ($associations as $association) {
134 34
            if ($this->metadata->isCollectionValuedAssociation($association)) {
135
                // Add a strategy if the association has none set by user
136 21
                if (!$this->hasStrategy($association)) {
137 19
                    if ($this->byValue) {
138 12
                        $this->addStrategy($association, new Strategy\AllowRemoveByValue());
139 12
                    } else {
140 7
                        $this->addStrategy($association, new Strategy\AllowRemoveByReference());
141 2
                    }
142 19
                }
143
144 21
                $strategy = $this->getStrategy($association);
145
146 21
                if (!$strategy instanceof Strategy\AbstractCollectionStrategy) {
147
                    throw new InvalidArgumentException(
148
                        sprintf(
149
                            'Strategies used for collections valued associations must inherit from '
150
                            . 'Strategy\AbstractCollectionStrategy, %s given',
151
                            get_class($strategy)
152
                        )
153
                    );
154
                }
155
156 21
                $strategy->setCollectionName($association)
157 23
                         ->setClassMetadata($this->metadata);
158 21
            }
159 55
        }
160 55
    }
161
162
    /**
163
     * Extract values from an object using a by-value logic (this means that it uses the entity
164
     * API, in this case, getters)
165
     *
166
     * @param  object $object
167
     * @throws RuntimeException
168
     * @return array
169
     */
170 10
    protected function extractByValue($object)
171
    {
172 10
        $fieldNames = array_merge($this->metadata->getFieldNames(), $this->metadata->getAssociationNames());
173 10
        $methods    = get_class_methods($object);
174
        $filter     = $object instanceof FilterProviderInterface
175 10
            ? $object->getFilter()
176 10
            : $this->filterComposite;
177
178 10
        $data = array();
179 10
        foreach ($fieldNames as $fieldName) {
180 10
            if ($filter && !$filter->filter($fieldName)) {
181 1
                continue;
182
            }
183
184 10
            $getter = 'get' . Inflector::classify($fieldName);
185 10
            $isser  = 'is' . Inflector::classify($fieldName);
186
187 10
            $dataFieldName = $this->computeExtractFieldName($fieldName);
188 10
            if (in_array($getter, $methods)) {
189 10
                $data[$dataFieldName] = $this->extractValue($fieldName, $object->$getter(), $object);
190 10
            } elseif (in_array($isser, $methods)) {
191 1
                $data[$dataFieldName] = $this->extractValue($fieldName, $object->$isser(), $object);
192 2
            } elseif (substr($fieldName, 0, 2) === 'is'
193 1
                && ctype_upper(substr($fieldName, 2, 1))
194 1
                && in_array($fieldName, $methods)) {
195 1
                $data[$dataFieldName] = $this->extractValue($fieldName, $object->$fieldName(), $object);
196 1
            }
197
198
            // Unknown fields are ignored
199 10
        }
200
201 10
        return $data;
202
    }
203
204
    /**
205
     * Extract values from an object using a by-reference logic (this means that values are
206
     * directly fetched without using the public API of the entity, in this case, getters)
207
     *
208
     * @param  object $object
209
     * @return array
210
     */
211 7
    protected function extractByReference($object)
212
    {
213 7
        $fieldNames = array_merge($this->metadata->getFieldNames(), $this->metadata->getAssociationNames());
214 7
        $refl       = $this->metadata->getReflectionClass();
215
        $filter     = $object instanceof FilterProviderInterface
216 7
            ? $object->getFilter()
217 7
            : $this->filterComposite;
218
219 7
        $data = array();
220 7
        foreach ($fieldNames as $fieldName) {
221 7
            if ($filter && !$filter->filter($fieldName)) {
222 1
                continue;
223
            }
224 7
            $reflProperty = $refl->getProperty($fieldName);
225 7
            $reflProperty->setAccessible(true);
226
227 7
            $dataFieldName        = $this->computeExtractFieldName($fieldName);
228 7
            $data[$dataFieldName] = $this->extractValue($fieldName, $reflProperty->getValue($object), $object);
229 7
        }
230
231 7
        return $data;
232
    }
233
234
    /**
235
     * Hydrate the object using a by-value logic (this means that it uses the entity API, in this
236
     * case, setters)
237
     *
238
     * @param  array  $data
239
     * @param  object $object
240
     * @throws RuntimeException
241
     * @return object
242
     */
243 27
    protected function hydrateByValue(array $data, $object)
244
    {
245 27
        $tryObject = $this->tryConvertArrayToObject($data, $object);
246 27
        $metadata  = $this->metadata;
247
248 27
        if (is_object($tryObject)) {
249 25
            $object = $tryObject;
250 25
        }
251
252 27
        foreach ($data as $field => $value) {
253 27
            $field  = $this->computeHydrateFieldName($field);
254 27
            $value  = $this->handleTypeConversions($value, $metadata->getTypeOfField($field));
255 27
            $setter = 'set' . Inflector::classify($field);
256
257 27
            if ($metadata->hasAssociation($field)) {
258 18
                $target = $metadata->getAssociationTargetClass($field);
259
260 18
                if ($metadata->isSingleValuedAssociation($field)) {
261 7
                    if (! method_exists($object, $setter)) {
262
                        continue;
263
                    }
264
265 7
                    $value = $this->toOne($target, $this->hydrateValue($field, $value, $data));
266
267 7
                    if (null === $value
268 7
                        && !current($metadata->getReflectionClass()->getMethod($setter)->getParameters())->allowsNull()
269 7
                    ) {
270 1
                        continue;
271
                    }
272
273 6
                    $object->$setter($value);
274 17
                } elseif ($metadata->isCollectionValuedAssociation($field)) {
275 11
                    $this->toMany($object, $field, $target, $value);
276 11
                }
277 17
            } else {
278 12
                if (! method_exists($object, $setter)) {
279
                    continue;
280
                }
281
282 12
                $object->$setter($this->hydrateValue($field, $value, $data));
283
            }
284 27
        }
285
286 27
        return $object;
287
    }
288
289
    /**
290
     * Hydrate the object using a by-reference logic (this means that values are modified directly without
291
     * using the public API, in this case setters, and hence override any logic that could be done in those
292
     * setters)
293
     *
294
     * @param  array  $data
295
     * @param  object $object
296
     * @return object
297
     */
298 14
    protected function hydrateByReference(array $data, $object)
299
    {
300 14
        $tryObject = $this->tryConvertArrayToObject($data, $object);
301 14
        $metadata  = $this->metadata;
302 14
        $refl      = $metadata->getReflectionClass();
303
304 14
        if (is_object($tryObject)) {
305 13
            $object = $tryObject;
306 13
        }
307
308 14
        foreach ($data as $field => $value) {
309 14
            $field = $this->computeHydrateFieldName($field);
310
311
            // Ignore unknown fields
312 14
            if (!$refl->hasProperty($field)) {
313
                continue;
314
            }
315
316 14
            $value        = $this->handleTypeConversions($value, $metadata->getTypeOfField($field));
317 14
            $reflProperty = $refl->getProperty($field);
318 14
            $reflProperty->setAccessible(true);
319
320 14
            if ($metadata->hasAssociation($field)) {
321 10
                $target = $metadata->getAssociationTargetClass($field);
322
323 10
                if ($metadata->isSingleValuedAssociation($field)) {
324 4
                    $value = $this->toOne($target, $this->hydrateValue($field, $value, $data));
325 4
                    $reflProperty->setValue($object, $value);
326 10
                } elseif ($metadata->isCollectionValuedAssociation($field)) {
327 6
                    $this->toMany($object, $field, $target, $value);
328 6
                }
329 10
            } else {
330 5
                $reflProperty->setValue($object, $this->hydrateValue($field, $value, $data));
331
            }
332 14
        }
333
334 14
        return $object;
335
    }
336
337
    /**
338
     * This function tries, given an array of data, to convert it to an object if the given array contains
339
     * an identifier for the object. This is useful in a context of updating existing entities, without ugly
340
     * tricks like setting manually the existing id directly into the entity
341
     *
342
     * @param  array  $data   The data that may contain identifiers keys
343
     * @param  object $object
344
     * @return object
345
     */
346 39
    protected function tryConvertArrayToObject($data, $object)
347
    {
348 39
        $metadata         = $this->metadata;
349 39
        $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...
350 39
        $identifierValues = array();
351
352 39
        if (empty($identifierNames)) {
353 28
            return $object;
354
        }
355
356 11
        foreach ($identifierNames as $identifierName) {
357 11
            if (!isset($data[$identifierName])) {
358 8
                return $object;
359
            }
360
361 3
            $identifierValues[$identifierName] = $data[$identifierName];
362 3
        }
363
364 3
        return $this->find($identifierValues, $metadata->getName());
365
    }
366
367
    /**
368
     * Handle ToOne associations
369
     *
370
     * When $value is an array but is not the $target's identifiers, $value is
371
     * most likely an array of fieldset data. The identifiers will be determined
372
     * and a target instance will be initialized and then hydrated. The hydrated
373
     * target will be returned.
374
     *
375
     * @param  string $target
376
     * @param  mixed  $value
377
     * @return object
378
     */
379 11
    protected function toOne($target, $value)
380
    {
381 11
        $metadata = $this->objectManager->getClassMetadata($target);
382
383 11
        if (is_array($value) && array_keys($value) != $metadata->getIdentifier()) {
384
            // $value is most likely an array of fieldset data
385 1
            $identifiers = array_intersect_key(
386 1
                $value,
387 1
                array_flip($metadata->getIdentifier())
388 1
            );
389 1
            $object      = $this->find($identifiers, $target) ?: new $target;
390 1
            return $this->hydrate($value, $object);
391
        }
392
393 10
        return $this->find($value, $target);
394
    }
395
396
    /**
397
     * Handle ToMany associations. In proper Doctrine design, Collections should not be swapped, so
398
     * collections are always handled by reference. Internally, every collection is handled using specials
399
     * strategies that inherit from AbstractCollectionStrategy class, and that add or remove elements but without
400
     * changing the collection of the object
401
     *
402
     * @param  object $object
403
     * @param  mixed  $collectionName
404
     * @param  string $target
405
     * @param  mixed  $values
406
     *
407
     * @throws \InvalidArgumentException
408
     *
409
     * @return void
410
     */
411 17
    protected function toMany($object, $collectionName, $target, $values)
412
    {
413 17
        $metadata   = $this->objectManager->getClassMetadata(ltrim($target, '\\'));
414 17
        $identifier = $metadata->getIdentifier();
415
416 17
        if (!is_array($values) && !$values instanceof Traversable) {
417
            $values = (array)$values;
418
        }
419
420 17
        $collection = array();
421
422
        // If the collection contains identifiers, fetch the objects from database
423 17
        foreach ($values as $value) {
424 17
            if ($value instanceof $target) {
425
                // assumes modifications have already taken place in object
426 7
                $collection[] = $value;
427 7
                continue;
428 10
            } elseif (empty($value)) {
429
                // assumes no id and retrieves new $target
430 1
                $collection[] = $this->find($value, $target);
431 1
                continue;
432
            }
433
434 9
            $find = array();
435 9
            if (is_array($identifier)) {
436 9
                foreach ($identifier as $field) {
437 9
                    switch (gettype($value)) {
438 9
                        case 'object':
439 1
                            $getter = 'get' . ucfirst($field);
440 1
                            if (method_exists($value, $getter)) {
441
                                $find[$field] = $value->$getter();
442 1
                            } elseif (property_exists($value, $field)) {
443 1
                                $find[$field] = $value->$field;
444 1
                            }
445 1
                            break;
446 8
                        case 'array':
447 5
                            if (array_key_exists($field, $value) && $value[$field] != null) {
448 5
                                $find[$field] = $value[$field];
449 5
                                unset($value[$field]); // removed identifier from persistable data
450 5
                            }
451 5
                            break;
452 3
                        default:
453 3
                            $find[$field] = $value;
454 3
                            break;
455 9
                    }
456 9
                }
457 9
            }
458
459 9
            if (!empty($find) && $found = $this->find($find, $target)) {
460 9
                $collection[] = (is_array($value)) ? $this->hydrate($value, $found) : $found;
461 9
            } else {
462
                $collection[] = (is_array($value)) ? $this->hydrate($value, new $target) : new $target;
463
            }
464 17
        }
465
466 17
        $collection = array_filter(
467 17
            $collection,
468
            function ($item) {
469 17
                return null !== $item;
470
            }
471 17
        );
472
473
        // Set the object so that the strategy can extract the Collection from it
474
475
        /** @var \DoctrineModule\Stdlib\Hydrator\Strategy\AbstractCollectionStrategy $collectionStrategy */
476 17
        $collectionStrategy = $this->getStrategy($collectionName);
477 17
        $collectionStrategy->setObject($object);
478
479
        // We could directly call hydrate method from the strategy, but if people want to override
480
        // hydrateValue function, they can do it and do their own stuff
481 17
        $this->hydrateValue($collectionName, $collection, $values);
0 ignored issues
show
Bug introduced by
It seems like $values defined by parameter $values on line 411 can also be of type object<Traversable>; however, Zend\Hydrator\AbstractHydrator::hydrateValue() does only seem to accept array|null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
482 17
    }
483
484
    /**
485
     * Handle various type conversions that should be supported natively by Doctrine (like DateTime)
486
     *
487
     * @param  mixed  $value
488
     * @param  string $typeOfField
489
     * @return DateTime
490
     */
491 39
    protected function handleTypeConversions($value, $typeOfField)
492
    {
493
        switch ($typeOfField) {
494 39
            case 'datetimetz':
495 39
            case 'datetime':
496 39
            case 'time':
497 39
            case 'date':
498 2
                if ('' === $value) {
499 1
                    return null;
500
                }
501
502 1
                if (is_int($value)) {
503 1
                    $dateTime = new DateTime();
504 1
                    $dateTime->setTimestamp($value);
505 1
                    $value = $dateTime;
506 1
                } elseif (is_string($value)) {
507
                    $value = new DateTime($value);
508
                }
509
510 1
                break;
511 37
            default:
512 37
        }
513
514 38
        return $value;
515
    }
516
517
    /**
518
     * Find an object by a given target class and identifier
519
     *
520
     * @param  mixed   $identifiers
521
     * @param  string  $targetClass
522
     *
523
     * @return object|null
524
     */
525 24
    protected function find($identifiers, $targetClass)
526
    {
527 24
        if ($identifiers instanceof $targetClass) {
528 2
            return $identifiers;
529
        }
530
531 22
        if ($this->isNullIdentifier($identifiers)) {
532 4
            return null;
533
        }
534
535 18
        return $this->objectManager->find($targetClass, $identifiers);
536
    }
537
538
    /**
539
     * Verifies if a provided identifier is to be considered null
540
     *
541
     * @param  mixed $identifier
542
     *
543
     * @return bool
544
     */
545 22
    private function isNullIdentifier($identifier)
546
    {
547 22
        if (null === $identifier) {
548 4
            return true;
549
        }
550
551 18
        if ($identifier instanceof Traversable || is_array($identifier)) {
552 15
            $nonNullIdentifiers = array_filter(
553 15
                ArrayUtils::iteratorToArray($identifier),
554 15
                function ($value) {
555 15
                    return null !== $value;
556
                }
557 15
            );
558
559 15
            return empty($nonNullIdentifiers);
560
        }
561
562 3
        return false;
563
    }
564
565
    /**
566
     * Applies the naming strategy if there is one set
567
     *
568
     * @param string $field
569
     *
570
     * @return string
571
     */
572 39
    protected function computeHydrateFieldName($field)
573
    {
574 39
        if ($this->hasNamingStrategy()) {
575 2
            $field = $this->getNamingStrategy()->hydrate($field);
576 2
        }
577 39
        return $field;
578
    }
579
580
    /**
581
     * Applies the naming strategy if there is one set
582
     *
583
     * @param string $field
584
     *
585
     * @return string
586
     */
587 17
    protected function computeExtractFieldName($field)
588
    {
589 17
        if ($this->hasNamingStrategy()) {
590 2
            $field = $this->getNamingStrategy()->extract($field);
591 2
        }
592 17
        return $field;
593
    }
594
}
595