Passed
Branch master (a6a24c)
by kacper
04:55
created

RowEvent::makeDeleteRowsDTO()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2.0078

Importance

Changes 0
Metric Value
cc 2
eloc 8
nc 2
nop 0
dl 0
loc 13
ccs 7
cts 8
cp 0.875
crap 2.0078
rs 9.4285
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\EventException;
16
use MySQLReplication\Event\EventInfo;
17
use MySQLReplication\Exception\MySQLReplicationException;
18
use MySQLReplication\JsonBinaryDecoder\JsonBinaryDecoderException;
19
use MySQLReplication\JsonBinaryDecoder\JsonBinaryDecoderFactory;
20
use MySQLReplication\Repository\RepositoryInterface;
21
use Psr\SimpleCache\CacheInterface;
22
use Psr\SimpleCache\InvalidArgumentException;
23
24
/**
25
 * Class RowEvent
26
 * @package MySQLReplication\RowEvent
27
 */
28
class RowEvent extends EventCommon
29
{
30
    /**
31
     * @var array
32
     */
33
    private static $bitCountInByte = [
34
        0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4,
35
        1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
36
        1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
37
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
38
        1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
39
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
40
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
41
        3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
42
        1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
43
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
44
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
45
        3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
46
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
47
        3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
48
        3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
49
        4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, 6, 7, 7, 8,
50
    ];
51
    /**
52
     * @var RepositoryInterface
53
     */
54
    private $repository;
55
    /**
56
     * @var TableMap
57
     */
58
    private $currentTableMap;
59
    /**
60
     * @var JsonBinaryDecoderFactory
61
     */
62
    private $jsonBinaryDecoderFactory;
63
    /**
64
     * @var CacheInterface
65
     */
66
    private $cache;
67
68
    /**
69
     * RowEvent constructor.
70
     * @param RepositoryInterface $repository
71
     * @param BinaryDataReader $binaryDataReader
72
     * @param EventInfo $eventInfo
73
     * @param JsonBinaryDecoderFactory $jsonBinaryDecoderFactory
74
     * @param CacheInterface $cache
75
     */
76 53
    public function __construct(
77
        RepositoryInterface $repository,
78
        BinaryDataReader $binaryDataReader,
79
        EventInfo $eventInfo,
80
        JsonBinaryDecoderFactory $jsonBinaryDecoderFactory,
81
        CacheInterface $cache
82
    ) {
83 53
        parent::__construct($eventInfo, $binaryDataReader);
84
85 53
        $this->repository = $repository;
86 53
        $this->jsonBinaryDecoderFactory = $jsonBinaryDecoderFactory;
87 53
        $this->cache = $cache;
88 53
    }
89
90
    /**
91
     * This describe the structure of a table.
92
     * It's send before a change append on a table.
93
     * A end user of the lib should have no usage of this
94
     *
95
     * @return TableMapDTO
96
     * @throws \Psr\SimpleCache\InvalidArgumentException
97
     * @throws \MySQLReplication\BinaryDataReader\BinaryDataReaderException
98
     */
99 52
    public function makeTableMapDTO()
100
    {
101 52
        $data = [];
102 52
        $data['table_id'] = $this->binaryDataReader->readTableId();
103 52
        $this->binaryDataReader->advance(2);
104 52
        $data['schema_length'] = $this->binaryDataReader->readUInt8();
105 52
        $data['schema_name'] = $this->binaryDataReader->read($data['schema_length']);
106
107 52
        if (Config::checkDataBasesOnly($data['schema_name'])) {
108
            return null;
109
        }
110
111 52
        $this->binaryDataReader->advance(1);
112 52
        $data['table_length'] = $this->binaryDataReader->readUInt8();
113 52
        $data['table_name'] = $this->binaryDataReader->read($data['table_length']);
114
115 52
        if (Config::checkTablesOnly($data['table_name'])) {
116 1
            return null;
117
        }
118
119 52
        $this->binaryDataReader->advance(1);
120 52
        $data['columns_amount'] = $this->binaryDataReader->readCodedBinary();
121 52
        $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

121
        $data['column_types'] = $this->binaryDataReader->read(/** @scrutinizer ignore-type */ $data['columns_amount']);
Loading history...
122
123 52
        if ($this->cache->has($data['table_id'])) {
124 2
            return new TableMapDTO($this->eventInfo, $this->cache->get($data['table_id']));
125
        }
126
127 52
        $this->binaryDataReader->readCodedBinary();
128
129 52
        $columns = $this->repository->getFields($data['schema_name'], $data['table_name']);
130
131 52
        $fields = [];
132
        // if you drop tables and parse of logs you will get empty scheme
133 52
        if (!empty($columns)) {
134 51
            $columnLength = strlen($data['column_types']);
135 51
            for ($i = 0; $i < $columnLength; ++$i) {
136
                // this a dirty hack to prevent row events containing columns which have been dropped
137 51
                if (!isset($columns[$i])) {
138 1
                    $columns[$i] = [
139 1
                        'COLUMN_NAME' => 'DROPPED_COLUMN_' . $i,
140 1
                        'COLLATION_NAME' => null,
141 1
                        'CHARACTER_SET_NAME' => null,
142 1
                        'COLUMN_COMMENT' => null,
143 1
                        'COLUMN_TYPE' => 'BLOB',
144
                        'COLUMN_KEY' => ''
145 1
                    ];
146
147 1
                    $type = ConstFieldType::IGNORE;
148 1
                } else {
149 51
                    $type = ord($data['column_types'][$i]);
150
                }
151
152 51
                $fields[$i] = Columns::parse($type, $columns[$i], $this->binaryDataReader);
153 51
            }
154 51
        }
155
156 52
        $tableMap = new TableMap(
157 52
            $data['schema_name'],
158 52
            $data['table_name'],
159 52
            $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

159
            /** @scrutinizer ignore-type */ $data['table_id'],
Loading history...
160 52
            $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

160
            /** @scrutinizer ignore-type */ $data['columns_amount'],
Loading history...
161
            $fields
162 52
        );
163
164 52
        $this->cache->set($data['table_id'], $tableMap);
165
166 52
        return new TableMapDTO($this->eventInfo, $tableMap);
167
    }
168
169
    /**
170
     * @return WriteRowsDTO|null
171
     * @throws InvalidArgumentException
172
     * @throws BinaryDataReaderException
173
     * @throws EventException
174
     * @throws JsonBinaryDecoderException
175
     * @throws MySQLReplicationException
176
     */
177 52
    public function makeWriteRowsDTO()
178
    {
179 52
        if (!$this->rowInit()) {
180 1
            return null;
181
        }
182
183 52
        $values = $this->getValues();
184
185 52
        return new WriteRowsDTO(
186 52
            $this->eventInfo,
187 52
            $this->currentTableMap,
188 52
            count($values),
189
            $values
190 52
        );
191
    }
192
193
    /**
194
     * @return bool
195
     * @throws InvalidArgumentException
196
     * @throws BinaryDataReaderException
197
     */
198 52
    protected function rowInit()
199
    {
200 52
        $tableId = $this->binaryDataReader->readTableId();
201 52
        $this->binaryDataReader->advance(2);
202
203 52
        if (in_array(
204 52
            $this->eventInfo->getType(), [
205 52
            ConstEventType::DELETE_ROWS_EVENT_V2,
206 52
            ConstEventType::WRITE_ROWS_EVENT_V2,
207
            ConstEventType::UPDATE_ROWS_EVENT_V2
208 52
        ], true
209 52
        )) {
210 52
            $this->binaryDataReader->read($this->binaryDataReader->readUInt16() / 8);
211 52
        }
212
213 52
        $this->binaryDataReader->readCodedBinary();
214
215 52
        if ($this->cache->has($tableId)) {
216
            /** @var TableMap $tableMap */
217 52
            $this->currentTableMap = $this->cache->get($tableId);
218 52
            return true;
219
        }
220
221 1
        return false;
222
    }
223
224
    /**
225
     * @return array
226
     * @throws BinaryDataReaderException
227
     * @throws EventException
228
     * @throws JsonBinaryDecoderException
229
     * @throws MySQLReplicationException
230
     */
231 52
    protected function getValues()
232
    {
233
        // if we don't get columns from information schema we don't know how to assign them
234 52
        if ([] === $this->currentTableMap->getFields()) {
235 1
            return [];
236
        }
237
238 51
        $binaryData = $this->binaryDataReader->read(
239 51
            $this->getColumnsBinarySize($this->currentTableMap->getColumnsAmount())
240 51
        );
241
242 51
        $values = [];
243 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

243
        while (!$this->binaryDataReader->isComplete(/** @scrutinizer ignore-type */ $this->eventInfo->getSizeNoHeader())) {
Loading history...
244 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

244
            $values[] = $this->getColumnData(/** @scrutinizer ignore-type */ $binaryData);
Loading history...
245 51
        }
246
247 51
        return $values;
248
    }
249
250
    /**
251
     * @param int $columnsAmount
252
     * @return int
253
     */
254 51
    protected function getColumnsBinarySize($columnsAmount)
255
    {
256 51
        return (int)(($columnsAmount + 7) / 8);
257
    }
258
259
    /**
260
     * @param int $colsBitmap
261
     * @return array
262
     * @throws BinaryDataReaderException
263
     * @throws EventException
264
     * @throws JsonBinaryDecoderException
265
     * @throws MySQLReplicationException
266
     */
267 51
    protected function getColumnData($colsBitmap)
268
    {
269 51
        $values = [];
270
271
        // null bitmap length = (bits set in 'columns-present-bitmap'+7)/8
272
        // see http://dev.mysql.com/doc/internals/en/rows-event.html
273 51
        $null_bitmap = $this->binaryDataReader->read($this->getColumnsBinarySize($this->bitCount($colsBitmap)));
274 51
        $nullBitmapIndex = 0;
275
276 51
        foreach ($this->currentTableMap->getFields() as $i => $column) {
277 51
            $name = $column['name'];
278
279 51
            if (0 === $this->bitGet($colsBitmap, $i)) {
280
                $values[$name] = null;
281
                continue;
282
            }
283
284 51
            if ($this->checkNull($null_bitmap, $nullBitmapIndex)) {
285 5
                $values[$name] = null;
286 51
            } elseif ($column['type'] === ConstFieldType::IGNORE) {
287 1
                $this->binaryDataReader->advance($column['length_size']);
288 1
                $values[$name] = null;
289 51
            } elseif ($column['type'] === ConstFieldType::TINY) {
290 5
                if ($column['unsigned']) {
291 4
                    $values[$name] = $this->binaryDataReader->readUInt8();
292 4
                } else {
293 4
                    $values[$name] = $this->binaryDataReader->readInt8();
294
                }
295 51
            } elseif ($column['type'] === ConstFieldType::SHORT) {
296 1
                if ($column['unsigned']) {
297 1
                    $values[$name] = $this->binaryDataReader->readUInt16();
298 1
                } else {
299 1
                    $values[$name] = $this->binaryDataReader->readInt16();
300
                }
301 46
            } elseif ($column['type'] === ConstFieldType::LONG) {
302 10
                if ($column['unsigned']) {
303 1
                    $values[$name] = $this->binaryDataReader->readUInt32();
304 1
                } else {
305 10
                    $values[$name] = $this->binaryDataReader->readInt32();
306
                }
307 45
            } elseif ($column['type'] === ConstFieldType::LONGLONG) {
308 1
                if ($column['unsigned']) {
309 1
                    $values[$name] = $this->binaryDataReader->readUInt64();
310 1
                } else {
311 1
                    $values[$name] = $this->binaryDataReader->readInt64();
312
                }
313 43
            } elseif ($column['type'] === ConstFieldType::INT24) {
314 1
                if ($column['unsigned']) {
315 1
                    $values[$name] = $this->binaryDataReader->readUInt24();
316 1
                } else {
317 1
                    $values[$name] = $this->binaryDataReader->readInt24();
318
                }
319 42
            } elseif ($column['type'] === ConstFieldType::FLOAT) {
320
                // http://dev.mysql.com/doc/refman/5.7/en/floating-point-types.html FLOAT(7,4)
321 1
                $values[$name] = round($this->binaryDataReader->readFloat(), 4);
322 41
            } elseif ($column['type'] === ConstFieldType::DOUBLE) {
323 1
                $values[$name] = $this->binaryDataReader->readDouble();
324 40
            } elseif ($column['type'] === ConstFieldType::VARCHAR || $column['type'] === ConstFieldType::STRING) {
325 8
                $values[$name] = $column['max_length'] > 255 ? $this->getString(2) : $this->getString(1);
326 39
            } elseif ($column['type'] === ConstFieldType::NEWDECIMAL) {
327 10
                $values[$name] = $this->getDecimal($column);
328 31
            } elseif ($column['type'] === ConstFieldType::BLOB) {
329 4
                $values[$name] = $this->getString($column['length_size']);
330 21
            } elseif ($column['type'] === ConstFieldType::DATETIME) {
331
                $values[$name] = $this->getDatetime();
332 17
            } elseif ($column['type'] === ConstFieldType::DATETIME2) {
333 4
                $values[$name] = $this->getDatetime2($column);
334 17
            } elseif ($column['type'] === ConstFieldType::TIMESTAMP) {
335
                $values[$name] = date('c', $this->binaryDataReader->readUInt32());
336 13
            } elseif ($column['type'] === ConstFieldType::TIME2) {
337 2
                $values[$name] = $this->getTime2($column);
338 13
            } elseif ($column['type'] === ConstFieldType::TIMESTAMP2) {
339 2
                $values[$name] = $this->getTimestamp2($column);
340 11
            } elseif ($column['type'] === ConstFieldType::DATE) {
341 4
                $values[$name] = $this->getDate();
342 9
            } elseif ($column['type'] === ConstFieldType::YEAR) {
343
                // https://dev.mysql.com/doc/refman/5.7/en/year.html
344 1
                $year = $this->binaryDataReader->readUInt8();
345 1
                $values[$name] = 0 === $year ? null : 1900 + $year;
346 5
            } elseif ($column['type'] === ConstFieldType::ENUM) {
347 1
                $values[$name] = $this->getEnum($column);
348 4
            } elseif ($column['type'] === ConstFieldType::SET) {
349 1
                $values[$name] = $this->getSet($column);
350 3
            } elseif ($column['type'] === ConstFieldType::BIT) {
351 1
                $values[$name] = $this->getBit($column);
352 2
            } elseif ($column['type'] === ConstFieldType::GEOMETRY) {
353 1
                $values[$name] = $this->getString($column['length_size']);
354 1
            } elseif ($column['type'] === ConstFieldType::JSON) {
355
                $values[$name] = $this->jsonBinaryDecoderFactory->makeJsonBinaryDecoder(
356
                    $this->getString($column['length_size'])
357
                )->parseToString();
358
            } else {
359
                throw new MySQLReplicationException('Unknown row type: ' . $column['type']);
360
            }
361
362 51
            ++$nullBitmapIndex;
363 51
        }
364
365 51
        return $values;
366
    }
367
368
    /**
369
     * @param string $bitmap
370
     * @return int
371
     */
372 51
    protected function bitCount($bitmap)
373
    {
374 51
        $n = 0;
375 51
        $bitmapLength = strlen($bitmap);
376 51
        for ($i = 0; $i < $bitmapLength; ++$i) {
377 51
            $bit = $bitmap[$i];
378 51
            if (is_string($bit)) {
379 51
                $bit = ord($bit);
380 51
            }
381 51
            $n += self::$bitCountInByte[$bit];
382 51
        }
383
384 51
        return $n;
385
    }
386
387
    /**
388
     * @param string $bitmap
389
     * @param int $position
390
     * @return int
391
     */
392 51
    protected function bitGet($bitmap, $position)
393
    {
394 51
        $bit = $bitmap[(int)($position / 8)];
395 51
        if (is_string($bit)) {
396 51
            $bit = ord($bit);
397 51
        }
398
399 51
        return $bit & (1 << ($position & 7));
400
    }
401
402
    /**
403
     * @param string $nullBitmap
404
     * @param int $position
405
     * @return int
406
     */
407 51
    protected function checkNull($nullBitmap, $position)
408
    {
409 51
        $bit = $nullBitmap[(int)($position / 8)];
410 51
        if (is_string($bit)) {
411 51
            $bit = ord($bit);
412 51
        }
413
414 51
        return $bit & (1 << ($position % 8));
415
    }
416
417
    /**
418
     * @param int $size
419
     * @return string
420
     * @throws BinaryDataReaderException
421
     */
422 13
    protected function getString($size)
423
    {
424 13
        return $this->binaryDataReader->readLengthCodedPascalString($size);
425
    }
426
427
    /**
428
     * Read MySQL's new decimal format introduced in MySQL 5
429
     * https://dev.mysql.com/doc/refman/5.6/en/precision-math-decimal-characteristics.html
430
     * @param array $column
431
     * @return string
432
     * @throws BinaryDataReaderException
433
     */
434 10
    protected function getDecimal(array $column)
435
    {
436 10
        $digitsPerInteger = 9;
437 10
        $compressedBytes = [0, 1, 1, 2, 2, 3, 3, 4, 4, 4];
438 10
        $integral = $column['precision'] - $column['decimals'];
439 10
        $unCompIntegral = (int)($integral / $digitsPerInteger);
440 10
        $unCompFractional = (int)($column['decimals'] / $digitsPerInteger);
441 10
        $compIntegral = $integral - ($unCompIntegral * $digitsPerInteger);
442 10
        $compFractional = $column['decimals'] - ($unCompFractional * $digitsPerInteger);
443
444 10
        $value = $this->binaryDataReader->readUInt8();
445 10
        if (0 !== ($value & 0x80)) {
446 7
            $mask = 0;
447 7
            $res = '';
448 7
        } else {
449 3
            $mask = -1;
450 3
            $res = '-';
451
        }
452 10
        $this->binaryDataReader->unread(pack('C', $value ^ 0x80));
453
454 10
        $size = $compressedBytes[$compIntegral];
455 10
        if ($size > 0) {
456 10
            $value = $this->binaryDataReader->readIntBeBySize($size) ^ $mask;
457 10
            $res .= $value;
458 10
        }
459
460 10
        for ($i = 0; $i < $unCompIntegral; ++$i) {
461 9
            $value = $this->binaryDataReader->readInt32Be() ^ $mask;
462 9
            $res .= sprintf('%09d', $value);
463 9
        }
464
465 10
        $res .= '.';
466
467 10
        for ($i = 0; $i < $unCompFractional; ++$i) {
468 4
            $value = $this->binaryDataReader->readInt32Be() ^ $mask;
469 4
            $res .= sprintf('%09d', $value);
470 4
        }
471
472 10
        $size = $compressedBytes[$compFractional];
473 10
        if ($size > 0) {
474 5
            $value = $this->binaryDataReader->readIntBeBySize($size) ^ $mask;
475 5
            $res .= sprintf('%0' . $compFractional . 'd', $value);
476 5
        }
477
478 10
        return bcmul($res, 1, $column['precision']);
479
    }
480
481
    /**
482
     * @return null|string
483
     * @throws \MySQLReplication\BinaryDataReader\BinaryDataReaderException
484
     */
485
    protected function getDatetime()
486
    {
487
        $value = $this->binaryDataReader->readUInt64();
488
        // nasty mysql 0000-00-00 dates
489
        if ('0' === $value) {
490
            return null;
491
        }
492
493
        return \DateTime::createFromFormat('YmdHis', $value)->format('Y-m-d H:i:s');
494
    }
495
496
    /**
497
     * Date Time
498
     * 1 bit  sign           (1= non-negative, 0= negative)
499
     * 17 bits year*13+month  (year 0-9999, month 0-12)
500
     * 5 bits day            (0-31)
501
     * 5 bits hour           (0-23)
502
     * 6 bits minute         (0-59)
503
     * 6 bits second         (0-59)
504
     * ---------------------------
505
     * 40 bits = 5 bytes
506
     * @param array $column
507
     * @return string|null
508
     * @throws BinaryDataReaderException
509
     * @link https://dev.mysql.com/doc/internals/en/date-and-time-data-type-representation.html
510
     */
511 4
    protected function getDatetime2(array $column)
512
    {
513 4
        $data = $this->binaryDataReader->readIntBeBySize(5);
514
515 4
        $yearMonth = $this->binaryDataReader->getBinarySlice($data, 1, 17, 40);
516
517 4
        $year = (int)($yearMonth / 13);
518 4
        $month = $yearMonth % 13;
519 4
        $day = $this->binaryDataReader->getBinarySlice($data, 18, 5, 40);
520 4
        $hour = $this->binaryDataReader->getBinarySlice($data, 23, 5, 40);
521 4
        $minute = $this->binaryDataReader->getBinarySlice($data, 28, 6, 40);
522 4
        $second = $this->binaryDataReader->getBinarySlice($data, 34, 6, 40);
523
524
        try {
525 4
            $date = new \DateTime($year . '-' . $month . '-' . $day . ' ' . $hour . ':' . $minute . ':' . $second);
526 4
        } catch (\Exception $exception) {
527
            return null;
528
        }
529
        // not all errors are thrown as exception :(
530 4
        if (array_sum(\DateTime::getLastErrors()) > 0) {
531 3
            return null;
532
        }
533
534 1
        return $date->format('Y-m-d H:i:s') . $this->getFSP($column);
535
    }
536
537
    /**
538
     * @param array $column
539
     * @return string
540
     * @throws BinaryDataReaderException
541
     * @link https://dev.mysql.com/doc/internals/en/date-and-time-data-type-representation.html
542
     */
543 5
    protected function getFSP(array $column)
544
    {
545 5
        $read = 0;
546 5
        $time = '';
547 5
        if ($column['fsp'] === 1 || $column['fsp'] === 2) {
548 1
            $read = 1;
549 5
        } elseif ($column['fsp'] === 3 || $column['fsp'] === 4) {
550 1
            $read = 2;
551 5
        } elseif ($column ['fsp'] === 5 || $column['fsp'] === 6) {
552 1
            $read = 3;
553 1
        }
554 5
        if ($read > 0) {
555 1
            $microsecond = $this->binaryDataReader->readIntBeBySize($read);
556
557 1
            $time = $microsecond;
558 1
            if ($column['fsp'] % 2) {
559 1
                $time = (int)($microsecond / 10);
560 1
            }
561 1
        }
562
563 5
        return (string)$time;
564
    }
565
566
    /**
567
     * TIME encoding for non fractional part:
568
     * 1 bit sign    (1= non-negative, 0= negative)
569
     * 1 bit unused  (reserved for future extensions)
570
     * 10 bits hour   (0-838)
571
     * 6 bits minute (0-59)
572
     * 6 bits second (0-59)
573
     * ---------------------
574
     * 24 bits = 3 bytes
575
     *
576
     * @param array $column
577
     * @return string
578
     * @throws BinaryDataReaderException
579
     */
580 2
    protected function getTime2(array $column)
581
    {
582 2
        $data = $this->binaryDataReader->readInt24Be();
583
584 2
        $hour = $this->binaryDataReader->getBinarySlice($data, 2, 10, 24);
585 2
        $minute = $this->binaryDataReader->getBinarySlice($data, 12, 6, 24);
586 2
        $second = $this->binaryDataReader->getBinarySlice($data, 18, 6, 24);
587
588 2
        return (new \DateTime())->setTime($hour, $minute, $second)->format('H:i:s') . $this->getFSP($column);
589
    }
590
591
    /**
592
     * @param array $column
593
     * @return bool|string
594
     * @throws EventException
595
     * @throws BinaryDataReaderException
596
     */
597 2
    protected function getTimestamp2(array $column)
598
    {
599 2
        $datetime = date('Y-m-d H:i:s', $this->binaryDataReader->readInt32Be());
600 2
        $fsp = $this->getFSP($column);
601 2
        if ('' !== $fsp) {
602 1
            $datetime .= '.' . $fsp;
603 1
        }
604
605 2
        return $datetime;
606
    }
607
608
    /**
609
     * @return string
610
     * @throws \MySQLReplication\BinaryDataReader\BinaryDataReaderException
611
     */
612 4
    protected function getDate()
613
    {
614 4
        $time = $this->binaryDataReader->readUInt24();
615 4
        if (0 === $time) {
616
            return null;
617
        }
618
619 4
        $year = ($time & ((1 << 15) - 1) << 9) >> 9;
620 4
        $month = ($time & ((1 << 4) - 1) << 5) >> 5;
621 4
        $day = ($time & ((1 << 5) - 1));
622 4
        if ($year === 0 || $month === 0 || $day === 0) {
623 3
            return null;
624
        }
625
626 1
        return (new \DateTime())->setDate($year, $month, $day)->format('Y-m-d');
627
    }
628
629
    /**
630
     * @param array $column
631
     * @return array
632
     * @throws EventException
633
     * @throws BinaryDataReaderException
634
     */
635 1
    protected function getSet(array $column)
636
    {
637
        // we read set columns as a bitmap telling us which options are enabled
638 1
        $bit_mask = $this->binaryDataReader->readUIntBySize($column['size']);
639 1
        $sets = [];
640 1
        foreach ((array)$column['set_values'] as $k => $item) {
641 1
            if ($bit_mask & pow(2, $k)) {
642 1
                $sets[] = $item;
643 1
            }
644 1
        }
645
646 1
        return $sets;
647
    }
648
649
    /**
650
     * Read MySQL BIT type
651
     * @param array $column
652
     * @return string
653
     * @throws \MySQLReplication\BinaryDataReader\BinaryDataReaderException
654
     */
655 1
    protected function getBit(array $column)
656
    {
657 1
        $res = '';
658 1
        for ($byte = 0; $byte < $column['bytes']; ++$byte) {
659 1
            $current_byte = '';
660 1
            $data = $this->binaryDataReader->readUInt8();
661 1
            if (0 === $byte) {
662 1
                if (1 === $column['bytes']) {
663 1
                    $end = $column['bits'];
664 1
                } else {
665 1
                    $end = $column['bits'] % 8;
666 1
                    if (0 === $end) {
667 1
                        $end = 8;
668 1
                    }
669
                }
670 1
            } else {
671 1
                $end = 8;
672
            }
673
674 1
            for ($bit = 0; $bit < $end; ++$bit) {
675 1
                if ($data & (1 << $bit)) {
676 1
                    $current_byte .= '1';
677 1
                } else {
678 1
                    $current_byte .= '0';
679
                }
680
681 1
            }
682 1
            $res .= strrev($current_byte);
683 1
        }
684
685 1
        return $res;
686
    }
687
688
    /**
689
     * @return DeleteRowsDTO
690
     * @throws InvalidArgumentException
691
     * @throws BinaryDataReaderException
692
     * @throws EventException
693
     * @throws JsonBinaryDecoderException
694
     * @throws MySQLReplicationException
695
     */
696 1
    public function makeDeleteRowsDTO()
697
    {
698 1
        if (!$this->rowInit()) {
699
            return null;
700
        }
701
702 1
        $values = $this->getValues();
703
704 1
        return new DeleteRowsDTO(
705 1
            $this->eventInfo,
706 1
            $this->currentTableMap,
707 1
            count($values),
708
            $values
709 1
        );
710
    }
711
712
    /**
713
     * @return UpdateRowsDTO
714
     * @throws InvalidArgumentException
715
     * @throws BinaryDataReaderException
716
     * @throws EventException
717
     * @throws JsonBinaryDecoderException
718
     * @throws MySQLReplicationException
719
     */
720 1
    public function makeUpdateRowsDTO()
721
    {
722 1
        if (!$this->rowInit()) {
723
            return null;
724
        }
725
726 1
        $columnsBinarySize = $this->getColumnsBinarySize($this->currentTableMap->getColumnsAmount());
727 1
        $beforeBinaryData = $this->binaryDataReader->read($columnsBinarySize);
728 1
        $afterBinaryData = $this->binaryDataReader->read($columnsBinarySize);
729
730 1
        $values = [];
731 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

731
        while (!$this->binaryDataReader->isComplete(/** @scrutinizer ignore-type */ $this->eventInfo->getSizeNoHeader())) {
Loading history...
732 1
            $values[] = [
733 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

733
                'before' => $this->getColumnData(/** @scrutinizer ignore-type */ $beforeBinaryData),
Loading history...
734 1
                'after' => $this->getColumnData($afterBinaryData)
735 1
            ];
736 1
        }
737
738 1
        return new UpdateRowsDTO(
739 1
            $this->eventInfo,
740 1
            $this->currentTableMap,
741 1
            count($values),
742
            $values
743 1
        );
744
    }
745
746
    /**
747
     * @param array $column
748
     * @return string
749
     * @throws \MySQLReplication\BinaryDataReader\BinaryDataReaderException
750
     */
751 1
    protected function getEnum(array $column)
752
    {
753 1
        $value = $this->binaryDataReader->readUIntBySize($column['size']) - 1;
754
755
        // check if given value exists in enums, if there not existing enum mysql returns empty string.
756 1
        if (array_key_exists($value, $column['enum_values'])) {
757 1
            return $column['enum_values'][$value];
758
        }
759
760 1
        return '';
761
    }
762
}
763