Passed
Push — master ( 50749f...7b5f02 )
by Gerrit
12:36
created

SimpleSelectDataLoader::hasDataChanged()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 5

Importance

Changes 0
Metric Value
dl 0
loc 24
c 0
b 0
f 0
ccs 11
cts 11
cp 1
rs 9.2248
cc 5
nc 6
nop 2
crap 5
1
<?php
2
/**
3
 * Copyright (C) 2018 Gerrit Addiks.
4
 * This package (including this file) was released under the terms of the GPL-3.0.
5
 * You should have received a copy of the GNU General Public License along with this program.
6
 * If not, see <http://www.gnu.org/licenses/> or send me a mail so i can send you a copy.
7
 * @license GPL-3.0
8
 * @author Gerrit Addiks <[email protected]>
9
 */
10
11
namespace Addiks\RDMBundle\DataLoader;
12
13
use Addiks\RDMBundle\DataLoader\DataLoaderInterface;
14
use Addiks\RDMBundle\Mapping\Drivers\MappingDriverInterface;
15
use Addiks\RDMBundle\Mapping\EntityMappingInterface;
16
use Doctrine\Common\Util\ClassUtils;
17
use Doctrine\DBAL\Schema\Column;
18
use Doctrine\DBAL\Connection;
19
use Doctrine\DBAL\Query\QueryBuilder;
20
use Doctrine\ORM\EntityManagerInterface;
21
use Doctrine\ORM\Mapping\ClassMetadata;
22
use ReflectionClass;
23
use ReflectionProperty;
24
use Doctrine\ORM\Query\Expr;
25
use Doctrine\DBAL\Driver\Statement;
26
use PDO;
27
use Addiks\RDMBundle\Mapping\MappingInterface;
28
use Addiks\RDMBundle\Hydration\HydrationContext;
29
use Webmozart\Assert\Assert;
30
use ErrorException;
31
32
/**
33
 * A very simple loader that just executes one simple select statement for every entity to load the data for.
34
 *
35
 * Because it executes one query for every entity to load data for, this could (and probably will) have an bad impact on
36
 * performance.
37
 *
38
 * TODO: This may be replaced in the future by integrating that data-loading into the select(s) executed by doctrine.
39
 */
40
final class SimpleSelectDataLoader implements DataLoaderInterface
41
{
42
43
    /**
44
     * @var MappingDriverInterface
45
     */
46
    private $mappingDriver;
47
48
    /**
49
     * @var array<array<scalar>>
50
     */
51
    private $originalData = array();
52
53 10
    public function __construct(
54
        MappingDriverInterface $mappingDriver
55
    ) {
56 10
        $this->mappingDriver = $mappingDriver;
57 10
    }
58
59
    /**
60
     * @param object $entity
61
     */
62 6
    public function loadDBALDataForEntity($entity, EntityManagerInterface $entityManager): array
63
    {
64
        /** @var class-string $className */
0 ignored issues
show
Documentation introduced by
The doc-type class-string could not be parsed: Unknown type name "class-string" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
65 6
        $className = get_class($entity);
66
67
        /** @var string $entityObjectHash */
68 6
        $entityObjectHash = spl_object_hash($entity);
69
70 6
        $this->originalData[$entityObjectHash] = [];
71
72
        /** @var array<string> $additionalData */
73 6
        $additionalData = array();
0 ignored issues
show
Unused Code introduced by
$additionalData is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
74
75
        do {
76 6
            if (class_exists(ClassUtils::class)) {
77 6
                $className = ClassUtils::getRealClass($className);
78 6
                Assert::classExists($className);
79
            }
80
81 6
            if (!$entityManager->getMetadataFactory()->isTransient($className)) {
82
                /** @var ClassMetadata $classMetaData */
83 6
                $classMetaData = $entityManager->getClassMetadata($className);
84
85
                /** @var ?EntityMappingInterface $entityMapping */
0 ignored issues
show
Documentation introduced by
The doc-type ?EntityMappingInterface could not be parsed: Unknown type name "?EntityMappingInterface" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
86 6
                $entityMapping = $this->mappingDriver->loadRDMMetadataForClass($className);
87
88 6
                if ($entityMapping instanceof EntityMappingInterface) {
89
                    /** @var array<Column> $additionalColumns */
90 6
                    $additionalColumns = $entityMapping->collectDBALColumns();
91
92 6
                    if (!empty($additionalColumns)) {
93
                        /** @var Connection $connection */
94 3
                        $connection = $entityManager->getConnection();
95
96
                        /** @var QueryBuilder $queryBuilder */
97 3
                        $queryBuilder = $connection->createQueryBuilder();
98
99
                        /** @var Expr $expr */
100 3
                        $expr = $queryBuilder->expr();
101
102 3
                        foreach ($additionalColumns as $column) {
103
                            /** @var Column $column */
104
105 3
                            $queryBuilder->addSelect($column->getName());
106
                        }
107
108 3
                        $reflectionClass = new ReflectionClass($className);
109
110
                        /** @var bool $hasId */
111 3
                        $hasId = false;
112
113 3
                        foreach ($classMetaData->identifier as $idFieldName) {
114
                            /** @var string $idFieldName */
115
116
                            /** @var array $idColumn */
117 3
                            $idColumn = $classMetaData->fieldMappings[$idFieldName];
118
119
                            /** @var ReflectionProperty $reflectionProperty */
120 3
                            $reflectionProperty = $reflectionClass->getProperty($idFieldName);
121
122 3
                            $reflectionProperty->setAccessible(true);
123
124 3
                            $idValue = $reflectionProperty->getValue($entity);
125
126 3
                            if (!empty($idValue)) {
127 3
                                $hasId = true;
128 3
                                if (!is_numeric($idValue) || empty($idValue)) {
129 2
                                    $idValue = "'{$idValue}'";
130
                                }
131 3
                                $queryBuilder->andWhere($expr->eq($idColumn['columnName'], $idValue));
132
                            }
133
                        }
134
135 3
                        if ($hasId) {
136 3
                            $queryBuilder->from($classMetaData->getTableName());
137 3
                            $queryBuilder->setMaxResults(1);
138
139
                            /** @var Statement $statement */
140 3
                            $statement = $queryBuilder->execute();
141
142 3
                            $additionalData = $statement->fetch(PDO::FETCH_ASSOC);
143
144 3
                            if (!is_array($additionalData)) {
145
                                $additionalData = array();
146
                            }
147
148 3
                            $this->originalData[$entityObjectHash] = array_merge(
149 3
                                $this->originalData[$entityObjectHash],
150
                                $additionalData
151
                            );
152
                        }
153
                    }
154
                }
155
            }
156
157 6
            $className = current(class_parents($className));
158 6
        } while (class_exists($className));
159
160 6
        return $this->originalData[$entityObjectHash] ?? [];
161
    }
162
163
    /**
164
     * @param object $entity
165
     */
166 4
    public function storeDBALDataForEntity($entity, EntityManagerInterface $entityManager): void
167
    {
168
        /** @var class-string $className */
0 ignored issues
show
Documentation introduced by
The doc-type class-string could not be parsed: Unknown type name "class-string" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
169 4
        $className = get_class($entity);
170
171 4
        if (class_exists(ClassUtils::class)) {
172 4
            $className = ClassUtils::getRealClass($className);
173 4
            Assert::classExists($className);
174
        }
175
176
        do {
177
            /** @var null|EntityMappingInterface $entityMapping */
178 4
            $entityMapping = $this->mappingDriver->loadRDMMetadataForClass($className);
179
180 4
            if ($entityMapping instanceof EntityMappingInterface) {
181 4
                $context = new HydrationContext($entity, $entityManager);
182
183
                /** @var array<scalar> */
184 4
                $additionalData = $entityMapping->revertValue($context, $entity);
185
186 4
                if ($this->hasDataChanged($entity, $additionalData)) {
187
                    /** @var ClassMetadata $classMetaData */
188 3
                    $classMetaData = $entityManager->getClassMetadata($className);
189
190
                    /** @var array<scalar> $identifier */
191 3
                    $identifier = $this->collectIdentifierForEntity($entity, $entityMapping, $classMetaData);
192
193
                    /** @var string $tableName */
194 3
                    $tableName = $classMetaData->getTableName();
195
196
                    /** @var Connection $connection */
197 3
                    $connection = $entityManager->getConnection();
198
199 3
                    $connection->update($tableName, $additionalData, $identifier);
200
                }
201
            }
202
203 4
            $className = current(class_parents($className));
204 4
        } while (class_exists($className));
205 4
    }
206
207
    /**
208
     * @param object $entity
209
     */
210 1
    public function removeDBALDataForEntity($entity, EntityManagerInterface $entityManager): void
211
    {
212
        # This data-loader does not store data outside the entity-table.
213
        # No additional data need to be removed.
214 1
    }
215
216 5
    public function prepareOnMetadataLoad(EntityManagerInterface $entityManager, ClassMetadata $classMetadata): void
217
    {
218
        # This data-loader does not need any preparation
219 5
    }
220
221
    /**
222
     * @param object $entity
223
     */
224 4
    private function hasDataChanged($entity, array $additionalData): bool
225
    {
226
        /** @var array<scalar> */
227 4
        $originalData = array();
228
229
        /** @var string $entityObjectHash */
230 4
        $entityObjectHash = spl_object_hash($entity);
231
232 4
        if (isset($this->originalData[$entityObjectHash])) {
233 2
            $originalData = $this->originalData[$entityObjectHash];
234
        }
235
236
        /** @var bool $hasDataChanged */
237 4
        $hasDataChanged = false;
238
239 4
        foreach ($additionalData as $key => $value) {
240 3
            if (!array_key_exists($key, $originalData) || $originalData[$key] != $value) {
241 3
                $hasDataChanged = true;
242 3
                break;
243
            }
244
        }
245
246 4
        return $hasDataChanged;
247
    }
248
249
    /**
250
     * @param object $entity
251
     */
252 3
    private function collectIdentifierForEntity(
253
        $entity,
254
        EntityMappingInterface $entityMapping,
255
        ClassMetadata $classMetaData
256
    ): array {
257 3
        $reflectionClass = new ReflectionClass($entityMapping->getEntityClassName());
258
259
        /** @var array<scalar> $identifier */
260 3
        $identifier = array();
261
262 3
        foreach ($classMetaData->identifier as $idFieldName) {
263
            /** @var string $idFieldName */
264
265
            /** @var array $idColumn */
266 3
            $idColumn = $classMetaData->fieldMappings[$idFieldName];
267
268
            /** @var ReflectionProperty $reflectionProperty */
269 3
            $reflectionProperty = $reflectionClass->getProperty($idFieldName);
270
271 3
            $reflectionProperty->setAccessible(true);
272
273 3
            $idValue = $reflectionProperty->getValue($entity);
274
275 3
            $identifier[$idColumn['columnName']] = $idValue;
276
        }
277
278 3
        return $identifier;
279
    }
280
281
}
282