DoctrineInsertUpdateLoader::getLogs()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 0
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Smart\EtlBundle\Loader;
4
5
use Doctrine\ORM\EntityManager;
6
use Smart\EtlBundle\Entity\ImportableInterface;
7
use Smart\EtlBundle\Exception\Loader\EntityTypeNotHandledException;
8
use Smart\EtlBundle\Exception\Loader\EntityAlreadyRegisteredException;
9
use Smart\EtlBundle\Exception\Loader\LoaderException;
10
use Smart\EtlBundle\Exception\Loader\LoadUnvalidObjectsException;
11
use Symfony\Component\PropertyAccess\PropertyAccess;
12
use Symfony\Component\PropertyAccess\PropertyAccessor;
13
use Symfony\Component\Validator\Validator\ValidatorInterface;
14
15
/**
16
 * Nicolas Bastien <[email protected]>
17
 */
18
class DoctrineInsertUpdateLoader implements LoaderInterface
19
{
20
    const VALIDATION_GROUPS = 'smart_etl_loader';
21
22
    /**
23
     * @var EntityManager
24
     */
25
    protected $entityManager;
26
27
    /**
28
     * @var array
29
     */
30
    protected $references;
31
32
    /**
33
     * @var PropertyAccessor
34
     */
35
    protected $accessor;
36
37
    /**
38
     * List of entities to extract
39
     * [
40
     *      'class' => []
41 1
     * ]
42
     * @var array
43 1
     */
44 1
    protected $entitiesToProcess = [];
45 1
46
    /**
47
     * @var array
48
     */
49
    protected $loadLogs = [];
50
51
    /**
52
     * @var array keep validation errors for each process object with his own associative data index
53
     */
54 1
    protected $arrayValidationErrors = [];
55
56 1
    /**
57
     * @var mixed
58
     */
59
    protected $processKey = null;
60 1
61 1
    protected ?ValidatorInterface $validator = null;
62 1
63 1
    /**
64 1
     * @param ValidatorInterface|null $validator TODO NEXT_MAJOR remove nullable
65
     */
66
    public function __construct($entityManager, ValidatorInterface $validator = null)
67 1
    {
68
        $this->entityManager = $entityManager;
69
        $this->validator = $validator;
70
        $this->accessor = PropertyAccess::createPropertyAccessor();
71
    }
72
73 1
    /**
74
     * @param string $entityClass
75 1
     * @param callback $identifierCallback
76
     * @param string $identifierProperty : if null this entity will be always insert
77 1
     * @param array $entityProperties properties to synchronize
78 1
     * @return $this
79
     */
80 1
    public function addEntityToProcess($entityClass, $identifierCallback, $identifierProperty, array $entityProperties = [])
81 1
    {
82
        if (isset($this->entitiesToProcess[$entityClass])) {
83
            throw new EntityAlreadyRegisteredException($entityClass);
84
        }
85
86 1
        $this->entitiesToProcess[$entityClass] = [
87
            'class' => $entityClass,
88
            // TODO NEXT MAJOR remove callback param and use accessor getValue instead
89
            'callback' => $identifierCallback,
90
            'identifier' => $identifierProperty,
91
            'properties' => $entityProperties
92
        ];
93
94 1
        return $this;
95
    }
96 1
97
    /**
98
     * @throws \Exception
99 1
     */
100
    public function load(array $data)
101
    {
102 1
        $this->entityManager->beginTransaction();
103 1
        try {
104 1
            foreach ($data as $key => $object) {
105 1
                $this->processKey = $key;
106
                $this->processObject($object);
107 1
            }
108
109
            if (count($this->arrayValidationErrors) > 0) {
110 1
                throw new LoadUnvalidObjectsException($this->arrayValidationErrors);
111 1
            }
112
113 1
            // todo add a batch size for performance
114
            $this->entityManager->flush();
115 1
            $this->entityManager->commit();
116 1
        } catch (\Exception $e) {
117
            $this->entityManager->rollback();
118 1
            if ($e instanceof LoaderException) {
119
                throw $e;
120 1
            }
121 1
122 1
            throw new \Exception('EXCEPTION LOADER : ' . $e->getMessage());
123 1
        }
124
    }
125
126 1
    /**
127 1
     * @param  ImportableInterface $object
128
     * @return ImportableInterface
129 1
     * @throws \Exception
130
     * @throws \TypeError
131 1
     */
132
    protected function processObject($object)
133
    {
134 1
        $objectClass = get_class($object);
135 1
        if (!isset($this->entitiesToProcess[$objectClass])) {
136
            throw new EntityTypeNotHandledException($objectClass);
137
        }
138
139
        if ($this->validator !== null) {
140
            $validationErrors = $this->validator->validate($object, null, self::VALIDATION_GROUPS);
141
            if ($validationErrors->count() > 0) {
142 1
                $this->arrayValidationErrors[$this->processKey] = $validationErrors;
143 1
144 1
                return null;
145
            }
146 1
        }
147 1
148 1
        $identifier = $this->entitiesToProcess[$objectClass]['callback']($object);
149
150 1
        //Replace relations by their reference
151 1
        foreach ($this->entitiesToProcess[$objectClass]['properties'] as $property) {
152 1
            $propertyValue = $this->accessor->getValue($object, $property);
153
            if ($this->isEntityRelation($propertyValue)) {
154
                $relation = $propertyValue; //better understanding
155 1
156 1
                if (!isset($this->entitiesToProcess[get_class($relation)])) {
157
                    throw new EntityTypeNotHandledException(get_class($relation));
158 1
                }
159 1
                $relationIdentifier = $this->entitiesToProcess[get_class($relation)]['callback']($relation);
160
                if (!isset($this->references[$relationIdentifier])) {
161 1
                    //new relation should be processed before
162
                    $this->processObject($relation);
163
                }
164 1
                $this->accessor->setValue(
165
                    $object,
166
                    $property,
167
                    $this->references[$relationIdentifier]
168
                );
169
            } elseif ($propertyValue instanceof \Traversable) {
170
                foreach ($propertyValue as $k => $v) {
171
                    if ($this->isEntityRelation($v)) {
172
                        if (!isset($this->entitiesToProcess[get_class($v)])) {
173 1
                            throw new EntityTypeNotHandledException(get_class($v));
174
                        }
175 1
                        $relationIdentifier = $this->entitiesToProcess[get_class($v)]['callback']($v);
176
                        if (!isset($this->references[$relationIdentifier])) {
177
                            //new relation should be processed before
178
                            $this->processObject($v);
179
                        }
180
                        $propertyValue[$k] = $this->references[$relationIdentifier];
181
                    }
182
                }
183
                $this->accessor->setValue(
184
                    $object,
185
                    $property,
186
                    $propertyValue
187
                );
188
            }
189
        }
190
191
        $dbObject = null;
192
        if (!is_null($this->entitiesToProcess[$objectClass]['identifier'])) {
193
            // todo enhance entity query by moving this on the load method and init the existing $dbObjects with matching identifier
194
            $dbObject = $this->entityManager->getRepository($objectClass)->findOneBy([$this->entitiesToProcess[$objectClass]['identifier'] => $identifier]);
195
        }
196
        if ($dbObject === null) {
197
            if (!$object->isImported()) {
198
                $object->setImportedAt(new \DateTime());
199
            }
200
            $this->entityManager->persist($object);
201
            if (!is_null($identifier)) {
202
                $this->references[$identifier] = $object;
203
            }
204
205
            if (isset($this->loadLogs[$objectClass])) {
206
                $this->loadLogs[$objectClass]['nb_created']++;
207
            } else {
208
                $this->loadLogs[$objectClass] = [
209
                    'nb_created' => 1,
210
                    'nb_updated' => 0,
211
                ];
212
            }
213
        } else {
214
            // todo validate if there is no change (if so do not increase the nb_updated)
215
            foreach ($this->entitiesToProcess[$objectClass]['properties'] as $property) {
216
                $this->accessor->setValue($dbObject, $property, $this->accessor->getValue($object, $property));
217
            }
218
            if (!$dbObject->isImported()) {
219
                $dbObject->setImportedAt(new \DateTime());
220
            }
221
            $this->references[$identifier] = $dbObject;
222
223
            if (isset($this->loadLogs[$objectClass])) {
224
                $this->loadLogs[$objectClass]['nb_updated']++;
225
            } else {
226
                $this->loadLogs[$objectClass] = [
227
                    'nb_created' => 0,
228
                    'nb_updated' => 1,
229
                ];
230
            }
231
        }
232
233
        return $object;
234
    }
235
236
    /**
237
     * Check if $propertyValue is an entity relation to process
238
     *
239
     * @param  mixed $propertyValue
240
     * @return bool
241
     */
242
    protected function isEntityRelation($propertyValue)
243
    {
244
        return (is_object($propertyValue) && !($propertyValue instanceof \DateTime) && !($propertyValue instanceof \Traversable));
245
    }
246
247
    public function getLogs(): array
248
    {
249
        return $this->loadLogs;
250
    }
251
252
    public function clearLogs(): void
253
    {
254
        $this->loadLogs = [];
255
    }
256
}
257