Completed
Branch master (e178de)
by kacper
06:21
created

RowEvent::getTime()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

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

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

353
        $data['column_types'] = $this->binaryDataReader->read(/** @scrutinizer ignore-type */ $data['columns_amount']);
Loading history...
354
355 53
        if ($this->cache->has($data['table_id'])) {
356 2
            return new TableMapDTO($this->eventInfo, $this->cache->get($data['table_id']));
357
        }
358
359 53
        $this->binaryDataReader->readCodedBinary();
360
361 53
        $columns = $this->repository->getFields($data['schema_name'], $data['table_name']);
362
363 53
        $fields = [];
364
        // if you drop tables and parse of logs you will get empty scheme
365 53
        if (!empty($columns)) {
366 52
            $columnLength = strlen($data['column_types']);
367 52
            for ($i = 0; $i < $columnLength; ++$i) {
368
                // this a dirty hack to prevent row events containing columns which have been dropped
369 52
                if (!isset($columns[$i])) {
370 1
                    $columns[$i] = [
371 1
                        'COLUMN_NAME' => 'DROPPED_COLUMN_' . $i,
372 1
                        'COLLATION_NAME' => null,
373 1
                        'CHARACTER_SET_NAME' => null,
374 1
                        'COLUMN_COMMENT' => null,
375 1
                        'COLUMN_TYPE' => 'BLOB',
376
                        'COLUMN_KEY' => ''
377 1
                    ];
378
379 1
                    $type = ConstFieldType::IGNORE;
380 1
                } else {
381 52
                    $type = ord($data['column_types'][$i]);
382
                }
383
384 52
                $fields[$i] = Columns::parse($type, $columns[$i], $this->binaryDataReader);
385 52
            }
386 52
        }
387
388 53
        $tableMap = new TableMap(
389 53
            $data['schema_name'],
390 53
            $data['table_name'],
391 53
            $data['table_id'],
0 ignored issues
show
Bug introduced by
$data['table_id'] of type string is incompatible with the type integer expected by parameter $tableId of MySQLReplication\Event\R...TableMap::__construct(). ( Ignorable by Annotation )

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

391
            /** @scrutinizer ignore-type */ $data['table_id'],
Loading history...
392 53
            $data['columns_amount'],
0 ignored issues
show
Bug introduced by
It seems like $data['columns_amount'] can also be of type string; however, parameter $columnsAmount of MySQLReplication\Event\R...TableMap::__construct() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

392
            /** @scrutinizer ignore-type */ $data['columns_amount'],
Loading history...
393
            $fields
394 53
        );
395
396 53
        $this->cache->set($data['table_id'], $tableMap);
397
398 53
        return new TableMapDTO($this->eventInfo, $tableMap);
399
    }
400
401
    /**
402
     * @return WriteRowsDTO|null
403
     * @throws InvalidArgumentException
404
     * @throws BinaryDataReaderException
405
     * @throws JsonBinaryDecoderException
406
     * @throws MySQLReplicationException
407
     */
408 52
    public function makeWriteRowsDTO()
409
    {
410 52
        if (!$this->rowInit()) {
411 1
            return null;
412
        }
413
414 52
        $values = $this->getValues();
415
416 52
        return new WriteRowsDTO(
417 52
            $this->eventInfo,
418 52
            $this->currentTableMap,
419 52
            count($values),
420
            $values
421 52
        );
422
    }
423
424
    /**
425
     * @return bool
426
     * @throws InvalidArgumentException
427
     * @throws BinaryDataReaderException
428
     */
429 52
    protected function rowInit()
430
    {
431 52
        $tableId = $this->binaryDataReader->readTableId();
432 52
        $this->binaryDataReader->advance(2);
433
434 52
        if (in_array(
435 52
            $this->eventInfo->getType(), [
436 52
            ConstEventType::DELETE_ROWS_EVENT_V2,
437 52
            ConstEventType::WRITE_ROWS_EVENT_V2,
438
            ConstEventType::UPDATE_ROWS_EVENT_V2
439 52
        ], true
440 52
        )) {
441 52
            $this->binaryDataReader->read($this->binaryDataReader->readUInt16() / 8);
442 52
        }
443
444 52
        $this->binaryDataReader->readCodedBinary();
445
446 52
        if ($this->cache->has($tableId)) {
447
            /** @var TableMap $tableMap */
448 52
            $this->currentTableMap = $this->cache->get($tableId);
449
450 52
            return true;
451
        }
452
453 1
        return false;
454
    }
455
456
    /**
457
     * @return array
458
     * @throws BinaryDataReaderException
459
     * @throws JsonBinaryDecoderException
460
     * @throws MySQLReplicationException
461
     */
462 52
    protected function getValues()
463
    {
464
        // if we don't get columns from information schema we don't know how to assign them
465 52
        if ([] === $this->currentTableMap->getFields()) {
466 1
            return [];
467
        }
468
469 51
        $binaryData = $this->binaryDataReader->read(
470 51
            $this->getColumnsBinarySize($this->currentTableMap->getColumnsAmount())
471 51
        );
472
473 51
        $values = [];
474 51
        while (!$this->binaryDataReader->isComplete($this->eventInfo->getSizeNoHeader())) {
0 ignored issues
show
Bug introduced by
$this->eventInfo->getSizeNoHeader() of type string is incompatible with the type integer expected by parameter $size of MySQLReplication\BinaryD...ataReader::isComplete(). ( Ignorable by Annotation )

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

474
        while (!$this->binaryDataReader->isComplete(/** @scrutinizer ignore-type */ $this->eventInfo->getSizeNoHeader())) {
Loading history...
475 51
            $values[] = $this->getColumnData($binaryData);
0 ignored issues
show
Bug introduced by
$binaryData of type string is incompatible with the type integer expected by parameter $colsBitmap of MySQLReplication\Event\R...wEvent::getColumnData(). ( Ignorable by Annotation )

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

475
            $values[] = $this->getColumnData(/** @scrutinizer ignore-type */ $binaryData);
Loading history...
476 51
        }
477
478 51
        return $values;
479
    }
480
481
    /**
482
     * @param int $columnsAmount
483
     * @return int
484
     */
485 51
    protected function getColumnsBinarySize($columnsAmount)
486
    {
487 51
        return (int)(($columnsAmount + 7) / 8);
488
    }
489
490
    /**
491
     * @param int $colsBitmap
492
     * @return array
493
     * @throws BinaryDataReaderException
494
     * @throws JsonBinaryDecoderException
495
     * @throws MySQLReplicationException
496
     */
497 51
    protected function getColumnData($colsBitmap)
498
    {
499 51
        $values = [];
500
501
        // null bitmap length = (bits set in 'columns-present-bitmap'+7)/8
502
        // see http://dev.mysql.com/doc/internals/en/rows-event.html
503 51
        $null_bitmap = $this->binaryDataReader->read($this->getColumnsBinarySize($this->bitCount($colsBitmap)));
504 51
        $nullBitmapIndex = 0;
505
506 51
        foreach ($this->currentTableMap->getFields() as $i => $column) {
507 51
            $name = $column['name'];
508
509 51
            if (0 === $this->bitGet($colsBitmap, $i)) {
510
                $values[$name] = null;
511
                continue;
512
            }
513
514 51
            if ($this->checkNull($null_bitmap, $nullBitmapIndex)) {
515 5
                $values[$name] = null;
516 51
            } else if ($column['type'] === ConstFieldType::IGNORE) {
517 1
                $this->binaryDataReader->advance($column['length_size']);
518 1
                $values[$name] = null;
519 51
            } else if ($column['type'] === ConstFieldType::TINY) {
520 5
                if ($column['unsigned']) {
521 4
                    $values[$name] = $this->binaryDataReader->readUInt8();
522 4
                } else {
523 4
                    $values[$name] = $this->binaryDataReader->readInt8();
524
                }
525 51
            } else if ($column['type'] === ConstFieldType::SHORT) {
526 1
                if ($column['unsigned']) {
527 1
                    $values[$name] = $this->binaryDataReader->readUInt16();
528 1
                } else {
529 1
                    $values[$name] = $this->binaryDataReader->readInt16();
530
                }
531 46
            } else if ($column['type'] === ConstFieldType::LONG) {
532 10
                if ($column['unsigned']) {
533 1
                    $values[$name] = $this->binaryDataReader->readUInt32();
534 1
                } else {
535 10
                    $values[$name] = $this->binaryDataReader->readInt32();
536
                }
537 45
            } else if ($column['type'] === ConstFieldType::LONGLONG) {
538 1
                if ($column['unsigned']) {
539 1
                    $values[$name] = $this->binaryDataReader->readUInt64();
540 1
                } else {
541 1
                    $values[$name] = $this->binaryDataReader->readInt64();
542
                }
543 43
            } else if ($column['type'] === ConstFieldType::INT24) {
544 1
                if ($column['unsigned']) {
545 1
                    $values[$name] = $this->binaryDataReader->readUInt24();
546 1
                } else {
547 1
                    $values[$name] = $this->binaryDataReader->readInt24();
548
                }
549 42
            } else if ($column['type'] === ConstFieldType::FLOAT) {
550
                // http://dev.mysql.com/doc/refman/5.7/en/floating-point-types.html FLOAT(7,4)
551 1
                $values[$name] = round($this->binaryDataReader->readFloat(), 4);
552 41
            } else if ($column['type'] === ConstFieldType::DOUBLE) {
553 1
                $values[$name] = $this->binaryDataReader->readDouble();
554 40
            } else if ($column['type'] === ConstFieldType::VARCHAR || $column['type'] === ConstFieldType::STRING) {
555 8
                $values[$name] = $column['max_length'] > 255 ? $this->getString(2) : $this->getString(1);
556 39
            } else if ($column['type'] === ConstFieldType::NEWDECIMAL) {
557 10
                $values[$name] = $this->getDecimal($column);
558 31
            } else if ($column['type'] === ConstFieldType::BLOB) {
559 4
                $values[$name] = $this->getString($column['length_size']);
560 21
            } else if ($column['type'] === ConstFieldType::DATETIME) {
561
                $values[$name] = $this->getDatetime();
562 17
            } else if ($column['type'] === ConstFieldType::DATETIME2) {
563 4
                $values[$name] = $this->getDatetime2($column);
564 17
            } else if ($column['type'] === ConstFieldType::TIMESTAMP) {
565
                $values[$name] = date('c', $this->binaryDataReader->readUInt32());
566 13
            } else if ($column['type'] === ConstFieldType::TIME) {
567
                $values[$name] = $this->getTime();
568 13
            } else if ($column['type'] === ConstFieldType::TIME2) {
569 2
                $values[$name] = $this->getTime2($column);
570 13
            } else if ($column['type'] === ConstFieldType::TIMESTAMP2) {
571 2
                $values[$name] = $this->getTimestamp2($column);
572 11
            } else if ($column['type'] === ConstFieldType::DATE) {
573 4
                $values[$name] = $this->getDate();
574 9
            } else if ($column['type'] === ConstFieldType::YEAR) {
575
                // https://dev.mysql.com/doc/refman/5.7/en/year.html
576 1
                $year = $this->binaryDataReader->readUInt8();
577 1
                $values[$name] = 0 === $year ? null : 1900 + $year;
578 5
            } else if ($column['type'] === ConstFieldType::ENUM) {
579 1
                $values[$name] = $this->getEnum($column);
580 4
            } else if ($column['type'] === ConstFieldType::SET) {
581 1
                $values[$name] = $this->getSet($column);
582 3
            } else if ($column['type'] === ConstFieldType::BIT) {
583 1
                $values[$name] = $this->getBit($column);
584 2
            } else if ($column['type'] === ConstFieldType::GEOMETRY) {
585 1
                $values[$name] = $this->getString($column['length_size']);
586 1
            } else if ($column['type'] === ConstFieldType::JSON) {
587
                $values[$name] = JsonBinaryDecoderFactory::makeJsonBinaryDecoder(
588
                    $this->getString($column['length_size'])
589
                )->parseToString();
590
            } else {
591
                throw new MySQLReplicationException('Unknown row type: ' . $column['type']);
592
            }
593
594 51
            ++$nullBitmapIndex;
595 51
        }
596
597 51
        return $values;
598
    }
599
600
    /**
601
     * @param string $bitmap
602
     * @return int
603
     */
604 51
    protected function bitCount($bitmap)
605
    {
606 51
        $n = 0;
607 51
        $bitmapLength = strlen($bitmap);
608 51
        for ($i = 0; $i < $bitmapLength; ++$i) {
609 51
            $bit = $bitmap[$i];
610 51
            if (is_string($bit)) {
611 51
                $bit = ord($bit);
612 51
            }
613 51
            $n += self::$bitCountInByte[$bit];
614 51
        }
615
616 51
        return $n;
617
    }
618
619
    /**
620
     * @param string $bitmap
621
     * @param int $position
622
     * @return int
623
     */
624 51
    protected function bitGet($bitmap, $position)
625
    {
626 51
        $bit = $bitmap[(int)($position / 8)];
627 51
        if (is_string($bit)) {
628 51
            $bit = ord($bit);
629 51
        }
630
631 51
        return $bit & (1 << ($position & 7));
632
    }
633
634
    /**
635
     * @param string $nullBitmap
636
     * @param int $position
637
     * @return int
638
     */
639 51
    protected function checkNull($nullBitmap, $position)
640
    {
641 51
        $bit = $nullBitmap[(int)($position / 8)];
642 51
        if (is_string($bit)) {
643 51
            $bit = ord($bit);
644 51
        }
645
646 51
        return $bit & (1 << ($position % 8));
647
    }
648
649
    /**
650
     * @param int $size
651
     * @return string
652
     * @throws BinaryDataReaderException
653
     */
654 13
    protected function getString($size)
655
    {
656 13
        return $this->binaryDataReader->readLengthCodedPascalString($size);
657
    }
658
659
    /**
660
     * Read MySQL's new decimal format introduced in MySQL 5
661
     * https://dev.mysql.com/doc/refman/5.6/en/precision-math-decimal-characteristics.html
662
     * @param array $column
663
     * @return string
664
     * @throws BinaryDataReaderException
665
     */
666 10
    protected function getDecimal(array $column)
667
    {
668 10
        $digitsPerInteger = 9;
669 10
        $compressedBytes = [0, 1, 1, 2, 2, 3, 3, 4, 4, 4];
670 10
        $integral = $column['precision'] - $column['decimals'];
671 10
        $unCompIntegral = (int)($integral / $digitsPerInteger);
672 10
        $unCompFractional = (int)($column['decimals'] / $digitsPerInteger);
673 10
        $compIntegral = $integral - ($unCompIntegral * $digitsPerInteger);
674 10
        $compFractional = $column['decimals'] - ($unCompFractional * $digitsPerInteger);
675
676 10
        $value = $this->binaryDataReader->readUInt8();
677 10
        if (0 !== ($value & 0x80)) {
678 7
            $mask = 0;
679 7
            $res = '';
680 7
        } else {
681 3
            $mask = -1;
682 3
            $res = '-';
683
        }
684 10
        $this->binaryDataReader->unread(pack('C', $value ^ 0x80));
685
686 10
        $size = $compressedBytes[$compIntegral];
687 10
        if ($size > 0) {
688 10
            $value = $this->binaryDataReader->readIntBeBySize($size) ^ $mask;
689 10
            $res .= $value;
690 10
        }
691
692 10
        for ($i = 0; $i < $unCompIntegral; ++$i) {
693 9
            $value = $this->binaryDataReader->readInt32Be() ^ $mask;
694 9
            $res .= sprintf('%09d', $value);
695 9
        }
696
697 10
        $res .= '.';
698
699 10
        for ($i = 0; $i < $unCompFractional; ++$i) {
700 4
            $value = $this->binaryDataReader->readInt32Be() ^ $mask;
701 4
            $res .= sprintf('%09d', $value);
702 4
        }
703
704 10
        $size = $compressedBytes[$compFractional];
705 10
        if ($size > 0) {
706 5
            $value = $this->binaryDataReader->readIntBeBySize($size) ^ $mask;
707 5
            $res .= sprintf('%0' . $compFractional . 'd', $value);
708 5
        }
709
710 10
        return bcmul($res, 1, $column['precision']);
711
    }
712
713
    /**
714
     * @return null|string
715
     */
716
    protected function getDatetime()
717
    {
718
        $value = $this->binaryDataReader->readUInt64();
719
        // nasty mysql 0000-00-00 dates
720
        if ('0' === $value) {
721
            return null;
722
        }
723
724
        return \DateTime::createFromFormat('YmdHis', $value)->format('Y-m-d H:i:s');
725
    }
726
727
    /**
728
     * Date Time
729
     * 1 bit  sign           (1= non-negative, 0= negative)
730
     * 17 bits year*13+month  (year 0-9999, month 0-12)
731
     * 5 bits day            (0-31)
732
     * 5 bits hour           (0-23)
733
     * 6 bits minute         (0-59)
734
     * 6 bits second         (0-59)
735
     * ---------------------------
736
     * 40 bits = 5 bytes
737
     * @param array $column
738
     * @return string|null
739
     * @throws BinaryDataReaderException
740
     * @link https://dev.mysql.com/doc/internals/en/date-and-time-data-type-representation.html
741
     */
742 4
    protected function getDatetime2(array $column)
743
    {
744 4
        $data = $this->binaryDataReader->readIntBeBySize(5);
745
746 4
        $yearMonth = $this->binaryDataReader->getBinarySlice($data, 1, 17, 40);
747
748 4
        $year = (int)($yearMonth / 13);
749 4
        $month = $yearMonth % 13;
750 4
        $day = $this->binaryDataReader->getBinarySlice($data, 18, 5, 40);
751 4
        $hour = $this->binaryDataReader->getBinarySlice($data, 23, 5, 40);
752 4
        $minute = $this->binaryDataReader->getBinarySlice($data, 28, 6, 40);
753 4
        $second = $this->binaryDataReader->getBinarySlice($data, 34, 6, 40);
754
755
        try {
756 4
            $date = new \DateTime($year . '-' . $month . '-' . $day . ' ' . $hour . ':' . $minute . ':' . $second);
757 4
        } catch (\Exception $exception) {
758
            return null;
759
        }
760
        // not all errors are thrown as exception :(
761 4
        if (array_sum(\DateTime::getLastErrors()) > 0) {
762 3
            return null;
763
        }
764
765 1
        return $date->format('Y-m-d H:i:s') . $this->getFSP($column);
766
    }
767
768
    /**
769
     * @param array $column
770
     * @return string
771
     * @throws BinaryDataReaderException
772
     * @link https://dev.mysql.com/doc/internals/en/date-and-time-data-type-representation.html
773
     */
774 5
    protected function getFSP(array $column)
775
    {
776 5
        $read = 0;
777 5
        $time = '';
778 5
        if ($column['fsp'] === 1 || $column['fsp'] === 2) {
779 1
            $read = 1;
780 5
        } else if ($column['fsp'] === 3 || $column['fsp'] === 4) {
781 1
            $read = 2;
782 5
        } else if ($column ['fsp'] === 5 || $column['fsp'] === 6) {
783 1
            $read = 3;
784 1
        }
785 5
        if ($read > 0) {
786 1
            $microsecond = $this->binaryDataReader->readIntBeBySize($read);
787
788 1
            $time = $microsecond;
789 1
            if ($column['fsp'] % 2) {
790 1
                $time = (int)($microsecond / 10);
791 1
            }
792 1
        }
793
794 5
        return (string)$time;
795
    }
796
797
    /**
798
     * TIME encoding for non fractional part:
799
     * 1 bit sign    (1= non-negative, 0= negative)
800
     * 1 bit unused  (reserved for future extensions)
801
     * 10 bits hour   (0-838)
802
     * 6 bits minute (0-59)
803
     * 6 bits second (0-59)
804
     * ---------------------
805
     * 24 bits = 3 bytes
806
     *
807
     * @param array $column
808
     * @return string
809
     * @throws BinaryDataReaderException
810
     */
811 2
    protected function getTime2(array $column)
812
    {
813 2
        $data = $this->binaryDataReader->readInt24Be();
814
815 2
        $hour = $this->binaryDataReader->getBinarySlice($data, 2, 10, 24);
816 2
        $minute = $this->binaryDataReader->getBinarySlice($data, 12, 6, 24);
817 2
        $second = $this->binaryDataReader->getBinarySlice($data, 18, 6, 24);
818
819 2
        return (new \DateTime())->setTime($hour, $minute, $second)->format('H:i:s') . $this->getFSP($column);
820
    }
821
822
    /**
823
     * @param array $column
824
     * @return bool|string
825
     * @throws BinaryDataReaderException
826
     */
827 2
    protected function getTimestamp2(array $column)
828
    {
829 2
        $datetime = date('Y-m-d H:i:s', $this->binaryDataReader->readInt32Be());
830 2
        $fsp = $this->getFSP($column);
831 2
        if ('' !== $fsp) {
832 1
            $datetime .= '.' . $fsp;
833 1
        }
834
835 2
        return $datetime;
836
    }
837
838
    /**
839
     * @return string
840
     */
841 4
    protected function getDate()
842
    {
843 4
        $time = $this->binaryDataReader->readUInt24();
844 4
        if (0 === $time) {
845
            return null;
846
        }
847
848 4
        $year = ($time & ((1 << 15) - 1) << 9) >> 9;
849 4
        $month = ($time & ((1 << 4) - 1) << 5) >> 5;
850 4
        $day = ($time & ((1 << 5) - 1));
851 4
        if ($year === 0 || $month === 0 || $day === 0) {
852 3
            return null;
853
        }
854
855 1
        return (new \DateTime())->setDate($year, $month, $day)->format('Y-m-d');
856
    }
857
858
    /**
859
     * @param array $column
860
     * @return array
861
     * @throws BinaryDataReaderException
862
     */
863 1
    protected function getSet(array $column)
864
    {
865
        // we read set columns as a bitmap telling us which options are enabled
866 1
        $bit_mask = $this->binaryDataReader->readUIntBySize($column['size']);
867 1
        $sets = [];
868 1
        foreach ((array)$column['set_values'] as $k => $item) {
869 1
            if ($bit_mask & (2 ** $k)) {
870 1
                $sets[] = $item;
871 1
            }
872 1
        }
873
874 1
        return $sets;
875
    }
876
877
    /**
878
     * Read MySQL BIT type
879
     * @param array $column
880
     * @return string
881
     */
882 1
    protected function getBit(array $column)
883
    {
884 1
        $res = '';
885 1
        for ($byte = 0; $byte < $column['bytes']; ++$byte) {
886 1
            $current_byte = '';
887 1
            $data = $this->binaryDataReader->readUInt8();
888 1
            if (0 === $byte) {
889 1
                if (1 === $column['bytes']) {
890 1
                    $end = $column['bits'];
891 1
                } else {
892 1
                    $end = $column['bits'] % 8;
893 1
                    if (0 === $end) {
894 1
                        $end = 8;
895 1
                    }
896
                }
897 1
            } else {
898 1
                $end = 8;
899
            }
900
901 1
            for ($bit = 0; $bit < $end; ++$bit) {
902 1
                if ($data & (1 << $bit)) {
903 1
                    $current_byte .= '1';
904 1
                } else {
905 1
                    $current_byte .= '0';
906
                }
907
908 1
            }
909 1
            $res .= strrev($current_byte);
910 1
        }
911
912 1
        return $res;
913
    }
914
915
    /**
916
     * @return DeleteRowsDTO
917
     * @throws InvalidArgumentException
918
     * @throws BinaryDataReaderException
919
     * @throws JsonBinaryDecoderException
920
     * @throws MySQLReplicationException
921
     */
922 1
    public function makeDeleteRowsDTO()
923
    {
924 1
        if (!$this->rowInit()) {
925
            return null;
926
        }
927
928 1
        $values = $this->getValues();
929
930 1
        return new DeleteRowsDTO(
931 1
            $this->eventInfo,
932 1
            $this->currentTableMap,
933 1
            count($values),
934
            $values
935 1
        );
936
    }
937
938
    /**
939
     * @return UpdateRowsDTO
940
     * @throws InvalidArgumentException
941
     * @throws BinaryDataReaderException
942
     * @throws JsonBinaryDecoderException
943
     * @throws MySQLReplicationException
944
     */
945 1
    public function makeUpdateRowsDTO()
946
    {
947 1
        if (!$this->rowInit()) {
948
            return null;
949
        }
950
951 1
        $columnsBinarySize = $this->getColumnsBinarySize($this->currentTableMap->getColumnsAmount());
952 1
        $beforeBinaryData = $this->binaryDataReader->read($columnsBinarySize);
953 1
        $afterBinaryData = $this->binaryDataReader->read($columnsBinarySize);
954
955 1
        $values = [];
956 1
        while (!$this->binaryDataReader->isComplete($this->eventInfo->getSizeNoHeader())) {
0 ignored issues
show
Bug introduced by
$this->eventInfo->getSizeNoHeader() of type string is incompatible with the type integer expected by parameter $size of MySQLReplication\BinaryD...ataReader::isComplete(). ( Ignorable by Annotation )

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

956
        while (!$this->binaryDataReader->isComplete(/** @scrutinizer ignore-type */ $this->eventInfo->getSizeNoHeader())) {
Loading history...
957 1
            $values[] = [
958 1
                'before' => $this->getColumnData($beforeBinaryData),
0 ignored issues
show
Bug introduced by
$beforeBinaryData of type string is incompatible with the type integer expected by parameter $colsBitmap of MySQLReplication\Event\R...wEvent::getColumnData(). ( Ignorable by Annotation )

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

958
                'before' => $this->getColumnData(/** @scrutinizer ignore-type */ $beforeBinaryData),
Loading history...
959 1
                'after' => $this->getColumnData($afterBinaryData)
960 1
            ];
961 1
        }
962
963 1
        return new UpdateRowsDTO(
964 1
            $this->eventInfo,
965 1
            $this->currentTableMap,
966 1
            count($values),
967
            $values
968 1
        );
969
    }
970
971
    /**
972
     * @param array $column
973
     * @return string
974
     * @throws BinaryDataReaderException
975
     */
976 1
    protected function getEnum(array $column)
977
    {
978 1
        $value = $this->binaryDataReader->readUIntBySize($column['size']) - 1;
979
980
        // check if given value exists in enums, if there not existing enum mysql returns empty string.
981 1
        if (array_key_exists($value, $column['enum_values'])) {
982 1
            return $column['enum_values'][$value];
983
        }
984
985 1
        return '';
986
    }
987
988
    /**
989
     * @return string
990
     */
991
    protected function getTime()
992
    {
993
        $data = $this->binaryDataReader->readUInt24();
994
995
        if (0 === $data) {
996
            return '00-00-00';
997
        }
998
999
        return sprintf('%s%02d:%02d:%02d', $data < 0 ? '-' : '', $data / 10000, ($data % 10000) / 100, $data % 100);
1000
    }
1001
}
1002