RowEvent   F
last analyzed

Complexity

Total Complexity 115

Size/Duplication

Total Lines 913
Duplicated Lines 0 %

Test Coverage

Coverage 91.45%

Importance

Changes 13
Bugs 1 Features 0
Metric Value
wmc 115
eloc 324
c 13
b 1
f 0
dl 0
loc 913
ccs 310
cts 339
cp 0.9145
rs 2

25 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 12 1
A bitGet() 0 3 1
A getValues() 0 17 4
B makeTableMapDTO() 0 63 8
B getDecimal() 0 45 6
A getString() 0 3 1
A makeWriteRowsDTO() 0 13 2
A rowInit() 0 27 3
A bitCount() 0 13 3
D getColumnData() 0 104 37
A getColumnsBinarySize() 0 3 1
A getBitFromBitmap() 0 8 2
A checkNull() 0 3 1
A getTime() 0 8 3
B getFSP() 0 21 9
A getSet() 0 12 3
A makeDeleteRowsDTO() 0 13 2
B getBit() 0 30 7
A getDatetime() 0 15 4
A getEnum() 0 10 2
A getDate() 0 15 5
A makeUpdateRowsDTO() 0 23 3
A getTime2() 0 9 1
A getTimestamp2() 0 9 2
A getDatetime2() 0 25 4

How to fix   Complexity   

Complex Class

Complex classes like RowEvent often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use RowEvent, and based on these observations, apply Extract Interface, too.

1
<?php
2
declare(strict_types=1);
3
4
namespace MySQLReplication\Event\RowEvent;
5
6
use DateTime;
7
use Exception;
8
use MySQLReplication\BinaryDataReader\BinaryDataReader;
9
use MySQLReplication\BinaryDataReader\BinaryDataReaderException;
10
use MySQLReplication\Config\Config;
11
use MySQLReplication\Definitions\ConstEventType;
12
use MySQLReplication\Definitions\ConstFieldType;
13
use MySQLReplication\Event\DTO\DeleteRowsDTO;
14
use MySQLReplication\Event\DTO\TableMapDTO;
15
use MySQLReplication\Event\DTO\UpdateRowsDTO;
16
use MySQLReplication\Event\DTO\WriteRowsDTO;
17
use MySQLReplication\Event\EventCommon;
18
use MySQLReplication\Event\EventInfo;
19
use MySQLReplication\Exception\MySQLReplicationException;
20
use MySQLReplication\JsonBinaryDecoder\JsonBinaryDecoderException;
21
use MySQLReplication\JsonBinaryDecoder\JsonBinaryDecoderService;
22
use MySQLReplication\Repository\FieldDTO;
23
use MySQLReplication\Repository\RepositoryInterface;
24
use Psr\SimpleCache\CacheInterface;
25
use Psr\SimpleCache\InvalidArgumentException;
26
use RuntimeException;
27
28
class RowEvent extends EventCommon
29
{
30
    private static $bitCountInByte = [
31
        0,
32
        1,
33
        1,
34
        2,
35
        1,
36
        2,
37
        2,
38
        3,
39
        1,
40
        2,
41
        2,
42
        3,
43
        2,
44
        3,
45
        3,
46
        4,
47
        1,
48
        2,
49
        2,
50
        3,
51
        2,
52
        3,
53
        3,
54
        4,
55
        2,
56
        3,
57
        3,
58
        4,
59
        3,
60
        4,
61
        4,
62
        5,
63
        1,
64
        2,
65
        2,
66
        3,
67
        2,
68
        3,
69
        3,
70
        4,
71
        2,
72
        3,
73
        3,
74
        4,
75
        3,
76
        4,
77
        4,
78
        5,
79
        2,
80
        3,
81
        3,
82
        4,
83
        3,
84
        4,
85
        4,
86
        5,
87
        3,
88
        4,
89
        4,
90
        5,
91
        4,
92
        5,
93
        5,
94
        6,
95
        1,
96
        2,
97
        2,
98
        3,
99
        2,
100
        3,
101
        3,
102
        4,
103
        2,
104
        3,
105
        3,
106
        4,
107
        3,
108
        4,
109
        4,
110
        5,
111
        2,
112
        3,
113
        3,
114
        4,
115
        3,
116
        4,
117
        4,
118
        5,
119
        3,
120
        4,
121
        4,
122
        5,
123
        4,
124
        5,
125
        5,
126
        6,
127
        2,
128
        3,
129
        3,
130
        4,
131
        3,
132
        4,
133
        4,
134
        5,
135
        3,
136
        4,
137
        4,
138
        5,
139
        4,
140
        5,
141
        5,
142
        6,
143
        3,
144
        4,
145
        4,
146
        5,
147
        4,
148
        5,
149
        5,
150
        6,
151
        4,
152
        5,
153
        5,
154
        6,
155
        5,
156
        6,
157
        6,
158
        7,
159
        1,
160
        2,
161
        2,
162
        3,
163
        2,
164
        3,
165
        3,
166
        4,
167
        2,
168
        3,
169
        3,
170
        4,
171
        3,
172
        4,
173
        4,
174
        5,
175
        2,
176
        3,
177
        3,
178
        4,
179
        3,
180
        4,
181
        4,
182
        5,
183
        3,
184
        4,
185
        4,
186
        5,
187
        4,
188
        5,
189
        5,
190
        6,
191
        2,
192
        3,
193
        3,
194
        4,
195
        3,
196
        4,
197
        4,
198
        5,
199
        3,
200
        4,
201
        4,
202
        5,
203
        4,
204
        5,
205
        5,
206
        6,
207
        3,
208
        4,
209
        4,
210
        5,
211
        4,
212
        5,
213
        5,
214
        6,
215
        4,
216
        5,
217
        5,
218
        6,
219
        5,
220
        6,
221
        6,
222
        7,
223
        2,
224
        3,
225
        3,
226
        4,
227
        3,
228
        4,
229
        4,
230
        5,
231
        3,
232
        4,
233
        4,
234
        5,
235
        4,
236
        5,
237
        5,
238
        6,
239
        3,
240
        4,
241
        4,
242
        5,
243
        4,
244
        5,
245
        5,
246
        6,
247
        4,
248
        5,
249
        5,
250
        6,
251
        5,
252
        6,
253
        6,
254
        7,
255
        3,
256
        4,
257
        4,
258
        5,
259
        4,
260
        5,
261
        5,
262
        6,
263
        4,
264
        5,
265
        5,
266
        6,
267
        5,
268
        6,
269
        6,
270
        7,
271
        4,
272
        5,
273
        5,
274
        6,
275
        5,
276
        6,
277
        6,
278
        7,
279
        5,
280
        6,
281
        6,
282
        7,
283
        6,
284
        7,
285
        7,
286
        8,
287
    ];
288
    private $repository;
289
    private $cache;
290
291
    /**
292
     * @var TableMap|null
293
     */
294
    private $currentTableMap;
295
    /**
296
     * @var Config
297
     */
298
    private $config;
299
300 54
    public function __construct(
301
        Config $config,
302
        RepositoryInterface $repository,
303
        BinaryDataReader $binaryDataReader,
304
        EventInfo $eventInfo,
305
        CacheInterface $cache
306
    ) {
307 54
        parent::__construct($eventInfo, $binaryDataReader);
308
309 54
        $this->repository = $repository;
310 54
        $this->cache = $cache;
311 54
        $this->config = $config;
312
    }
313
314
    /**
315
     * This describe the structure of a table.
316
     * It's send before a change append on a table.
317
     * A end user of the lib should have no usage of this
318
     * @throws BinaryDataReaderException
319
     * @throws InvalidArgumentException
320
     */
321 54
    public function makeTableMapDTO(): ?TableMapDTO
322
    {
323 54
        $data = [];
324 54
        $data['table_id'] = $this->binaryDataReader->readTableId();
325 54
        $this->binaryDataReader->advance(2);
326 54
        $data['schema_length'] = $this->binaryDataReader->readUInt8();
327 54
        $data['schema_name'] = $this->binaryDataReader->read($data['schema_length']);
328
329 54
        if ($this->config->checkDataBasesOnly($data['schema_name'])) {
330
            return null;
331
        }
332
333 54
        $this->binaryDataReader->advance(1);
334 54
        $data['table_length'] = $this->binaryDataReader->readUInt8();
335 54
        $data['table_name'] = $this->binaryDataReader->read($data['table_length']);
336
337 54
        if ($this->config->checkTablesOnly($data['table_name'])) {
338 1
            return null;
339
        }
340
341 54
        $this->binaryDataReader->advance(1);
342 54
        $data['columns_amount'] = (int)$this->binaryDataReader->readCodedBinary();
343 54
        $data['column_types'] = $this->binaryDataReader->read($data['columns_amount']);
344
345 54
        if ($this->cache->has($data['table_id'])) {
346 2
            return new TableMapDTO($this->eventInfo, $this->cache->get($data['table_id']));
347
        }
348
349 54
        $this->binaryDataReader->readCodedBinary();
350
351 54
        $fieldDTOCollection = $this->repository->getFields($data['schema_name'], $data['table_name']);
352 54
        $columnDTOCollection = new ColumnDTOCollection();
353
        // if you drop tables and parse of logs you will get empty scheme
354 54
        if (! $fieldDTOCollection->isEmpty()) {
355 53
            $columnLength = strlen($data['column_types']);
356 53
            for ($offset = 0; $offset < $columnLength; ++$offset) {
357
                // this a dirty hack to prevent row events containing columns which have been dropped
358 53
                if ($fieldDTOCollection->offsetExists($offset)) {
0 ignored issues
show
Bug introduced by
$offset of type integer is incompatible with the type Doctrine\Common\Collections\TKey expected by parameter $offset of Doctrine\Common\Collecti...lection::offsetExists(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

358
                if ($fieldDTOCollection->offsetExists(/** @scrutinizer ignore-type */ $offset)) {
Loading history...
359 53
                    $type = ord($data['column_types'][$offset]);
360
                } else {
361 1
                    $fieldDTOCollection->offsetSet($offset, FieldDTO::makeDummy($offset));
0 ignored issues
show
Bug introduced by
$offset of type integer is incompatible with the type Doctrine\Common\Collections\TKey|null expected by parameter $offset of Doctrine\Common\Collecti...Collection::offsetSet(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

361
                    $fieldDTOCollection->offsetSet(/** @scrutinizer ignore-type */ $offset, FieldDTO::makeDummy($offset));
Loading history...
362 1
                    $type = ConstFieldType::IGNORE;
363
                }
364
365
                /** @var FieldDTO $fieldDTO */
366 53
                $fieldDTO = $fieldDTOCollection->offsetGet($offset);
0 ignored issues
show
Bug introduced by
$offset of type integer is incompatible with the type Doctrine\Common\Collections\TKey expected by parameter $offset of Doctrine\Common\Collecti...Collection::offsetGet(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

366
                $fieldDTO = $fieldDTOCollection->offsetGet(/** @scrutinizer ignore-type */ $offset);
Loading history...
367 53
                if (null !== $fieldDTO) {
368 53
                    $columnDTOCollection->set($offset, ColumnDTO::make($type, $fieldDTO, $this->binaryDataReader));
369
                }
370
            }
371
        }
372
373 54
        $tableMap = new TableMap(
374 54
            $data['schema_name'],
375 54
            $data['table_name'],
376 54
            $data['table_id'],
377 54
            $data['columns_amount'],
378 54
            $columnDTOCollection
379 54
        );
380
381 54
        $this->cache->set($data['table_id'], $tableMap);
382
383 54
        return new TableMapDTO($this->eventInfo, $tableMap);
384
    }
385
386
    /**
387
     * @throws BinaryDataReaderException
388
     * @throws InvalidArgumentException
389
     * @throws JsonBinaryDecoderException
390
     * @throws MySQLReplicationException
391
     */
392 53
    public function makeWriteRowsDTO(): ?WriteRowsDTO
393
    {
394 53
        if (! $this->rowInit()) {
395 1
            return null;
396
        }
397
398 53
        $values = $this->getValues();
399
400 53
        return new WriteRowsDTO(
401 53
            $this->eventInfo,
402 53
            $this->currentTableMap,
403 53
            count($values),
404 53
            $values
405 53
        );
406
    }
407
408
    /**
409
     * @throws InvalidArgumentException
410
     * @throws BinaryDataReaderException
411
     */
412 53
    protected function rowInit(): bool
413
    {
414 53
        $tableId = $this->binaryDataReader->readTableId();
415 53
        $this->binaryDataReader->advance(2);
416
417 53
        if (in_array(
418 53
            $this->eventInfo->getType(),
419 53
            [
420 53
            ConstEventType::DELETE_ROWS_EVENT_V2,
421 53
            ConstEventType::WRITE_ROWS_EVENT_V2,
422 53
            ConstEventType::UPDATE_ROWS_EVENT_V2
423 53
            ],
424 53
            true
425 53
        )) {
426 53
            $this->binaryDataReader->read((int)($this->binaryDataReader->readUInt16() / 8));
427
        }
428
429 53
        $this->binaryDataReader->readCodedBinary();
430
431 53
        if ($this->cache->has($tableId)) {
432
            /** @var TableMap $tableMap */
433 53
            $this->currentTableMap = $this->cache->get($tableId);
434
435 53
            return true;
436
        }
437
438 1
        return false;
439
    }
440
441
    /**
442
     * @throws BinaryDataReaderException
443
     * @throws JsonBinaryDecoderException
444
     * @throws MySQLReplicationException
445
     */
446 53
    protected function getValues(): array
447
    {
448
        // if we don't get columns from information schema we don't know how to assign them
449 53
        if ($this->currentTableMap === null || $this->currentTableMap->getColumnDTOCollection()->isEmpty()) {
450 1
            return [];
451
        }
452
453 52
        $binaryData = $this->binaryDataReader->read(
454 52
            $this->getColumnsBinarySize($this->currentTableMap->getColumnsAmount())
455 52
        );
456
457 52
        $values = [];
458 52
        while (! $this->binaryDataReader->isComplete($this->eventInfo->getSizeNoHeader())) {
459 52
            $values[] = $this->getColumnData($binaryData);
460
        }
461
462 52
        return $values;
463
    }
464
465 52
    protected function getColumnsBinarySize(int $columnsAmount): int
466
    {
467 52
        return (int)(($columnsAmount + 7) / 8);
468
    }
469
470
    /**
471
     * @throws BinaryDataReaderException
472
     * @throws JsonBinaryDecoderException
473
     * @throws MySQLReplicationException
474
     */
475 52
    protected function getColumnData(string $colsBitmap): array
476
    {
477 52
        if (null === $this->currentTableMap) {
478
            throw new RuntimeException('Current table map is missing!');
479
        }
480
481 52
        $values = [];
482
483
        // null bitmap length = (bits set in 'columns-present-bitmap'+7)/8
484
        // see http://dev.mysql.com/doc/internals/en/rows-event.html
485 52
        $nullBitmap = $this->binaryDataReader->read($this->getColumnsBinarySize($this->bitCount($colsBitmap)));
486 52
        $nullBitmapIndex = 0;
487
488 52
        foreach ($this->currentTableMap->getColumnDTOCollection() as $i => $columnDTO) {
489 52
            $name = $columnDTO->getName();
490 52
            $type = $columnDTO->getType();
491
492 52
            if (0 === $this->bitGet($colsBitmap, $i)) {
493
                $values[$name] = null;
494
                continue;
495
            }
496
497 52
            if ($this->checkNull($nullBitmap, $nullBitmapIndex)) {
498 6
                $values[$name] = null;
499 51
            } elseif ($type === ConstFieldType::IGNORE) {
500 1
                $this->binaryDataReader->advance($columnDTO->getLengthSize());
501 1
                $values[$name] = null;
502 51
            } elseif ($type === ConstFieldType::TINY) {
503 5
                if ($columnDTO->isUnsigned()) {
504 4
                    $values[$name] = $this->binaryDataReader->readUInt8();
505
                } else {
506 5
                    $values[$name] = $this->binaryDataReader->readInt8();
507
                }
508 46
            } elseif ($type === ConstFieldType::SHORT) {
509 1
                if ($columnDTO->isUnsigned()) {
510 1
                    $values[$name] = $this->binaryDataReader->readUInt16();
511
                } else {
512 1
                    $values[$name] = $this->binaryDataReader->readInt16();
513
                }
514 45
            } elseif ($type === ConstFieldType::LONG) {
515 10
                if ($columnDTO->isUnsigned()) {
516 1
                    $values[$name] = $this->binaryDataReader->readUInt32();
517
                } else {
518 10
                    $values[$name] = $this->binaryDataReader->readInt32();
519
                }
520 43
            } elseif ($type === ConstFieldType::LONGLONG) {
521 1
                if ($columnDTO->isUnsigned()) {
522 1
                    $values[$name] = $this->binaryDataReader->readUInt64();
523
                } else {
524 1
                    $values[$name] = $this->binaryDataReader->readInt64();
525
                }
526 42
            } elseif ($type === ConstFieldType::INT24) {
527 1
                if ($columnDTO->isUnsigned()) {
528 1
                    $values[$name] = $this->binaryDataReader->readUInt24();
529
                } else {
530 1
                    $values[$name] = $this->binaryDataReader->readInt24();
531
                }
532 41
            } elseif ($type === ConstFieldType::FLOAT) {
533
                // http://dev.mysql.com/doc/refman/5.7/en/floating-point-types.html FLOAT(7,4)
534 1
                $values[$name] = round($this->binaryDataReader->readFloat(), 4);
535 40
            } elseif ($type === ConstFieldType::DOUBLE) {
536 1
                $values[$name] = $this->binaryDataReader->readDouble();
537 39
            } elseif ($type === ConstFieldType::VARCHAR || $type === ConstFieldType::STRING) {
538 8
                $values[$name] = $columnDTO->getMaxLength() > 255 ? $this->getString(2) : $this->getString(1);
539 31
            } elseif ($type === ConstFieldType::NEWDECIMAL) {
540 10
                $values[$name] = $this->getDecimal($columnDTO);
541 21
            } elseif ($type === ConstFieldType::BLOB) {
542 4
                $values[$name] = $this->getString($columnDTO->getLengthSize());
543 17
            } elseif ($type === ConstFieldType::DATETIME) {
544
                $values[$name] = $this->getDatetime();
545 17
            } elseif ($type === ConstFieldType::DATETIME2) {
546 4
                $values[$name] = $this->getDatetime2($columnDTO);
547 13
            } elseif ($type === ConstFieldType::TIMESTAMP) {
548
                $values[$name] = date('Y-m-d H:i:s', $this->binaryDataReader->readUInt32());
549 13
            } elseif ($type === ConstFieldType::TIME) {
550
                $values[$name] = $this->getTime();
551 13
            } elseif ($type === ConstFieldType::TIME2) {
552 2
                $values[$name] = $this->getTime2($columnDTO);
553 11
            } elseif ($type === ConstFieldType::TIMESTAMP2) {
554 2
                $values[$name] = $this->getTimestamp2($columnDTO);
555 9
            } elseif ($type === ConstFieldType::DATE) {
556 4
                $values[$name] = $this->getDate();
557 5
            } elseif ($type === ConstFieldType::YEAR) {
558
                // https://dev.mysql.com/doc/refman/5.7/en/year.html
559 1
                $year = $this->binaryDataReader->readUInt8();
560 1
                $values[$name] = 0 === $year ? null : 1900 + $year;
561 4
            } elseif ($type === ConstFieldType::ENUM) {
562 1
                $values[$name] = $this->getEnum($columnDTO);
563 3
            } elseif ($type === ConstFieldType::SET) {
564 1
                $values[$name] = $this->getSet($columnDTO);
565 2
            } elseif ($type === ConstFieldType::BIT) {
566 1
                $values[$name] = $this->getBit($columnDTO);
567 1
            } elseif ($type === ConstFieldType::GEOMETRY) {
568 1
                $values[$name] = $this->getString($columnDTO->getLengthSize());
569
            } elseif ($type === ConstFieldType::JSON) {
570
                $values[$name] = JsonBinaryDecoderService::makeJsonBinaryDecoder($this->getString($columnDTO->getLengthSize()))->parseToString();
571
            } else {
572
                throw new MySQLReplicationException('Unknown row type: ' . $type);
573
            }
574
575 52
            ++$nullBitmapIndex;
576
        }
577
578 52
        return $values;
579
    }
580
581 52
    protected function bitCount(string $bitmap): int
582
    {
583 52
        $n = 0;
584 52
        $bitmapLength = strlen($bitmap);
585 52
        for ($i = 0; $i < $bitmapLength; ++$i) {
586 52
            $bit = $bitmap[$i];
587 52
            if (is_string($bit)) {
588 52
                $bit = ord($bit);
589
            }
590 52
            $n += self::$bitCountInByte[$bit];
591
        }
592
593 52
        return $n;
594
    }
595
596 52
    protected function bitGet(string $bitmap, int $position): int
597
    {
598 52
        return $this->getBitFromBitmap($bitmap, $position) & (1 << ($position & 7));
599
    }
600
601 52
    protected function getBitFromBitmap(string $bitmap, int $position): int
602
    {
603 52
        $bit = $bitmap[(int)($position / 8)];
604 52
        if (is_string($bit)) {
605 52
            $bit = ord($bit);
606
        }
607
608 52
        return $bit;
609
    }
610
611 52
    protected function checkNull(string $nullBitmap, int $position): int
612
    {
613 52
        return $this->getBitFromBitmap($nullBitmap, $position) & (1 << ($position % 8));
614
    }
615
616
    /**
617
     * @throws BinaryDataReaderException
618
     */
619 13
    protected function getString(int $size): string
620
    {
621 13
        return $this->binaryDataReader->readLengthString($size);
622
    }
623
624
    /**
625
     * Read MySQL's new decimal format introduced in MySQL 5
626
     * https://dev.mysql.com/doc/refman/5.6/en/precision-math-decimal-characteristics.html
627
     * @throws BinaryDataReaderException
628
     */
629 10
    protected function getDecimal(ColumnDTO $columnDTO): string
630
    {
631 10
        $digitsPerInteger = 9;
632 10
        $compressedBytes = [0, 1, 1, 2, 2, 3, 3, 4, 4, 4];
633 10
        $integral = $columnDTO->getPrecision() - $columnDTO->getDecimals();
634 10
        $unCompIntegral = (int)($integral / $digitsPerInteger);
635 10
        $unCompFractional = (int)($columnDTO->getDecimals() / $digitsPerInteger);
636 10
        $compIntegral = $integral - ($unCompIntegral * $digitsPerInteger);
637 10
        $compFractional = $columnDTO->getDecimals() - ($unCompFractional * $digitsPerInteger);
638
639 10
        $value = $this->binaryDataReader->readUInt8();
640 10
        if (0 !== ($value & 0x80)) {
641 7
            $mask = 0;
642 7
            $res = '';
643
        } else {
644 3
            $mask = -1;
645 3
            $res = '-';
646
        }
647 10
        $this->binaryDataReader->unread(pack('C', $value ^ 0x80));
648
649 10
        $size = $compressedBytes[$compIntegral];
650 10
        if ($size > 0) {
651 10
            $value = $this->binaryDataReader->readIntBeBySize($size) ^ $mask;
652 10
            $res .= $value;
653
        }
654
655 10
        for ($i = 0; $i < $unCompIntegral; ++$i) {
656 9
            $value = $this->binaryDataReader->readInt32Be() ^ $mask;
657 9
            $res .= sprintf('%09d', $value);
658
        }
659
660 10
        $res .= '.';
661
662 10
        for ($i = 0; $i < $unCompFractional; ++$i) {
663 4
            $value = $this->binaryDataReader->readInt32Be() ^ $mask;
664 4
            $res .= sprintf('%09d', $value);
665
        }
666
667 10
        $size = $compressedBytes[$compFractional];
668 10
        if ($size > 0) {
669 5
            $value = $this->binaryDataReader->readIntBeBySize($size) ^ $mask;
670 5
            $res .= sprintf('%0' . $compFractional . 'd', $value);
671
        }
672
673 10
        return bcmul($res, '1', $columnDTO->getDecimals());
674
    }
675
676
    protected function getDatetime(): ?string
677
    {
678
        $value = $this->binaryDataReader->readUInt64();
679
        // nasty mysql 0000-00-00 dates
680
        if ('0' === $value) {
681
            return null;
682
        }
683
684
        $date = DateTime::createFromFormat('YmdHis', $value)->format('Y-m-d H:i:s');
685
        $dateLastErrors = DateTime::getLastErrors();
686
        if ($dateLastErrors && array_sum($dateLastErrors) > 0) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $dateLastErrors of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
687
            return null;
688
        }
689
690
        return $date;
691
    }
692
693
    /**
694
     * Date Time
695
     * 1 bit  sign           (1= non-negative, 0= negative)
696
     * 17 bits year*13+month  (year 0-9999, month 0-12)
697
     * 5 bits day            (0-31)
698
     * 5 bits hour           (0-23)
699
     * 6 bits minute         (0-59)
700
     * 6 bits second         (0-59)
701
     * ---------------------------
702
     * 40 bits = 5 bytes
703
     *
704
     * @throws BinaryDataReaderException
705
     *
706
     * @link https://dev.mysql.com/doc/internals/en/date-and-time-data-type-representation.html
707
     */
708 4
    protected function getDatetime2(ColumnDTO $columnDTO): ?string
709
    {
710 4
        $data = $this->binaryDataReader->readIntBeBySize(5);
711
712 4
        $yearMonth = $this->binaryDataReader->getBinarySlice($data, 1, 17, 40);
713
714 4
        $year = (int)($yearMonth / 13);
715 4
        $month = $yearMonth % 13;
716 4
        $day = $this->binaryDataReader->getBinarySlice($data, 18, 5, 40);
717 4
        $hour = $this->binaryDataReader->getBinarySlice($data, 23, 5, 40);
718 4
        $minute = $this->binaryDataReader->getBinarySlice($data, 28, 6, 40);
719 4
        $second = $this->binaryDataReader->getBinarySlice($data, 34, 6, 40);
720 4
        $fsp = $this->getFSP($columnDTO);
721
722
        try {
723 4
            $date = new DateTime($year . '-' . $month . '-' . $day . ' ' . $hour . ':' . $minute . ':' . $second);
724
        } catch (Exception $exception) {
725
            return null;
726
        }
727 4
        $dateLastErrors = DateTime::getLastErrors();
728 4
        if ($dateLastErrors && array_sum($dateLastErrors) > 0) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $dateLastErrors of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
729 3
            return null;
730
        }
731
732 1
        return $date->format('Y-m-d H:i:s') . $fsp;
733
    }
734
735
    /**
736
     * @throws BinaryDataReaderException
737
     * @link https://dev.mysql.com/doc/internals/en/date-and-time-data-type-representation.html
738
     */
739 8
    protected function getFSP(ColumnDTO $columnDTO): string
740
    {
741 8
        $read = 0;
742 8
        $time = '';
743 8
        $fsp = $columnDTO->getFsp();
744 8
        if ($fsp === 1 || $fsp === 2) {
745 1
            $read = 1;
746 8
        } elseif ($fsp === 3 || $fsp === 4) {
747 1
            $read = 2;
748 8
        } elseif ($fsp === 5 || $fsp === 6) {
749 1
            $read = 3;
750
        }
751 8
        if ($read > 0) {
752 1
            $microsecond = $this->binaryDataReader->readIntBeBySize($read);
753 1
            if ($fsp % 2) {
754 1
                $microsecond = (int)($microsecond / 10);
755
            }
756 1
            $time = $microsecond * (10 ** (6 - $fsp));
757
        }
758
759 8
        return (string)$time;
760
    }
761
762
    protected function getTime(): string
763
    {
764
        $data = $this->binaryDataReader->readUInt24();
765
        if (0 === $data) {
766
            return '00:00:00';
767
        }
768
769
        return sprintf('%s%02d:%02d:%02d', $data < 0 ? '-' : '', $data / 10000, ($data % 10000) / 100, $data % 100);
770
    }
771
772
    /**
773
     * TIME encoding for non fractional part:
774
     * 1 bit sign    (1= non-negative, 0= negative)
775
     * 1 bit unused  (reserved for future extensions)
776
     * 10 bits hour   (0-838)
777
     * 6 bits minute (0-59)
778
     * 6 bits second (0-59)
779
     * ---------------------
780
     * 24 bits = 3 bytes
781
     *
782
     * @throws BinaryDataReaderException
783
     */
784 2
    protected function getTime2(ColumnDTO $columnDTO): string
785
    {
786 2
        $data = $this->binaryDataReader->readInt24Be();
787
788 2
        $hour = $this->binaryDataReader->getBinarySlice($data, 2, 10, 24);
789 2
        $minute = $this->binaryDataReader->getBinarySlice($data, 12, 6, 24);
790 2
        $second = $this->binaryDataReader->getBinarySlice($data, 18, 6, 24);
791
792 2
        return (new DateTime())->setTime($hour, $minute, $second)->format('H:i:s') . $this->getFSP($columnDTO);
793
    }
794
795
    /**
796
     * @throws BinaryDataReaderException
797
     */
798 2
    protected function getTimestamp2(ColumnDTO $columnDTO): string
799
    {
800 2
        $datetime = (string)date('Y-m-d H:i:s', $this->binaryDataReader->readInt32Be());
801 2
        $fsp = $this->getFSP($columnDTO);
802 2
        if ('' !== $fsp) {
803 1
            $datetime .= '.' . $fsp;
804
        }
805
806 2
        return $datetime;
807
    }
808
809 4
    protected function getDate(): ?string
810
    {
811 4
        $time = $this->binaryDataReader->readUInt24();
812 4
        if (0 === $time) {
813
            return null;
814
        }
815
816 4
        $year = ($time & ((1 << 15) - 1) << 9) >> 9;
817 4
        $month = ($time & ((1 << 4) - 1) << 5) >> 5;
818 4
        $day = ($time & ((1 << 5) - 1));
819 4
        if ($year === 0 || $month === 0 || $day === 0) {
820 3
            return null;
821
        }
822
823 1
        return (new DateTime())->setDate($year, $month, $day)->format('Y-m-d');
824
    }
825
826
    /**
827
     * @throws BinaryDataReaderException
828
     */
829 1
    protected function getEnum(ColumnDTO $columnDTO): string
830
    {
831 1
        $value = $this->binaryDataReader->readUIntBySize($columnDTO->getSize()) - 1;
832
833
        // check if given value exists in enums, if there not existing enum mysql returns empty string.
834 1
        if (array_key_exists($value, $columnDTO->getEnumValues())) {
835 1
            return $columnDTO->getEnumValues()[$value];
836
        }
837
838 1
        return '';
839
    }
840
841
    /**
842
     * @throws BinaryDataReaderException
843
     */
844 1
    protected function getSet(ColumnDTO $columnDTO): array
845
    {
846
        // we read set columns as a bitmap telling us which options are enabled
847 1
        $bitMask = $this->binaryDataReader->readUIntBySize($columnDTO->getSize());
848 1
        $sets = [];
849 1
        foreach ($columnDTO->getSetValues() as $k => $item) {
850 1
            if ($bitMask & (2 ** $k)) {
851 1
                $sets[] = $item;
852
            }
853
        }
854
855 1
        return $sets;
856
    }
857
858 1
    protected function getBit(ColumnDTO $columnDTO): string
859
    {
860 1
        $res = '';
861 1
        for ($byte = 0; $byte < $columnDTO->getBytes(); ++$byte) {
862 1
            $currentByte = '';
863 1
            $data = $this->binaryDataReader->readUInt8();
864 1
            if (0 === $byte) {
865 1
                if (1 === $columnDTO->getBytes()) {
866 1
                    $end = $columnDTO->getBits();
867
                } else {
868 1
                    $end = $columnDTO->getBits() % 8;
869 1
                    if (0 === $end) {
870 1
                        $end = 8;
871
                    }
872
                }
873
            } else {
874 1
                $end = 8;
875
            }
876
877 1
            for ($bit = 0; $bit < $end; ++$bit) {
878 1
                if ($data & (1 << $bit)) {
879 1
                    $currentByte .= '1';
880
                } else {
881 1
                    $currentByte .= '0';
882
                }
883
            }
884 1
            $res .= strrev($currentByte);
885
        }
886
887 1
        return $res;
888
    }
889
890
    /**
891
     * @throws InvalidArgumentException
892
     * @throws BinaryDataReaderException
893
     * @throws JsonBinaryDecoderException
894
     * @throws MySQLReplicationException
895
     */
896 1
    public function makeDeleteRowsDTO(): ?DeleteRowsDTO
897
    {
898 1
        if (! $this->rowInit()) {
899
            return null;
900
        }
901
902 1
        $values = $this->getValues();
903
904 1
        return new DeleteRowsDTO(
905 1
            $this->eventInfo,
906 1
            $this->currentTableMap,
907 1
            count($values),
908 1
            $values
909 1
        );
910
    }
911
912
    /**
913
     * @throws InvalidArgumentException
914
     * @throws BinaryDataReaderException
915
     * @throws JsonBinaryDecoderException
916
     * @throws MySQLReplicationException
917
     */
918 1
    public function makeUpdateRowsDTO(): ?UpdateRowsDTO
919
    {
920 1
        if (! $this->rowInit()) {
921
            return null;
922
        }
923
924 1
        $columnsBinarySize = $this->getColumnsBinarySize($this->currentTableMap->getColumnsAmount());
925 1
        $beforeBinaryData = $this->binaryDataReader->read($columnsBinarySize);
926 1
        $afterBinaryData = $this->binaryDataReader->read($columnsBinarySize);
927
928 1
        $values = [];
929 1
        while (! $this->binaryDataReader->isComplete($this->eventInfo->getSizeNoHeader())) {
930 1
            $values[] = [
931 1
                'before' => $this->getColumnData($beforeBinaryData),
932 1
                'after' => $this->getColumnData($afterBinaryData)
933 1
            ];
934
        }
935
936 1
        return new UpdateRowsDTO(
937 1
            $this->eventInfo,
938 1
            $this->currentTableMap,
939 1
            count($values),
940 1
            $values
941 1
        );
942
    }
943
}
944