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

BulkEntityUpdater::runQuery()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 46
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 0
Metric Value
cc 7
eloc 30
nc 7
nop 0
dl 0
loc 46
ccs 0
cts 46
cp 0
crap 56
rs 8.5066
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace EdmondsCommerce\DoctrineStaticMeta\Entity\Savers;
6
7
use Doctrine\ORM\EntityManagerInterface;
8
use EdmondsCommerce\DoctrineStaticMeta\Entity\Interfaces\EntityInterface;
9
use EdmondsCommerce\DoctrineStaticMeta\Entity\Savers\BulkEntityUpdater\BulkEntityUpdateHelper;
10
use EdmondsCommerce\DoctrineStaticMeta\Schema\MysqliConnectionFactory;
11
use EdmondsCommerce\DoctrineStaticMeta\Schema\UuidFunctionPolyfill;
12
use mysqli;
13
use Ramsey\Uuid\Doctrine\UuidBinaryOrderedTimeType;
14
use Ramsey\Uuid\UuidInterface;
15
use RuntimeException;
16
17
use function get_class;
18
19
class BulkEntityUpdater extends AbstractBulkProcess
20
{
21
    /**
22
     * @var BulkEntityUpdateHelper
23
     */
24
    private $extractor;
25
    /**
26
     * @var string
27
     */
28
    private $tableName;
29
    /**
30
     * @var string
31
     */
32
    private $entityFqn;
33
    /**
34
     * @var mysqli
35
     */
36
    private $mysqli;
37
    /**
38
     * This holds the bulk SQL query
39
     *
40
     * @var string
41
     */
42
    private $query;
43
44
    /**
45
     * @var float
46
     */
47
    private $requireAffectedRatio = 1.0;
48
49
    /**
50
     * @var int
51
     */
52
    private $totalAffectedRows = 0;
53
    /**
54
     * @var UuidFunctionPolyfill
55
     */
56
    private $uuidFunctionPolyfill;
57
58
    /**
59
     * Is the UUID binary
60
     *
61
     * @var bool
62
     */
63
    private $isBinaryUuid = true;
64
65
    public function __construct(
66
        EntityManagerInterface $entityManager,
67
        UuidFunctionPolyfill $uuidFunctionPolyfill,
68
        MysqliConnectionFactory $mysqliConnectionFactory
69
    ) {
70
        parent::__construct($entityManager);
71
        $this->uuidFunctionPolyfill = $uuidFunctionPolyfill;
72
        $this->mysqli               = $mysqliConnectionFactory->createFromEntityManager($entityManager);
73
    }
74
75
    /**
76
     * @param float $requireAffectedRatio
77
     *
78
     * @return BulkEntityUpdater
79
     */
80
    public function setRequireAffectedRatio(float $requireAffectedRatio): BulkEntityUpdater
81
    {
82
        $this->requireAffectedRatio = $requireAffectedRatio;
83
84
        return $this;
85
    }
86
87
    public function addEntityToSave(EntityInterface $entity)
88
    {
89
        if (false === $entity instanceof $this->entityFqn) {
90
            throw new RuntimeException('You can only bulk save a single entity type, currently saving ' .
91
                                        $this->entityFqn .
92
                                        ' but you are trying to save ' .
93
                                        get_class($entity));
94
        }
95
        parent::addEntityToSave($entity);
96
    }
97
98
    public function setExtractor(BulkEntityUpdateHelper $extractor): void
99
    {
100
        $this->extractor    = $extractor;
101
        $this->tableName    = $extractor->getTableName();
102
        $this->entityFqn    = $extractor->getEntityFqn();
103
        $this->isBinaryUuid = $this->isBinaryUuid();
104
        $this->runPolyfillIfRequired();
105
    }
106
107
    private function isBinaryUuid(): bool
108
    {
109
        $meta      = $this->entityManager->getClassMetadata($this->entityFqn);
110
        $idMapping = $meta->getFieldMapping($meta->getSingleIdentifierFieldName());
111
112
        return $idMapping['type'] === UuidBinaryOrderedTimeType::NAME;
113
    }
114
115
    private function runPolyfillIfRequired(): void
116
    {
117
        if (false === $this->isBinaryUuid) {
118
            return;
119
        }
120
        $this->uuidFunctionPolyfill->run();
121
    }
122
123
    public function startBulkProcess(): AbstractBulkProcess
124
    {
125
        if (!$this->extractor instanceof BulkEntityUpdateHelper) {
0 ignored issues
show
introduced by
$this->extractor is always a sub-type of EdmondsCommerce\Doctrine...\BulkEntityUpdateHelper.
Loading history...
126
            throw new RuntimeException(
127
                'You must call setExtractor with your extractor logic before starting the process. '
128
                . 'Note - a small anonymous class would be ideal'
129
            );
130
        }
131
        $this->resetQuery();
132
133
        return parent::startBulkProcess();
134
    }
135
136
    private function resetQuery(): void
137
    {
138
        $this->query = '';
139
    }
140
141
    /**
142
     * @return int
143
     */
144
    public function getTotalAffectedRows(): int
145
    {
146
        return $this->totalAffectedRows;
147
    }
148
149
    protected function doSave(): void
150
    {
151
        foreach ($this->entitiesToSave as $entity) {
152
            if (!$entity instanceof $this->entityFqn || !$entity instanceof EntityInterface) {
153
                throw new RuntimeException(
154
                    'You can only bulk save a single entity type, currently saving ' . $this->entityFqn .
155
                    ' but you are trying to save ' . get_class($entity)
156
                );
157
            }
158
            $this->appendToQuery(
159
                $this->convertExtractedToSqlRow(
160
                    $this->extractor->extract($entity)
161
                )
162
            );
163
        }
164
        $this->runQuery();
165
        $this->resetQuery();
166
    }
167
168
    private function appendToQuery(string $sql): void
169
    {
170
        $this->query .= "\n$sql";
171
    }
172
173
    /**
174
     * Take the extracted array and build an update query
175
     *
176
     * @param array $extracted
177
     *
178
     * @return string
179
     */
180
    private function convertExtractedToSqlRow(array $extracted): string
181
    {
182
        if ([] === $extracted) {
183
            throw new RuntimeException('Extracted array is empty in ' . __METHOD__);
184
        }
185
        $primaryKeyCol = null;
186
        $primaryKey    = null;
187
        $sql           = "update `{$this->tableName}` set ";
188
        $sqls          = [];
189
        foreach ($extracted as $key => $value) {
190
            if (null === $primaryKeyCol) {
191
                $primaryKeyCol = $key;
192
                $primaryKey    = $this->convertUuidToSqlString($value);
193
                continue;
194
            }
195
            $value  = $this->mysqli->escape_string((string)$value);
196
            $sqls[] = "`$key` = '$value'";
197
        }
198
        $sql .= implode(",\n", $sqls);
199
        $sql .= " where `$primaryKeyCol` = $primaryKey; ";
200
201
        return $sql;
202
    }
203
204
    private function convertUuidToSqlString(UuidInterface $uuid): string
205
    {
206
        $uuidString = (string)$uuid;
207
        if (false === $this->isBinaryUuid) {
208
            return "'$uuidString'";
209
        }
210
211
        return UuidFunctionPolyfill::UUID_TO_BIN . "('$uuidString', true)";
212
    }
213
214
    private function runQuery(): void
215
    {
216
        if ('' === $this->query) {
217
            return;
218
        }
219
        $this->query = "
220
           START TRANSACTION;
221
           SET FOREIGN_KEY_CHECKS = 0; 
222
           SET UNIQUE_CHECKS = 0;
223
           {$this->query}             
224
           SET FOREIGN_KEY_CHECKS = 1; 
225
           SET UNIQUE_CHECKS = 1;
226
           COMMIT;";
227
        $result      = $this->mysqli->multi_query($this->query);
228
        if (true !== $result) {
229
            throw new RuntimeException(
230
                'Multi Query returned false which means the first statement failed: ' .
231
                $this->mysqli->error
232
            );
233
        }
234
        $affectedRows = 0;
235
        $queryCount   = 0;
236
        do {
237
            $queryCount++;
238
            if (0 !== $this->mysqli->errno) {
239
                throw new RuntimeException(
240
                    'Query #' . $queryCount .
241
                    ' got MySQL Error #' . $this->mysqli->errno .
242
                    ': ' . $this->mysqli->error
243
                    . "\nQuery: " . $this->getQueryLine($queryCount) . "'\n"
244
                );
245
            }
246
            $affectedRows += max($this->mysqli->affected_rows, 0);
247
            if (false === $this->mysqli->more_results()) {
248
                break;
249
            }
250
            $this->mysqli->next_result();
251
        } while (true);
252
        if ($affectedRows < count($this->entitiesToSave) * $this->requireAffectedRatio) {
253
            throw new RuntimeException(
254
                'Affected rows count of ' . $affectedRows .
255
                ' does match the expected count of entitiesToSave ' . count($this->entitiesToSave)
256
            );
257
        }
258
        $this->totalAffectedRows += $affectedRows;
259
        $this->mysqli->commit();
260
    }
261
262
    private function getQueryLine(int $line): string
263
    {
264
        $lines = explode(';', $this->query);
265
266
        return $lines[$line + 1];
267
    }
268
}
269