GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Pull Request — master (#225)
by joseph
18:50
created

BulkSimpleEntityCreator   A

Complexity

Total Complexity 39

Size/Duplication

Total Lines 301
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 15
Bugs 0 Features 1
Metric Value
eloc 127
c 15
b 0
f 1
dl 0
loc 301
ccs 0
cts 196
cp 0
rs 9.28
wmc 39

20 Methods

Rating   Name   Duplication   Size   Complexity  
A addEntityToSave() 0 3 1
A getQueryLine() 0 5 1
A runPolyfillIfRequired() 0 6 2
A connect() 0 3 1
A reset() 0 4 1
A doSave() 0 7 2
A addEntityCreationData() 0 4 1
A freeResources() 0 4 1
A appendToQuery() 0 3 1
A generateId() 0 7 2
A endBulkProcess() 0 5 1
A getUuidSql() 0 9 2
B runQuery() 0 45 7
A __construct() 0 12 1
A setInsertMode() 0 11 3
A setHelper() 0 9 1
A isBinaryUuid() 0 5 1
A pingAndReconnectOnFailure() 0 11 3
A buildSql() 0 22 4
A addEntitiesToSave() 0 8 3
1
<?php
2
3
declare(strict_types=1);
4
5
namespace EdmondsCommerce\DoctrineStaticMeta\Entity\Savers;
6
7
use Doctrine\ORM\EntityManagerInterface;
8
use Doctrine\ORM\Mapping\ClassMetadataInfo;
9
use EdmondsCommerce\DoctrineStaticMeta\Entity\Fields\Factories\UuidFactory;
10
use EdmondsCommerce\DoctrineStaticMeta\Entity\Interfaces\EntityInterface;
11
use EdmondsCommerce\DoctrineStaticMeta\Entity\Savers\BulkEntityUpdater\BulkSimpleEntityCreatorHelper;
12
use EdmondsCommerce\DoctrineStaticMeta\Schema\MysqliConnectionFactory;
13
use EdmondsCommerce\DoctrineStaticMeta\Schema\UuidFunctionPolyfill;
14
use InvalidArgumentException;
15
use mysqli;
16
use Ramsey\Uuid\Doctrine\UuidBinaryOrderedTimeType;
17
use Ramsey\Uuid\UuidInterface;
18
use RuntimeException;
19
use Throwable;
20
21
use function in_array;
22
use function is_array;
23
24
/**
25
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
26
 */
27
class BulkSimpleEntityCreator extends AbstractBulkProcess
28
{
29
    public const INSERT_MODE_INSERT  = 'INSERT ';
30
    public const INSERT_MODE_IGNORE  = 'INSERT IGNORE ';
31
    public const INSERT_MODE_DEFAULT = self::INSERT_MODE_INSERT;
32
    public const INSERT_MODES        = [
33
        self::INSERT_MODE_INSERT,
34
        self::INSERT_MODE_IGNORE,
35
    ];
36
37
    /**
38
     * @var BulkSimpleEntityCreatorHelper
39
     */
40
    private $helper;
41
    /**
42
     * @var string
43
     */
44
    private $tableName;
45
    /**
46
     * @var string
47
     */
48
    private $entityFqn;
49
    /**
50
     * Is the UUID binary
51
     *
52
     * @var bool
53
     */
54
    private $isBinaryUuid = true;
55
    /**
56
     * @var ClassMetadataInfo
57
     */
58
    private $meta;
59
    /**
60
     * @var string
61
     */
62
    private $primaryKeyCol;
63
    /**
64
     * @var mysqli
65
     */
66
    private $mysqli;
67
    /**
68
     * @var UuidFunctionPolyfill
69
     */
70
    private $uuidFunctionPolyfill;
71
    /**
72
     * @var UuidFactory
73
     */
74
    private $uuidFactory;
75
    /**
76
     * @var string
77
     */
78
    private $query;
79
    /**
80
     * For creation this should always be 100%, so 1
81
     *
82
     * @var int
83
     */
84
    private $requireAffectedRatio = 1;
85
    /**
86
     * @var int
87
     */
88
    private $totalAffectedRows = 0;
89
90
    private $insertMode = self::INSERT_MODE_DEFAULT;
91
    /**
92
     * @var MysqliConnectionFactory
93
     */
94
    private $mysqliConnectionFactory;
95
96
    public function __construct(
97
        EntityManagerInterface $entityManager,
98
        MysqliConnectionFactory $mysqliConnectionFactory,
99
        UuidFunctionPolyfill $uuidFunctionPolyfill,
100
        UuidFactory $uuidFactory
101
    ) {
102
        parent::__construct($entityManager);
103
        $this->entityManager           = $entityManager;
104
        $this->mysqliConnectionFactory = $mysqliConnectionFactory;
105
        $this->uuidFunctionPolyfill    = $uuidFunctionPolyfill;
106
        $this->uuidFactory             = $uuidFactory;
107
        $this->connect();
108
    }
109
110
    private function connect(): void
111
    {
112
        $this->mysqli = $this->mysqliConnectionFactory->createFromEntityManager($this->entityManager);
113
    }
114
115
    public function endBulkProcess(): void
116
    {
117
        parent::endBulkProcess();
118
        // Reset the insert mode to default to prevent state bleeding across batch runs
119
        $this->setInsertMode(self::INSERT_MODE_DEFAULT);
120
    }
121
122
    /**
123
     * @param string $insertMode
124
     *
125
     * @return BulkSimpleEntityCreator
126
     */
127
    public function setInsertMode(string $insertMode): BulkSimpleEntityCreator
128
    {
129
        if (false === in_array($insertMode, self::INSERT_MODES, true)) {
130
            throw new InvalidArgumentException('Invalid insert mode');
131
        }
132
        $this->insertMode = $insertMode;
133
        if ($this->insertMode === self::INSERT_MODE_IGNORE) {
134
            $this->requireAffectedRatio = 0;
135
        }
136
137
        return $this;
138
    }
139
140
    public function addEntityToSave(EntityInterface $entity): void
141
    {
142
        throw new RuntimeException('You should not try to save Entities with this saver');
143
    }
144
145
    public function addEntitiesToSave(array $entities): void
146
    {
147
        foreach ($entities as $entityData) {
148
            if (is_array($entityData)) {
149
                $this->addEntityCreationData($entityData);
150
                continue;
151
            }
152
            throw new InvalidArgumentException('You should only pass in simple arrays of scalar entity data');
153
        }
154
    }
155
156
    public function addEntityCreationData(array $entityData): void
157
    {
158
        $this->entitiesToSave[] = $entityData;
159
        $this->bulkSaveIfChunkBigEnough();
160
    }
161
162
    public function setHelper(BulkSimpleEntityCreatorHelper $helper): void
163
    {
164
        $this->helper        = $helper;
165
        $this->tableName     = $helper->getTableName();
166
        $this->entityFqn     = $helper->getEntityFqn();
167
        $this->meta          = $this->entityManager->getClassMetadata($this->entityFqn);
168
        $this->primaryKeyCol = $this->meta->getSingleIdentifierFieldName();
169
        $this->isBinaryUuid  = $this->isBinaryUuid();
170
        $this->runPolyfillIfRequired();
171
    }
172
173
    private function isBinaryUuid(): bool
174
    {
175
        $idMapping = $this->meta->getFieldMapping($this->meta->getSingleIdentifierFieldName());
176
177
        return $idMapping['type'] === UuidBinaryOrderedTimeType::NAME;
178
    }
179
180
    private function runPolyfillIfRequired(): void
181
    {
182
        if (false === $this->isBinaryUuid) {
183
            return;
184
        }
185
        $this->uuidFunctionPolyfill->run();
186
    }
187
188
    /**
189
     * As these are not actually entities, lets empty them out before
190
     * parent::freeResources tries to detach from the entity manager
191
     */
192
    protected function freeResources()
193
    {
194
        $this->entitiesToSave = [];
195
        parent::freeResources();
196
    }
197
198
    protected function doSave(): void
199
    {
200
        foreach ($this->entitiesToSave as $entityData) {
201
            $this->appendToQuery($this->buildSql($entityData));
202
        }
203
        $this->runQuery();
204
        $this->reset();
205
    }
206
207
    private function appendToQuery(string $sql): void
208
    {
209
        $this->query .= "\n$sql";
210
    }
211
212
    private function buildSql(array $entityData): string
213
    {
214
        $sql  = $this->insertMode . " into {$this->tableName} set ";
215
        $sqls = [
216
            $this->primaryKeyCol . ' = ' . $this->generateId(),
217
        ];
218
        foreach ($entityData as $key => $value) {
219
            if ($key === $this->primaryKeyCol) {
220
                throw new InvalidArgumentException(
221
                    'You should not pass in IDs, they will be auto generated'
222
                );
223
            }
224
            if ($value instanceof UuidInterface) {
225
                $sqls[] = "`$key` = " . $this->getUuidSql($value);
226
                continue;
227
            }
228
            $value  = $this->mysqli->escape_string((string)$value);
229
            $sqls[] = "`$key` = '$value'";
230
        }
231
        $sql .= implode(', ', $sqls) . ';';
232
233
        return $sql;
234
    }
235
236
    private function generateId(): string
237
    {
238
        if ($this->isBinaryUuid) {
239
            return $this->getUuidSql($this->uuidFactory->getOrderedTimeUuid());
240
        }
241
242
        return $this->getUuidSql($this->uuidFactory->getUuid());
243
    }
244
245
    private function getUuidSql(UuidInterface $uuid): string
246
    {
247
        if ($this->isBinaryUuid) {
248
            $uuidString = (string)$uuid;
249
250
            return "UUID_TO_BIN('$uuidString', true)";
251
        }
252
253
        throw new RuntimeException('This is not currently suppported - should be easy enough though');
254
    }
255
256
    private function runQuery(): void
257
    {
258
        if ('' === $this->query) {
259
            return;
260
        }
261
        $this->pingAndReconnectOnFailure();
262
        $this->query = "
263
           START TRANSACTION;
264
           SET FOREIGN_KEY_CHECKS = 0; 
265
           {$this->query}             
266
           SET FOREIGN_KEY_CHECKS = 1; 
267
           COMMIT;";
268
        $result      = $this->mysqli->multi_query($this->query);
269
        if (true !== $result) {
270
            throw new RuntimeException(
271
                'Multi Query returned false which means the first statement failed: ' .
272
                $this->mysqli->error
273
            );
274
        }
275
        $affectedRows = 0;
276
        $queryCount   = 0;
277
        do {
278
            $queryCount++;
279
            $errorNo = (int)$this->mysqli->errno;
280
            if (0 !== $errorNo) {
281
                $errorMessage = 'Query #' . $queryCount .
282
                                ' got MySQL Error #' . $errorNo .
283
                                ': ' . $this->mysqli->error
284
                                . "\nQuery: " . $this->getQueryLine($queryCount) . "'\n";
285
                throw new RuntimeException($errorMessage);
286
            }
287
            $affectedRows += max($this->mysqli->affected_rows, 0);
288
            if (false === $this->mysqli->more_results()) {
289
                break;
290
            }
291
            $this->mysqli->next_result();
292
        } while (true);
293
        if ($affectedRows < count($this->entitiesToSave) * $this->requireAffectedRatio) {
294
            throw new RuntimeException(
295
                'Affected rows count of ' . $affectedRows .
296
                ' does match the expected count of entitiesToSave ' . count($this->entitiesToSave)
297
            );
298
        }
299
        $this->totalAffectedRows += $affectedRows;
300
        $this->mysqli->commit();
301
    }
302
303
    private function pingAndReconnectOnFailure(): void
304
    {
305
        if (null === $this->mysqli) {
306
            $this->connect();
307
        }
308
        try {
309
            $this->mysqli->query('select 1');
310
        } catch (Throwable $exception) {
311
            $this->mysqli->close();
312
            $this->mysqli = null;
313
            $this->connect();
314
        }
315
    }
316
317
    private function getQueryLine(int $line): string
318
    {
319
        $lines = explode(";\n", $this->query);
320
321
        return $lines[$line + 1];
322
    }
323
324
    private function reset(): void
325
    {
326
        $this->entitiesToSave = [];
327
        $this->query          = '';
328
    }
329
}
330