Completed
Push — master ( 35cd7c...bbee21 )
by David de
08:13 queued 02:08
created

DoctrineWriter::updateObject()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 22
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 6.0061

Importance

Changes 0
Metric Value
dl 0
loc 22
ccs 17
cts 18
cp 0.9444
rs 8.6737
c 0
b 0
f 0
cc 6
eloc 13
nc 7
nop 2
crap 6.0061
1
<?php
2
3
namespace Port\Doctrine;
4
5
use Port\Doctrine\Exception\UnsupportedDatabaseTypeException;
6
use Port\Writer;
7
use Doctrine\Common\Util\Inflector;
8
use Doctrine\DBAL\Logging\SQLLogger;
9
use Doctrine\Common\Persistence\ObjectManager;
10
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
11
12
/**
13
 * A bulk Doctrine writer
14
 *
15
 * See also the {@link http://www.doctrine-project.org/docs/orm/2.1/en/reference/batch-processing.html Doctrine documentation}
16
 * on batch processing.
17
 *
18
 * @author David de Boer <[email protected]>
19
 */
20
class DoctrineWriter implements Writer, Writer\FlushableWriter
21
{
22
    /**
23
     * Doctrine object manager
24
     *
25
     * @var ObjectManager
26
     */
27
    protected $objectManager;
28
29
    /**
30
     * Fully qualified model name
31
     *
32
     * @var string
33
     */
34
    protected $objectName;
35
36
    /**
37
     * Doctrine object repository
38
     *
39
     * @var ObjectRepository
40
     */
41
    protected $objectRepository;
42
43
    /**
44
     * @var ClassMetadata
45
     */
46
    protected $objectMetadata;
47
48
    /**
49
     * Original Doctrine logger
50
     *
51
     * @var SQLLogger
52
     */
53
    protected $originalLogger;
54
55
    /**
56
     * Whether to truncate the table first
57
     *
58
     * @var boolean
59
     */
60
    protected $truncate = true;
61
62
    /**
63
     * List of fields used to lookup an object
64
     *
65
     * @var array
66
     */
67
    protected $lookupFields = [];
68
69
    /**
70
     * Method used for looking up the item
71
     *
72
     * @var array
73
     */
74
    protected $lookupMethod;
75
76
    /**
77
     * Constructor
78
     *
79
     * @param ObjectManager $objectManager
80
     * @param string        $objectName
81
     * @param string|array  $index         Field or fields to find current entities by
82
     * @param string        $lookupMethod  Method used for looking up the item
83
     */
84 6
    public function __construct(
85
        ObjectManager $objectManager,
86
        $objectName,
87
        $index = null,
88
        $lookupMethod = 'findOneBy'
89
    ) {
90 6
        $this->ensureSupportedObjectManager($objectManager);
91 5
        $this->objectManager = $objectManager;
92 5
        $this->objectRepository = $objectManager->getRepository($objectName);
0 ignored issues
show
Documentation Bug introduced by
It seems like $objectManager->getRepository($objectName) of type object<Doctrine\Common\P...tence\ObjectRepository> is incompatible with the declared type object<Port\Doctrine\ObjectRepository> of property $objectRepository.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
93 5
        $this->objectMetadata = $objectManager->getClassMetadata($objectName);
94
        //translate objectName in case a namespace alias is used
95 5
        $this->objectName = $this->objectMetadata->getName();
96 5
        if ($index) {
97
            if (is_array($index)) {
98
                $this->lookupFields = $index;
99
            } else {
100
                $this->lookupFields = [$index];
101
            }
102
        }
103
104 5
        if (!method_exists($this->objectRepository, $lookupMethod)) {
105
            throw new \InvalidArgumentException(
106
                sprintf(
107
                    'Repository %s has no method %s',
108
                    get_class($this->objectRepository),
109
                    $lookupMethod
110
                )
111
            );
112
        }
113 5
        $this->lookupMethod = [$this->objectRepository, $lookupMethod];
114 5
    }
115
116
    /**
117
     * @return boolean
118
     */
119
    public function getTruncate()
120
    {
121
        return $this->truncate;
122
    }
123
124
    /**
125
     * Set whether to truncate the table first
126
     *
127
     * @param boolean $truncate
128
     *
129
     * @return $this
130
     */
131
    public function setTruncate($truncate)
132
    {
133
        $this->truncate = $truncate;
134
135
        return $this;
136
    }
137
138
    /**
139
     * Disable truncation
140
     *
141
     * @return $this
142
     */
143
    public function disableTruncate()
144
    {
145
        $this->truncate = false;
146
147
        return $this;
148
    }
149
150
    /**
151
     * Disable Doctrine logging
152
     *
153
     * @return $this
154
     */
155 1
    public function prepare()
156
    {
157 1
        $this->disableLogging();
158
159 1
        if (true === $this->truncate) {
160 1
            $this->truncateTable();
161 1
        }
162 1
    }
163
164
    /**
165
     * Re-enable Doctrine logging
166
     */
167 1
    public function finish()
168
    {
169 1
        $this->flush();
170 1
        $this->reEnableLogging();
171 1
    }
172
173
    /**
174
     * {@inheritdoc}
175
     */
176 4
    public function writeItem(array $item)
177
    {
178 4
        $object = $this->findOrCreateItem($item);
179
180 4
        $this->loadAssociationObjectsToObject($item, $object);
181 4
        $this->updateObject($item, $object);
182
183 4
        $this->objectManager->persist($object);
184 4
    }
185
186
    /**
187
     * Flush and clear the object manager
188
     */
189 1
    public function flush()
190
    {
191 1
        $this->objectManager->flush();
192 1
        $this->objectManager->clear($this->objectName);
193 1
    }
194
195
    /**
196
     * Return a new instance of the object
197
     *
198
     * @return object
199
     */
200 4
    protected function getNewInstance()
201
    {
202 4
        $className = $this->objectMetadata->getName();
203
204 4
        if (class_exists($className) === false) {
205
            throw new \RuntimeException('Unable to create new instance of ' . $className);
206
        }
207
208 4
        return new $className;
209
    }
210
211
    /**
212
     * Call a setter of the object
213
     *
214
     * @param object $object
215
     * @param mixed  $value
216
     * @param string $setter
217
     */
218 4
    protected function setValue($object, $value, $setter)
219
    {
220 4
        if (method_exists($object, $setter)) {
221 4
            $object->$setter($value);
222 4
        }
223 4
    }
224
225
    /**
226
     * @param array  $item
227
     * @param object $object
228
     */
229 4
    protected function updateObject(array $item, $object)
230
    {
231 4
        $fieldNames = array_merge($this->objectMetadata->getFieldNames(), $this->objectMetadata->getAssociationNames());
232 4
        foreach ($fieldNames as $fieldName) {
233 4
            $value = null;
234 4
            $classifiedFieldName = Inflector::classify($fieldName);
235 4
            if (isset($item[$fieldName])) {
236 4
                $value = $item[$fieldName];
237 4
            }
238
239 4
            if (null === $value) {
240
                continue;
241
            }
242
243 4
            if (!($value instanceof \DateTime)
244 4
                || $value != $this->objectMetadata->getFieldValue($object, $fieldName)
245 4
            ) {
246 4
                $setter = 'set' . $classifiedFieldName;
247 4
                $this->setValue($object, $value, $setter);
248 4
            }
249 4
        }
250 4
    }
251
252
    /**
253
     * Add the associated objects in case the item have for persist its relation
254
     *
255
     * @param array  $item
256
     * @param object $object
257
     */
258 4
    protected function loadAssociationObjectsToObject(array $item, $object)
259
    {
260 4
        foreach ($this->objectMetadata->getAssociationMappings() as $associationMapping) {
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Doctrine\Common\Persistence\Mapping\ClassMetadata as the method getAssociationMappings() does only exist in the following implementations of said interface: Doctrine\ORM\Mapping\ClassMetadata, Doctrine\ORM\Mapping\ClassMetadataInfo.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
261
262 4
            $value = null;
263 4
            if (isset($item[$associationMapping['fieldName']]) && !is_object($item[$associationMapping['fieldName']])) {
264 1
                $value = $this->objectManager->getReference($associationMapping['targetEntity'], $item[$associationMapping['fieldName']]);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Doctrine\Common\Persistence\ObjectManager as the method getReference() does only exist in the following implementations of said interface: Doctrine\ODM\MongoDB\DocumentManager, Doctrine\ORM\Decorator\EntityManagerDecorator, Doctrine\ORM\EntityManager.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
265 1
            }
266
267 4
            if (null === $value) {
268 4
                continue;
269
            }
270
271
            $setter = 'set' . ucfirst($associationMapping['fieldName']);
272
            $this->setValue($object, $value, $setter);
273 4
        }
274 4
    }
275
276
    /**
277
     * Truncate the database table for this writer
278
     */
279 1
    protected function truncateTable()
280
    {
281 1
        if ($this->objectManager instanceof \Doctrine\ORM\EntityManager) {
282
            $tableName = $this->objectMetadata->table['name'];
0 ignored issues
show
Bug introduced by
Accessing table 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...
283
            $connection = $this->objectManager->getConnection();
284
            $query = $connection->getDatabasePlatform()->getTruncateTableSQL($tableName, true);
285
            $connection->executeQuery($query);
286 1
        } elseif ($this->objectManager instanceof \Doctrine\ODM\MongoDB\DocumentManager) {
287 1
            $this->objectManager->getDocumentCollection($this->objectName)->remove(array());
288 1
        }
289 1
    }
290
291
    /**
292
     * Disable Doctrine logging
293
     */
294 1 View Code Duplication
    protected function disableLogging()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
295
    {
296
        //TODO: do we need to add support for MongoDB logging?
297 1
        if (!($this->objectManager instanceof \Doctrine\ORM\EntityManager)) return;
298
299
        $config = $this->objectManager->getConnection()->getConfiguration();
300
        $this->originalLogger = $config->getSQLLogger();
301
        $config->setSQLLogger(null);
302
    }
303
304
    /**
305
     * Re-enable Doctrine logging
306
     */
307 1 View Code Duplication
    protected function reEnableLogging()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
308
    {
309
        //TODO: do we need to add support for MongoDB logging?
310 1
        if (!($this->objectManager instanceof \Doctrine\ORM\EntityManager)) return;
311
312 1
        $config = $this->objectManager->getConnection()->getConfiguration();
313 1
        $config->setSQLLogger($this->originalLogger);
314 1
    }
315
316
    /**
317
     * @param array $item
318
     *
319
     * @return object
320
     */
321 4
    protected function findOrCreateItem(array $item)
322
    {
323 4
        $object = null;
324
        // If the table was not truncated to begin with, find current object
325
        // first
326 4
        if (!$this->truncate) {
327
            if (!empty($this->lookupFields)) {
328
                $lookupConditions = array();
329
                foreach ($this->lookupFields as $fieldName) {
330
                    $lookupConditions[$fieldName] = $item[$fieldName];
331
                }
332
333
                $object = call_user_func($this->lookupMethod, $lookupConditions);
334
            } else {
335
                $object = $this->objectRepository->find(current($item));
336
            }
337
        }
338
339 4
        if (!$object) {
340 4
            return $this->getNewInstance();
341
        }
342
343
        return $object;
344
    }
345
346 6
    protected function ensureSupportedObjectManager(ObjectManager $objectManager)
347
    {
348
        if (!($objectManager instanceof \Doctrine\ORM\EntityManager
349 6
            || $objectManager instanceof \Doctrine\ODM\MongoDB\DocumentManager)
350 6
        ) {
351 1
            throw new UnsupportedDatabaseTypeException($objectManager);
352
        }
353 5
    }
354
}
355