ArrayHydrator   A
last analyzed

Complexity

Total Complexity 37

Size/Duplication

Total Lines 262
Duplicated Lines 2.29 %

Coupling/Cohesion

Components 1
Dependencies 3

Importance

Changes 0
Metric Value
wmc 37
lcom 1
cbo 3
dl 6
loc 262
rs 9.44
c 0
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A hydrate() 0 13 4
A setHydrateAssociationReferences() 0 4 1
A setHydrateId() 0 4 1
A setHydrateBy() 0 4 1
B hydrateProperties() 0 31 7
A hydrateAssociations() 6 18 5
A getAssociatedId() 0 19 6
A hydrateToOneAssociation() 0 11 2
A hydrateToManyAssociation() 0 19 5
A setProperty() 0 8 2
A fetchAssociationEntity() 0 8 2

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

1
<?php
2
namespace pmill\Doctrine\Hydrator;
3
4
use Doctrine\DBAL\Types\Type;
5
use Doctrine\ORM\EntityManagerInterface;
6
use Doctrine\ORM\Mapping\ClassMetadataInfo;
7
use Exception;
8
9
class ArrayHydrator
10
{
11
    /**
12
     * The keys in the data array are entity field names
13
     */
14
    const HYDRATE_BY_FIELD = 1;
15
16
    /**
17
     * The keys in the data array are database column names
18
     */
19
    const HYDRATE_BY_COLUMN = 2;
20
21
    /**
22
     * @var EntityManagerInterface
23
     */
24
    protected $entityManager;
25
26
    /**
27
     * If true, then associations are filled only with reference proxies. This is faster than querying them from
28
     * database, but if the associated entity does not really exist, it will cause:
29
     * * The insert/update to fail, if there is a foreign key defined in database
30
     * * The record ind database also pointing to a non-existing record
31
     *
32
     * @var bool
33
     */
34
    protected $hydrateAssociationReferences = true;
35
36
    /**
37
     * Tells whether the input data array keys are entity field names or database column names
38
     *
39
     * @var int one of ArrayHydrator::HIDRATE_BY_* constants
40
     */
41
    protected $hydrateBy = self::HYDRATE_BY_FIELD;
42
43
    /**
44
     * If true, hydrate the primary key too. Useful if the primary key is not automatically generated by the database
45
     *
46
     * @var bool
47
     */
48
    protected $hydrateId = false;
49
50
    /**
51
     * @param EntityManagerInterface $entityManager
52
     */
53
    public function __construct(EntityManagerInterface $entityManager)
54
    {
55
        $this->entityManager = $entityManager;
56
    }
57
58
    /**
59
     * @param $entity
60
     * @param array $data
61
     * @return mixed|object
62
     * @throws Exception
63
     */
64
    public function hydrate($entity, array $data)
65
    {
66
        if (is_string($entity) && class_exists($entity)) {
67
            $entity = new $entity;
68
        }
69
        elseif (!is_object($entity)) {
70
            throw new Exception('Entity passed to ArrayHydrator::hydrate() must be a class name or entity object');
71
        }
72
73
        $entity = $this->hydrateProperties($entity, $data);
74
        $entity = $this->hydrateAssociations($entity, $data);
75
        return $entity;
76
    }
77
78
    /**
79
     * @param boolean $hydrateAssociationReferences
80
     */
81
    public function setHydrateAssociationReferences($hydrateAssociationReferences)
82
    {
83
        $this->hydrateAssociationReferences = $hydrateAssociationReferences;
84
    }
85
86
    /**
87
     * @param bool $hydrateId
88
     */
89
    public function setHydrateId($hydrateId)
90
    {
91
        $this->hydrateId = $hydrateId;
92
    }
93
94
    /**
95
     * @param int $hydrateBy
96
     */
97
    public function setHydrateBy($hydrateBy)
98
    {
99
        $this->hydrateBy = $hydrateBy;
100
    }
101
102
    /**
103
     * @param object $entity the doctrine entity
104
     * @param array $data
105
     * @return object
106
     */
107
    protected function hydrateProperties($entity, $data)
108
    {
109
        $reflectionObject = new \ReflectionObject($entity);
110
111
        $metaData = $this->entityManager->getClassMetadata(get_class($entity));
112
        
113
        $platform = $this->entityManager->getConnection()
114
                                        ->getDatabasePlatform();
115
116
        $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...
117
118
        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...
119
            $dataKey = $this->hydrateBy === self::HYDRATE_BY_FIELD ? $fieldName : $metaData->getColumnName($fieldName);
120
121
            if (array_key_exists($dataKey, $data) && !in_array($fieldName, $skipFields, true)) {
122
                $value = $data[$dataKey];
123
124
                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...
125
                    $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...
126
127
                    $type = Type::getType($fieldType);
128
129
                    $value = $type->convertToPHPValue($value, $platform);
130
                }
131
132
                $entity = $this->setProperty($entity, $fieldName, $value, $reflectionObject);
133
            }
134
        }
135
136
        return $entity;
137
    }
138
139
    /**
140
     * @param $entity
141
     * @param $data
142
     * @return mixed
143
     */
144
    protected function hydrateAssociations($entity, $data)
145
    {
146
        $metaData = $this->entityManager->getClassMetadata(get_class($entity));
147
        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...
148
            $associationData = $this->getAssociatedId($fieldName, $mapping, $data);
149
            if (!empty($associationData)) {
150 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...
151
                    $entity = $this->hydrateToOneAssociation($entity, $fieldName, $mapping, $associationData);
152
                }
153
154 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...
155
                    $entity = $this->hydrateToManyAssociation($entity, $fieldName, $mapping, $associationData);
156
                }
157
            }
158
        }
159
160
        return $entity;
161
    }
162
163
    /**
164
     * Retrieves the associated entity's id from $data
165
     *
166
     * @param string $fieldName name of field that stores the associated entity
167
     * @param array $mapping doctrine's association mapping array for the field
168
     * @param array $data the hydration data
169
     *
170
     * @return mixed null, if the association is not found
171
     */
172
    protected function getAssociatedId($fieldName, $mapping, $data)
173
    {
174
        if ($this->hydrateBy === self::HYDRATE_BY_FIELD) {
175
176
            return isset($data[$fieldName]) ? $data[$fieldName] : null;
177
        }
178
179
        // from this point it is self::HYDRATE_BY_COLUMN
180
        // we do not support compound foreign keys (yet)
181
        if (isset($mapping['joinColumns']) && count($mapping['joinColumns']) === 1) {
182
            $columnName = $mapping['joinColumns'][0]['name'];
183
184
            return isset($data[$columnName]) ? $data[$columnName] : null;
185
        }
186
187
        // If joinColumns does not exist, then this is not the owning side of an association
188
        // This should not happen with column based hydration
189
        return null;
190
    }
191
192
    /**
193
     * @param $entity
194
     * @param $propertyName
195
     * @param $mapping
196
     * @param $value
197
     * @return mixed
198
     */
199
    protected function hydrateToOneAssociation($entity, $propertyName, $mapping, $value)
200
    {
201
        $reflectionObject = new \ReflectionObject($entity);
202
203
        $toOneAssociationObject = $this->fetchAssociationEntity($mapping['targetEntity'], $value);
204
        if (!is_null($toOneAssociationObject)) {
205
            $entity = $this->setProperty($entity, $propertyName, $toOneAssociationObject, $reflectionObject);
206
        }
207
208
        return $entity;
209
    }
210
211
    /**
212
     * @param $entity
213
     * @param $propertyName
214
     * @param $mapping
215
     * @param $value
216
     * @return mixed
217
     */
218
    protected function hydrateToManyAssociation($entity, $propertyName, $mapping, $value)
219
    {
220
        $reflectionObject = new \ReflectionObject($entity);
221
        $values = is_array($value) ? $value : [$value];
222
223
        $assocationObjects = [];
224
        foreach ($values as $value) {
225
            if (is_array($value)) {
226
                $assocationObjects[] = $this->hydrate($mapping['targetEntity'], $value);
227
            }
228
            elseif ($associationObject = $this->fetchAssociationEntity($mapping['targetEntity'], $value)) {
229
                $assocationObjects[] = $associationObject;
230
            }
231
        }
232
233
        $entity = $this->setProperty($entity, $propertyName, $assocationObjects, $reflectionObject);
234
235
        return $entity;
236
    }
237
238
    /**
239
     * @param $entity
240
     * @param $propertyName
241
     * @param $value
242
     * @param null $reflectionObject
243
     * @return mixed
244
     */
245
    protected function setProperty($entity, $propertyName, $value, $reflectionObject = null)
246
    {
247
        $reflectionObject = is_null($reflectionObject) ? new \ReflectionObject($entity) : $reflectionObject;
248
        $property = $reflectionObject->getProperty($propertyName);
249
        $property->setAccessible(true);
250
        $property->setValue($entity, $value);
251
        return $entity;
252
    }
253
254
    /**
255
     * @param $className
256
     * @param $id
257
     * @return bool|\Doctrine\Common\Proxy\Proxy|null|object
258
     * @throws \Doctrine\ORM\ORMException
259
     * @throws \Doctrine\ORM\OptimisticLockException
260
     * @throws \Doctrine\ORM\TransactionRequiredException
261
     */
262
    protected function fetchAssociationEntity($className, $id)
263
    {
264
        if ($this->hydrateAssociationReferences) {
265
            return $this->entityManager->getReference($className, $id);
266
        }
267
268
        return $this->entityManager->find($className, $id);
269
    }
270
}
271