Completed
Push — master ( 1dca6d...9d62b8 )
by David de
04:20
created

DoctrineWriter::__construct()   B

Complexity

Conditions 4
Paths 6

Size

Total Lines 31
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 6.2994

Importance

Changes 0
Metric Value
dl 0
loc 31
ccs 10
cts 21
cp 0.4762
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 22
nc 6
nop 4
crap 6.2994
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
            } elseif (method_exists($item, 'get' . $classifiedFieldName)) {
238
                $value = $item->{'get' . $classifiedFieldName};
239
            }
240
241 4
            if (null === $value) {
242
                continue;
243
            }
244
245 4
            if (!($value instanceof \DateTime)
246 4
                || $value != $this->objectMetadata->getFieldValue($object, $fieldName)
247 4
            ) {
248 4
                $setter = 'set' . $classifiedFieldName;
249 4
                $this->setValue($object, $value, $setter);
250 4
            }
251 4
        }
252 4
    }
253
254
    /**
255
     * Add the associated objects in case the item have for persist its relation
256
     *
257
     * @param array  $item
258
     * @param object $object
259
     */
260 4
    protected function loadAssociationObjectsToObject(array $item, $object)
261
    {
262 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...
263
264 4
            $value = null;
265 4
            if (isset($item[$associationMapping['fieldName']]) && !is_object($item[$associationMapping['fieldName']])) {
266 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...
267 1
            }
268
269 4
            if (null === $value) {
270 4
                continue;
271
            }
272
273
            $setter = 'set' . ucfirst($associationMapping['fieldName']);
274
            $this->setValue($object, $value, $setter);
275 4
        }
276 4
    }
277
278
    /**
279
     * Truncate the database table for this writer
280
     */
281 1
    protected function truncateTable()
282
    {
283 1
        if ($this->objectManager instanceof \Doctrine\ORM\EntityManager) {
284
            $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...
285
            $connection = $this->objectManager->getConnection();
286
            $query = $connection->getDatabasePlatform()->getTruncateTableSQL($tableName, true);
287
            $connection->executeQuery($query);
288 1
        } elseif ($this->objectManager instanceof \Doctrine\ODM\MongoDB\DocumentManager) {
289 1
            $this->objectManager->getDocumentCollection($this->objectName)->remove(array());
290 1
        }
291 1
    }
292
293
    /**
294
     * Disable Doctrine logging
295
     */
296 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...
297
    {
298
        //TODO: do we need to add support for MongoDB logging?
299 1
        if (!($this->objectManager instanceof \Doctrine\ORM\EntityManager)) return;
300
301
        $config = $this->objectManager->getConnection()->getConfiguration();
302
        $this->originalLogger = $config->getSQLLogger();
303
        $config->setSQLLogger(null);
304
    }
305
306
    /**
307
     * Re-enable Doctrine logging
308
     */
309 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...
310
    {
311
        //TODO: do we need to add support for MongoDB logging?
312 1
        if (!($this->objectManager instanceof \Doctrine\ORM\EntityManager)) return;
313
314 1
        $config = $this->objectManager->getConnection()->getConfiguration();
315 1
        $config->setSQLLogger($this->originalLogger);
316 1
    }
317
318
    /**
319
     * @param array $item
320
     *
321
     * @return object
322
     */
323 4
    protected function findOrCreateItem(array $item)
324
    {
325 4
        $object = null;
326
        // If the table was not truncated to begin with, find current object
327
        // first
328 4
        if (!$this->truncate) {
329
            if (!empty($this->lookupFields)) {
330
                $lookupConditions = array();
331
                foreach ($this->lookupFields as $fieldName) {
332
                    $lookupConditions[$fieldName] = $item[$fieldName];
333
                }
334
335
                $object = call_user_func($this->lookupMethod, $lookupConditions);
336
            } else {
337
                $object = $this->objectRepository->find(current($item));
338
            }
339
        }
340
341 4
        if (!$object) {
342 4
            return $this->getNewInstance();
343
        }
344
345
        return $object;
346
    }
347
348 6
    protected function ensureSupportedObjectManager(ObjectManager $objectManager)
349
    {
350
        if (!($objectManager instanceof \Doctrine\ORM\EntityManager
351 6
            || $objectManager instanceof \Doctrine\ODM\MongoDB\DocumentManager)
352 6
        ) {
353 1
            throw new UnsupportedDatabaseTypeException($objectManager);
354
        }
355 5
    }
356
}
357