RowEvent   F
last analyzed

Complexity

Total Complexity 113

Size/Duplication

Total Lines 905
Duplicated Lines 0 %

Test Coverage

Coverage 91.33%

Importance

Changes 12
Bugs 1 Features 0
Metric Value
eloc 320
dl 0
loc 905
ccs 295
cts 323
cp 0.9133
rs 2
c 12
b 1
f 0
wmc 113

25 Methods

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

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 54
    public function __construct(
297
        RepositoryInterface $repository,
298
        BinaryDataReader $binaryDataReader,
299
        EventInfo $eventInfo,
300
        CacheInterface $cache
301
    ) {
302 54
        parent::__construct($eventInfo, $binaryDataReader);
303
304 54
        $this->repository = $repository;
305 54
        $this->cache = $cache;
306 54
    }
307
308
    /**
309
     * This describe the structure of a table.
310
     * It's send before a change append on a table.
311
     * A end user of the lib should have no usage of this
312
     * @throws BinaryDataReaderException
313
     * @throws InvalidArgumentException
314
     */
315 54
    public function makeTableMapDTO(): ?TableMapDTO
316
    {
317 54
        $data = [];
318 54
        $data['table_id'] = $this->binaryDataReader->readTableId();
319 54
        $this->binaryDataReader->advance(2);
320 54
        $data['schema_length'] = $this->binaryDataReader->readUInt8();
321 54
        $data['schema_name'] = $this->binaryDataReader->read($data['schema_length']);
322
323 54
        if (Config::checkDataBasesOnly($data['schema_name'])) {
324
            return null;
325
        }
326
327 54
        $this->binaryDataReader->advance(1);
328 54
        $data['table_length'] = $this->binaryDataReader->readUInt8();
329 54
        $data['table_name'] = $this->binaryDataReader->read($data['table_length']);
330
331 54
        if (Config::checkTablesOnly($data['table_name'])) {
332 1
            return null;
333
        }
334
335 54
        $this->binaryDataReader->advance(1);
336 54
        $data['columns_amount'] = (int)$this->binaryDataReader->readCodedBinary();
337 54
        $data['column_types'] = $this->binaryDataReader->read($data['columns_amount']);
338
339 54
        if ($this->cache->has($data['table_id'])) {
340 2
            return new TableMapDTO($this->eventInfo, $this->cache->get($data['table_id']));
341
        }
342
343 54
        $this->binaryDataReader->readCodedBinary();
344
345 54
        $fieldDTOCollection = $this->repository->getFields($data['schema_name'], $data['table_name']);
346 54
        $columnDTOCollection = new ColumnDTOCollection();
347
        // if you drop tables and parse of logs you will get empty scheme
348 54
        if (!$fieldDTOCollection->isEmpty()) {
349 53
            $columnLength = strlen($data['column_types']);
350 53
            for ($offset = 0; $offset < $columnLength; ++$offset) {
351
                // this a dirty hack to prevent row events containing columns which have been dropped
352 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

352
                if ($fieldDTOCollection->offsetExists(/** @scrutinizer ignore-type */ $offset)) {
Loading history...
353 53
                    $type = ord($data['column_types'][$offset]);
354
                } else {
355 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

355
                    $fieldDTOCollection->offsetSet(/** @scrutinizer ignore-type */ $offset, FieldDTO::makeDummy($offset));
Loading history...
356 1
                    $type = ConstFieldType::IGNORE;
357
                }
358
359
                /** @var FieldDTO $fieldDTO */
360 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

360
                $fieldDTO = $fieldDTOCollection->offsetGet(/** @scrutinizer ignore-type */ $offset);
Loading history...
361 53
                if (null !== $fieldDTO) {
362 53
                    $columnDTOCollection->set($offset, ColumnDTO::make($type, $fieldDTO, $this->binaryDataReader));
363
                }
364
            }
365
        }
366
367 54
        $tableMap = new TableMap(
368 54
            $data['schema_name'],
369 54
            $data['table_name'],
370 54
            $data['table_id'],
371 54
            $data['columns_amount'],
372
            $columnDTOCollection
373
        );
374
375 54
        $this->cache->set($data['table_id'], $tableMap);
376
377 54
        return new TableMapDTO($this->eventInfo, $tableMap);
378
    }
379
380
    /**
381
     * @throws BinaryDataReaderException
382
     * @throws InvalidArgumentException
383
     * @throws JsonBinaryDecoderException
384
     * @throws MySQLReplicationException
385
     */
386 53
    public function makeWriteRowsDTO(): ?WriteRowsDTO
387
    {
388 53
        if (!$this->rowInit()) {
389 1
            return null;
390
        }
391
392 53
        $values = $this->getValues();
393
394 53
        return new WriteRowsDTO(
395 53
            $this->eventInfo,
396 53
            $this->currentTableMap,
397 53
            count($values),
398
            $values
399
        );
400
    }
401
402
    /**
403
     * @throws InvalidArgumentException
404
     * @throws BinaryDataReaderException
405
     */
406 53
    protected function rowInit(): bool
407
    {
408 53
        $tableId = $this->binaryDataReader->readTableId();
409 53
        $this->binaryDataReader->advance(2);
410
411 53
        if (in_array(
412 53
            $this->eventInfo->getType(), [
413 53
            ConstEventType::DELETE_ROWS_EVENT_V2,
414
            ConstEventType::WRITE_ROWS_EVENT_V2,
415
            ConstEventType::UPDATE_ROWS_EVENT_V2
416 53
        ], true
417
        )) {
418 53
            $this->binaryDataReader->read((int)($this->binaryDataReader->readUInt16() / 8));
419
        }
420
421 53
        $this->binaryDataReader->readCodedBinary();
422
423 53
        if ($this->cache->has($tableId)) {
424
            /** @var TableMap $tableMap */
425 53
            $this->currentTableMap = $this->cache->get($tableId);
426
427 53
            return true;
428
        }
429
430 1
        return false;
431
    }
432
433
    /**
434
     * @throws BinaryDataReaderException
435
     * @throws JsonBinaryDecoderException
436
     * @throws MySQLReplicationException
437
     */
438 53
    protected function getValues(): array
439
    {
440
        // if we don't get columns from information schema we don't know how to assign them
441 53
        if ($this->currentTableMap === null || $this->currentTableMap->getColumnDTOCollection()->isEmpty()) {
442 1
            return [];
443
        }
444
445 52
        $binaryData = $this->binaryDataReader->read(
446 52
            $this->getColumnsBinarySize($this->currentTableMap->getColumnsAmount())
447
        );
448
449 52
        $values = [];
450 52
        while (!$this->binaryDataReader->isComplete($this->eventInfo->getSizeNoHeader())) {
451 52
            $values[] = $this->getColumnData($binaryData);
452
        }
453
454 52
        return $values;
455
    }
456
457 52
    protected function getColumnsBinarySize(int $columnsAmount): int
458
    {
459 52
        return (int)(($columnsAmount + 7) / 8);
460
    }
461
462
    /**
463
     * @throws BinaryDataReaderException
464
     * @throws JsonBinaryDecoderException
465
     * @throws MySQLReplicationException
466
     */
467 52
    protected function getColumnData(string $colsBitmap): array
468
    {
469 52
        if (null === $this->currentTableMap) {
470
            throw new RuntimeException('Current table map is missing!');
471
        }
472
473 52
        $values = [];
474
475
        // null bitmap length = (bits set in 'columns-present-bitmap'+7)/8
476
        // see http://dev.mysql.com/doc/internals/en/rows-event.html
477 52
        $nullBitmap = $this->binaryDataReader->read($this->getColumnsBinarySize($this->bitCount($colsBitmap)));
478 52
        $nullBitmapIndex = 0;
479
480 52
        foreach ($this->currentTableMap->getColumnDTOCollection() as $i => $columnDTO) {
481 52
            $name = $columnDTO->getName();
482 52
            $type = $columnDTO->getType();
483
484 52
            if (0 === $this->bitGet($colsBitmap, $i)) {
485
                $values[$name] = null;
486
                continue;
487
            }
488
489 52
            if ($this->checkNull($nullBitmap, $nullBitmapIndex)) {
490 6
                $values[$name] = null;
491 51
            } else if ($type === ConstFieldType::IGNORE) {
492 1
                $this->binaryDataReader->advance($columnDTO->getLengthSize());
493 1
                $values[$name] = null;
494 51
            } else if ($type === ConstFieldType::TINY) {
495 5
                if ($columnDTO->isUnsigned()) {
496 4
                    $values[$name] = $this->binaryDataReader->readUInt8();
497
                } else {
498 5
                    $values[$name] = $this->binaryDataReader->readInt8();
499
                }
500 46
            } else if ($type === ConstFieldType::SHORT) {
501 1
                if ($columnDTO->isUnsigned()) {
502 1
                    $values[$name] = $this->binaryDataReader->readUInt16();
503
                } else {
504 1
                    $values[$name] = $this->binaryDataReader->readInt16();
505
                }
506 45
            } else if ($type === ConstFieldType::LONG) {
507 10
                if ($columnDTO->isUnsigned()) {
508 1
                    $values[$name] = $this->binaryDataReader->readUInt32();
509
                } else {
510 10
                    $values[$name] = $this->binaryDataReader->readInt32();
511
                }
512 43
            } else if ($type === ConstFieldType::LONGLONG) {
513 1
                if ($columnDTO->isUnsigned()) {
514 1
                    $values[$name] = $this->binaryDataReader->readUInt64();
515
                } else {
516 1
                    $values[$name] = $this->binaryDataReader->readInt64();
517
                }
518 42
            } else if ($type === ConstFieldType::INT24) {
519 1
                if ($columnDTO->isUnsigned()) {
520 1
                    $values[$name] = $this->binaryDataReader->readUInt24();
521
                } else {
522 1
                    $values[$name] = $this->binaryDataReader->readInt24();
523
                }
524 41
            } else if ($type === ConstFieldType::FLOAT) {
525
                // http://dev.mysql.com/doc/refman/5.7/en/floating-point-types.html FLOAT(7,4)
526 1
                $values[$name] = round($this->binaryDataReader->readFloat(), 4);
527 40
            } else if ($type === ConstFieldType::DOUBLE) {
528 1
                $values[$name] = $this->binaryDataReader->readDouble();
529 39
            } else if ($type === ConstFieldType::VARCHAR || $type === ConstFieldType::STRING) {
530 8
                $values[$name] = $columnDTO->getMaxLength() > 255 ? $this->getString(2) : $this->getString(1);
531 31
            } else if ($type === ConstFieldType::NEWDECIMAL) {
532 10
                $values[$name] = $this->getDecimal($columnDTO);
533 21
            } else if ($type === ConstFieldType::BLOB) {
534 4
                $values[$name] = $this->getString($columnDTO->getLengthSize());
535 17
            } else if ($type === ConstFieldType::DATETIME) {
536
                $values[$name] = $this->getDatetime();
537 17
            } else if ($type === ConstFieldType::DATETIME2) {
538 4
                $values[$name] = $this->getDatetime2($columnDTO);
539 13
            } else if ($type === ConstFieldType::TIMESTAMP) {
540
                $values[$name] = date('Y-m-d H:i:s', $this->binaryDataReader->readUInt32());
541 13
            } else if ($type === ConstFieldType::TIME) {
542
                $values[$name] = $this->getTime();
543 13
            } else if ($type === ConstFieldType::TIME2) {
544 2
                $values[$name] = $this->getTime2($columnDTO);
545 11
            } else if ($type === ConstFieldType::TIMESTAMP2) {
546 2
                $values[$name] = $this->getTimestamp2($columnDTO);
547 9
            } else if ($type === ConstFieldType::DATE) {
548 4
                $values[$name] = $this->getDate();
549 5
            } else if ($type === ConstFieldType::YEAR) {
550
                // https://dev.mysql.com/doc/refman/5.7/en/year.html
551 1
                $year = $this->binaryDataReader->readUInt8();
552 1
                $values[$name] = 0 === $year ? null : 1900 + $year;
553 4
            } else if ($type === ConstFieldType::ENUM) {
554 1
                $values[$name] = $this->getEnum($columnDTO);
555 3
            } else if ($type === ConstFieldType::SET) {
556 1
                $values[$name] = $this->getSet($columnDTO);
557 2
            } else if ($type === ConstFieldType::BIT) {
558 1
                $values[$name] = $this->getBit($columnDTO);
559 1
            } else if ($type === ConstFieldType::GEOMETRY) {
560 1
                $values[$name] = $this->getString($columnDTO->getLengthSize());
561
            } else if ($type === ConstFieldType::JSON) {
562
                $values[$name] = JsonBinaryDecoderService::makeJsonBinaryDecoder($this->getString($columnDTO->getLengthSize()))->parseToString();
563
            } else {
564
                throw new MySQLReplicationException('Unknown row type: ' . $type);
565
            }
566
567 52
            ++$nullBitmapIndex;
568
        }
569
570 52
        return $values;
571
    }
572
573 52
    protected function bitCount(string $bitmap): int
574
    {
575 52
        $n = 0;
576 52
        $bitmapLength = strlen($bitmap);
577 52
        for ($i = 0; $i < $bitmapLength; ++$i) {
578 52
            $bit = $bitmap[$i];
579 52
            if (is_string($bit)) {
580 52
                $bit = ord($bit);
581
            }
582 52
            $n += self::$bitCountInByte[$bit];
583
        }
584
585 52
        return $n;
586
    }
587
588 52
    protected function bitGet(string $bitmap, int $position): int
589
    {
590 52
        return $this->getBitFromBitmap($bitmap, $position) & (1 << ($position & 7));
591
    }
592
593 52
    protected function getBitFromBitmap(string $bitmap, int $position): int
594
    {
595 52
        $bit = $bitmap[(int)($position / 8)];
596 52
        if (is_string($bit)) {
597 52
            $bit = ord($bit);
598
        }
599
600 52
        return $bit;
601
    }
602
603 52
    protected function checkNull(string $nullBitmap, int $position): int
604
    {
605 52
        return $this->getBitFromBitmap($nullBitmap, $position) & (1 << ($position % 8));
606
    }
607
608
    /**
609
     * @throws BinaryDataReaderException
610
     */
611 13
    protected function getString(int $size): string
612
    {
613 13
        return $this->binaryDataReader->readLengthString($size);
614
    }
615
616
    /**
617
     * Read MySQL's new decimal format introduced in MySQL 5
618
     * https://dev.mysql.com/doc/refman/5.6/en/precision-math-decimal-characteristics.html
619
     * @throws BinaryDataReaderException
620
     */
621 10
    protected function getDecimal(ColumnDTO $columnDTO): string
622
    {
623 10
        $digitsPerInteger = 9;
624 10
        $compressedBytes = [0, 1, 1, 2, 2, 3, 3, 4, 4, 4];
625 10
        $integral = $columnDTO->getPrecision() - $columnDTO->getDecimals();
626 10
        $unCompIntegral = (int)($integral / $digitsPerInteger);
627 10
        $unCompFractional = (int)($columnDTO->getDecimals() / $digitsPerInteger);
628 10
        $compIntegral = $integral - ($unCompIntegral * $digitsPerInteger);
629 10
        $compFractional = $columnDTO->getDecimals() - ($unCompFractional * $digitsPerInteger);
630
631 10
        $value = $this->binaryDataReader->readUInt8();
632 10
        if (0 !== ($value & 0x80)) {
633 7
            $mask = 0;
634 7
            $res = '';
635
        } else {
636 3
            $mask = -1;
637 3
            $res = '-';
638
        }
639 10
        $this->binaryDataReader->unread(pack('C', $value ^ 0x80));
640
641 10
        $size = $compressedBytes[$compIntegral];
642 10
        if ($size > 0) {
643 10
            $value = $this->binaryDataReader->readIntBeBySize($size) ^ $mask;
644 10
            $res .= $value;
645
        }
646
647 10
        for ($i = 0; $i < $unCompIntegral; ++$i) {
648 9
            $value = $this->binaryDataReader->readInt32Be() ^ $mask;
649 9
            $res .= sprintf('%09d', $value);
650
        }
651
652 10
        $res .= '.';
653
654 10
        for ($i = 0; $i < $unCompFractional; ++$i) {
655 4
            $value = $this->binaryDataReader->readInt32Be() ^ $mask;
656 4
            $res .= sprintf('%09d', $value);
657
        }
658
659 10
        $size = $compressedBytes[$compFractional];
660 10
        if ($size > 0) {
661 5
            $value = $this->binaryDataReader->readIntBeBySize($size) ^ $mask;
662 5
            $res .= sprintf('%0' . $compFractional . 'd', $value);
663
        }
664
665 10
        return bcmul($res, '1', $columnDTO->getDecimals());
666
    }
667
668
    protected function getDatetime(): ?string
669
    {
670
        $value = $this->binaryDataReader->readUInt64();
671
        // nasty mysql 0000-00-00 dates
672
        if ('0' === $value) {
673
            return null;
674
        }
675
676
        $date = DateTime::createFromFormat('YmdHis', $value)->format('Y-m-d H:i:s');
677
        if (array_sum(DateTime::getLastErrors()) > 0) {
678
            return null;
679
        }
680
681
        return $date;
682
    }
683
684
    /**
685
     * Date Time
686
     * 1 bit  sign           (1= non-negative, 0= negative)
687
     * 17 bits year*13+month  (year 0-9999, month 0-12)
688
     * 5 bits day            (0-31)
689
     * 5 bits hour           (0-23)
690
     * 6 bits minute         (0-59)
691
     * 6 bits second         (0-59)
692
     * ---------------------------
693
     * 40 bits = 5 bytes
694
     *
695
     * @throws BinaryDataReaderException
696
     *
697
     * @link https://dev.mysql.com/doc/internals/en/date-and-time-data-type-representation.html
698
     */
699 4
    protected function getDatetime2(ColumnDTO $columnDTO): ?string
700
    {
701 4
        $data = $this->binaryDataReader->readIntBeBySize(5);
702
703 4
        $yearMonth = $this->binaryDataReader->getBinarySlice($data, 1, 17, 40);
704
705 4
        $year = (int)($yearMonth / 13);
706 4
        $month = $yearMonth % 13;
707 4
        $day = $this->binaryDataReader->getBinarySlice($data, 18, 5, 40);
708 4
        $hour = $this->binaryDataReader->getBinarySlice($data, 23, 5, 40);
709 4
        $minute = $this->binaryDataReader->getBinarySlice($data, 28, 6, 40);
710 4
        $second = $this->binaryDataReader->getBinarySlice($data, 34, 6, 40);
711 4
        $fsp = $this->getFSP($columnDTO);
712
713
        try {
714 4
            $date = new DateTime($year . '-' . $month . '-' . $day . ' ' . $hour . ':' . $minute . ':' . $second);
715
        } catch (Exception $exception) {
716
            return null;
717
        }
718 4
        if (array_sum(DateTime::getLastErrors()) > 0) {
719 3
            return null;
720
        }
721
722 1
        return $date->format('Y-m-d H:i:s') . $fsp;
723
    }
724
725
    /**
726
     * @throws BinaryDataReaderException
727
     * @link https://dev.mysql.com/doc/internals/en/date-and-time-data-type-representation.html
728
     */
729 8
    protected function getFSP(ColumnDTO $columnDTO): string
730
    {
731 8
        $read = 0;
732 8
        $time = '';
733 8
        $fsp = $columnDTO->getFsp();
734 8
        if ($fsp === 1 || $fsp === 2) {
735 1
            $read = 1;
736 8
        } else if ($fsp === 3 || $fsp === 4) {
737 1
            $read = 2;
738 8
        } else if ($fsp === 5 || $fsp === 6) {
739 1
            $read = 3;
740
        }
741 8
        if ($read > 0) {
742 1
            $microsecond = $this->binaryDataReader->readIntBeBySize($read);
743 1
            if ($fsp % 2) {
744 1
                $microsecond = (int)($microsecond / 10);
745
746
            }
747 1
            $time = $microsecond * (10 ** (6 - $fsp));
748
        }
749
750 8
        return (string)$time;
751
    }
752
753
    protected function getTime(): string
754
    {
755
        $data = $this->binaryDataReader->readUInt24();
756
        if (0 === $data) {
757
            return '00:00:00';
758
        }
759
760
        return sprintf('%s%02d:%02d:%02d', $data < 0 ? '-' : '', $data / 10000, ($data % 10000) / 100, $data % 100);
761
    }
762
763
    /**
764
     * TIME encoding for non fractional part:
765
     * 1 bit sign    (1= non-negative, 0= negative)
766
     * 1 bit unused  (reserved for future extensions)
767
     * 10 bits hour   (0-838)
768
     * 6 bits minute (0-59)
769
     * 6 bits second (0-59)
770
     * ---------------------
771
     * 24 bits = 3 bytes
772
     *
773
     * @throws BinaryDataReaderException
774
     */
775 2
    protected function getTime2(ColumnDTO $columnDTO): string
776
    {
777 2
        $data = $this->binaryDataReader->readInt24Be();
778
779 2
        $hour = $this->binaryDataReader->getBinarySlice($data, 2, 10, 24);
780 2
        $minute = $this->binaryDataReader->getBinarySlice($data, 12, 6, 24);
781 2
        $second = $this->binaryDataReader->getBinarySlice($data, 18, 6, 24);
782
783 2
        return (new DateTime())->setTime($hour, $minute, $second)->format('H:i:s') . $this->getFSP($columnDTO);
784
    }
785
786
    /**
787
     * @throws BinaryDataReaderException
788
     */
789 2
    protected function getTimestamp2(ColumnDTO $columnDTO): string
790
    {
791 2
        $datetime = (string)date('Y-m-d H:i:s', $this->binaryDataReader->readInt32Be());
792 2
        $fsp = $this->getFSP($columnDTO);
793 2
        if ('' !== $fsp) {
794 1
            $datetime .= '.' . $fsp;
795
        }
796
797 2
        return $datetime;
798
    }
799
800 4
    protected function getDate(): ?string
801
    {
802 4
        $time = $this->binaryDataReader->readUInt24();
803 4
        if (0 === $time) {
804
            return null;
805
        }
806
807 4
        $year = ($time & ((1 << 15) - 1) << 9) >> 9;
808 4
        $month = ($time & ((1 << 4) - 1) << 5) >> 5;
809 4
        $day = ($time & ((1 << 5) - 1));
810 4
        if ($year === 0 || $month === 0 || $day === 0) {
811 3
            return null;
812
        }
813
814 1
        return (new DateTime())->setDate($year, $month, $day)->format('Y-m-d');
815
    }
816
817
    /**
818
     * @throws BinaryDataReaderException
819
     */
820 1
    protected function getEnum(ColumnDTO $columnDTO): string
821
    {
822 1
        $value = $this->binaryDataReader->readUIntBySize($columnDTO->getSize()) - 1;
823
824
        // check if given value exists in enums, if there not existing enum mysql returns empty string.
825 1
        if (array_key_exists($value, $columnDTO->getEnumValues())) {
826 1
            return $columnDTO->getEnumValues()[$value];
827
        }
828
829 1
        return '';
830
    }
831
832
    /**
833
     * @throws BinaryDataReaderException
834
     */
835 1
    protected function getSet(ColumnDTO $columnDTO): array
836
    {
837
        // we read set columns as a bitmap telling us which options are enabled
838 1
        $bit_mask = $this->binaryDataReader->readUIntBySize($columnDTO->getSize());
839 1
        $sets = [];
840 1
        foreach ($columnDTO->getSetValues() as $k => $item) {
841 1
            if ($bit_mask & (2 ** $k)) {
842 1
                $sets[] = $item;
843
            }
844
        }
845
846 1
        return $sets;
847
    }
848
849 1
    protected function getBit(ColumnDTO $columnDTO): string
850
    {
851 1
        $res = '';
852 1
        for ($byte = 0; $byte < $columnDTO->getBytes(); ++$byte) {
853 1
            $current_byte = '';
854 1
            $data = $this->binaryDataReader->readUInt8();
855 1
            if (0 === $byte) {
856 1
                if (1 === $columnDTO->getBytes()) {
857 1
                    $end = $columnDTO->getBits();
858
                } else {
859 1
                    $end = $columnDTO->getBits() % 8;
860 1
                    if (0 === $end) {
861 1
                        $end = 8;
862
                    }
863
                }
864
            } else {
865 1
                $end = 8;
866
            }
867
868 1
            for ($bit = 0; $bit < $end; ++$bit) {
869 1
                if ($data & (1 << $bit)) {
870 1
                    $current_byte .= '1';
871
                } else {
872 1
                    $current_byte .= '0';
873
                }
874
875
            }
876 1
            $res .= strrev($current_byte);
877
        }
878
879 1
        return $res;
880
    }
881
882
    /**
883
     * @throws InvalidArgumentException
884
     * @throws BinaryDataReaderException
885
     * @throws JsonBinaryDecoderException
886
     * @throws MySQLReplicationException
887
     */
888 1
    public function makeDeleteRowsDTO(): ?DeleteRowsDTO
889
    {
890 1
        if (!$this->rowInit()) {
891
            return null;
892
        }
893
894 1
        $values = $this->getValues();
895
896 1
        return new DeleteRowsDTO(
897 1
            $this->eventInfo,
898 1
            $this->currentTableMap,
899 1
            count($values),
900
            $values
901
        );
902
    }
903
904
    /**
905
     * @throws InvalidArgumentException
906
     * @throws BinaryDataReaderException
907
     * @throws JsonBinaryDecoderException
908
     * @throws MySQLReplicationException
909
     */
910 1
    public function makeUpdateRowsDTO(): ?UpdateRowsDTO
911
    {
912 1
        if (!$this->rowInit()) {
913
            return null;
914
        }
915
916 1
        $columnsBinarySize = $this->getColumnsBinarySize($this->currentTableMap->getColumnsAmount());
917 1
        $beforeBinaryData = $this->binaryDataReader->read($columnsBinarySize);
918 1
        $afterBinaryData = $this->binaryDataReader->read($columnsBinarySize);
919
920 1
        $values = [];
921 1
        while (!$this->binaryDataReader->isComplete($this->eventInfo->getSizeNoHeader())) {
922 1
            $values[] = [
923 1
                'before' => $this->getColumnData($beforeBinaryData),
924 1
                'after' => $this->getColumnData($afterBinaryData)
925
            ];
926
        }
927
928 1
        return new UpdateRowsDTO(
929 1
            $this->eventInfo,
930 1
            $this->currentTableMap,
931 1
            count($values),
932
            $values
933
        );
934
    }
935
}
936