Completed
Pull Request — master (#13)
by Anatoly
01:22
created

ArrayHydrator::hydrateToManyAssociation()   B

Complexity

Conditions 7
Paths 16

Size

Total Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 27
rs 8.5546
c 0
b 0
f 0
cc 7
nc 16
nop 4
1
<?php
2
namespace pmill\Doctrine\Hydrator;
3
4
use ArrayAccess;
5
use Doctrine\DBAL\Types\Type;
6
use Doctrine\ORM\EntityManagerInterface;
7
use Doctrine\ORM\Mapping\ClassMetadataInfo;
8
use Exception;
9
10
class ArrayHydrator
11
{
12
    /**
13
     * The keys in the data array are entity field names
14
     */
15
    const HYDRATE_BY_FIELD = 1;
16
17
    /**
18
     * The keys in the data array are database column names
19
     */
20
    const HYDRATE_BY_COLUMN = 2;
21
22
    /**
23
     * @var EntityManagerInterface
24
     */
25
    protected $entityManager;
26
27
    /**
28
     * If true, then associations are filled only with reference proxies. This is faster than querying them from
29
     * database, but if the associated entity does not really exist, it will cause:
30
     * * The insert/update to fail, if there is a foreign key defined in database
31
     * * The record ind database also pointing to a non-existing record
32
     *
33
     * @var bool
34
     */
35
    protected $hydrateAssociationReferences = true;
36
37
    /**
38
     * Tells whether the input data array keys are entity field names or database column names
39
     *
40
     * @var int one of ArrayHydrator::HIDRATE_BY_* constants
41
     */
42
    protected $hydrateBy = self::HYDRATE_BY_FIELD;
43
44
    /**
45
     * If true, hydrate the primary key too. Useful if the primary key is not automatically generated by the database
46
     *
47
     * @var bool
48
     */
49
    protected $hydrateId = false;
50
51
    /**
52
     * @param EntityManagerInterface $entityManager
53
     */
54
    public function __construct(EntityManagerInterface $entityManager)
55
    {
56
        $this->entityManager = $entityManager;
57
    }
58
59
    /**
60
     * @param $entity
61
     * @param array $data
62
     * @return mixed|object
63
     * @throws Exception
64
     */
65
    public function hydrate($entity, array $data)
66
    {
67
        if (is_string($entity) && class_exists($entity)) {
68
            $entity = new $entity;
69
        }
70
        elseif (!is_object($entity)) {
71
            throw new Exception('Entity passed to ArrayHydrator::hydrate() must be a class name or entity object');
72
        }
73
74
        $entity = $this->hydrateProperties($entity, $data);
75
        $entity = $this->hydrateAssociations($entity, $data);
76
        return $entity;
77
    }
78
79
    /**
80
     * @param boolean $hydrateAssociationReferences
81
     */
82
    public function setHydrateAssociationReferences($hydrateAssociationReferences)
83
    {
84
        $this->hydrateAssociationReferences = $hydrateAssociationReferences;
85
    }
86
87
    /**
88
     * @param bool $hydrateId
89
     */
90
    public function setHydrateId($hydrateId)
91
    {
92
        $this->hydrateId = $hydrateId;
93
    }
94
95
    /**
96
     * @param int $hydrateBy
97
     */
98
    public function setHydrateBy($hydrateBy)
99
    {
100
        $this->hydrateBy = $hydrateBy;
101
    }
102
103
    /**
104
     * @param object $entity the doctrine entity
105
     * @param array $data
106
     * @return object
107
     */
108
    protected function hydrateProperties($entity, $data)
109
    {
110
        $reflectionObject = new \ReflectionObject($entity);
111
112
        $metaData = $this->entityManager->getClassMetadata(get_class($entity));
113
        
114
        $platform = $this->entityManager->getConnection()
115
                                        ->getDatabasePlatform();
116
117
        $skipFields = $this->hydrateId ? [] : $metaData->identifier;
0 ignored issues
show
Bug introduced by
Accessing identifier on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
118
119
        foreach ($metaData->fieldNames as $fieldName) {
0 ignored issues
show
Bug introduced by
Accessing fieldNames on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
120
            $dataKey = $this->hydrateBy === self::HYDRATE_BY_FIELD ? $fieldName : $metaData->getColumnName($fieldName);
121
122
            if (array_key_exists($dataKey, $data) && !in_array($fieldName, $skipFields, true)) {
123
                $value = $data[$dataKey];
124
125
                if (array_key_exists('type', $metaData->fieldMappings[$fieldName])) {
0 ignored issues
show
Bug introduced by
Accessing fieldMappings on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
126
                    $fieldType = $metaData->fieldMappings[$fieldName]['type'];
0 ignored issues
show
Bug introduced by
Accessing fieldMappings on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
127
128
                    $type = Type::getType($fieldType);
129
130
                    $value = $type->convertToPHPValue($value, $platform);
131
                }
132
133
                $entity = $this->setProperty($entity, $fieldName, $value, $reflectionObject);
134
            }
135
        }
136
137
        return $entity;
138
    }
139
140
    /**
141
     * @param $entity
142
     * @param $data
143
     * @return mixed
144
     */
145
    protected function hydrateAssociations($entity, $data)
146
    {
147
        $metaData = $this->entityManager->getClassMetadata(get_class($entity));
148
        foreach ($metaData->associationMappings as $fieldName => $mapping) {
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
149
            $associationData = $this->getAssociatedId($fieldName, $mapping, $data);
150
            if (!empty($associationData)) {
151 View Code Duplication
                if (in_array($mapping['type'], [ClassMetadataInfo::ONE_TO_ONE, ClassMetadataInfo::MANY_TO_ONE])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
152
                    $entity = $this->hydrateToOneAssociation($entity, $fieldName, $mapping, $associationData);
153
                }
154
155 View Code Duplication
                if (in_array($mapping['type'], [ClassMetadataInfo::ONE_TO_MANY, ClassMetadataInfo::MANY_TO_MANY])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
156
                    $entity = $this->hydrateToManyAssociation($entity, $fieldName, $mapping, $associationData);
157
                }
158
            }
159
        }
160
161
        return $entity;
162
    }
163
164
    /**
165
     * Retrieves the associated entity's id from $data
166
     *
167
     * @param string $fieldName name of field that stores the associated entity
168
     * @param array $mapping doctrine's association mapping array for the field
169
     * @param array $data the hydration data
170
     *
171
     * @return mixed null, if the association is not found
172
     */
173
    protected function getAssociatedId($fieldName, $mapping, $data)
174
    {
175
        if ($this->hydrateBy === self::HYDRATE_BY_FIELD) {
176
177
            return isset($data[$fieldName]) ? $data[$fieldName] : null;
178
        }
179
180
        // from this point it is self::HYDRATE_BY_COLUMN
181
        // we do not support compound foreign keys (yet)
182
        if (isset($mapping['joinColumns']) && count($mapping['joinColumns']) === 1) {
183
            $columnName = $mapping['joinColumns'][0]['name'];
184
185
            return isset($data[$columnName]) ? $data[$columnName] : null;
186
        }
187
188
        // If joinColumns does not exist, then this is not the owning side of an association
189
        // This should not happen with column based hydration
190
        return null;
191
    }
192
193
    /**
194
     * @param $entity
195
     * @param $propertyName
196
     * @param $mapping
197
     * @param $value
198
     * @return mixed
199
     */
200
    protected function hydrateToOneAssociation($entity, $propertyName, $mapping, $value)
201
    {
202
        $reflectionObject = new \ReflectionObject($entity);
203
204
        $toOneAssociationObject = $this->fetchAssociationEntity($mapping['targetEntity'], $value);
205
        if (!is_null($toOneAssociationObject)) {
206
            $entity = $this->setProperty($entity, $propertyName, $toOneAssociationObject, $reflectionObject);
207
        }
208
209
        return $entity;
210
    }
211
212
    /**
213
     * @param $entity
214
     * @param $propertyName
215
     * @param $mapping
216
     * @param $value
217
     * @return mixed
218
     */
219
    protected function hydrateToManyAssociation($entity, $propertyName, $mapping, $value)
220
    {
221
        $reflectionObject = new \ReflectionObject($entity);
222
        $values = is_array($value) ? $value : [$value];
223
224
        $propertyRef = $reflectionObject->getProperty($propertyName);
225
        $propertyRef->setAccessible(true);
226
        $propertyValue = $propertyRef->getValue($entity);
227
228
        $assocationObjects = [];
229
        if (is_array($propertyValue) || $propertyValue instanceof ArrayAccess) {
230
            $assocationObjects = $propertyValue;
231
        }
232
233
        foreach ($values as $value) {
234
            if (is_array($value)) {
235
                $assocationObjects[] = $this->hydrate($mapping['targetEntity'], $value);
236
            }
237
            elseif ($associationObject = $this->fetchAssociationEntity($mapping['targetEntity'], $value)) {
238
                $assocationObjects[] = $associationObject;
239
            }
240
        }
241
242
        $entity = $this->setProperty($entity, $propertyName, $assocationObjects, $reflectionObject);
243
244
        return $entity;
245
    }
246
247
    /**
248
     * @param $entity
249
     * @param $propertyName
250
     * @param $value
251
     * @param null $reflectionObject
252
     * @return mixed
253
     */
254
    protected function setProperty($entity, $propertyName, $value, $reflectionObject = null)
255
    {
256
        $reflectionObject = is_null($reflectionObject) ? new \ReflectionObject($entity) : $reflectionObject;
257
        $property = $reflectionObject->getProperty($propertyName);
258
        $property->setAccessible(true);
259
        $property->setValue($entity, $value);
260
        return $entity;
261
    }
262
263
    /**
264
     * @param $className
265
     * @param $id
266
     * @return bool|\Doctrine\Common\Proxy\Proxy|null|object
267
     * @throws \Doctrine\ORM\ORMException
268
     * @throws \Doctrine\ORM\OptimisticLockException
269
     * @throws \Doctrine\ORM\TransactionRequiredException
270
     */
271
    protected function fetchAssociationEntity($className, $id)
272
    {
273
        if ($this->hydrateAssociationReferences) {
274
            return $this->entityManager->getReference($className, $id);
275
        }
276
277
        return $this->entityManager->find($className, $id);
278
    }
279
}
280