Passed
Pull Request — master (#8)
by Moln
05:05
created

RowEvent::getDate()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5.025

Importance

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