From 1d794b4b58313ba92dc99971142fa4ffda19ab0a Mon Sep 17 00:00:00 2001 From: hetao Date: Sun, 5 May 2024 08:20:55 +0800 Subject: [PATCH 01/10] fix #115 for mysql 8.0.0 --- .../BinLog/BinLogSocketConnect.php | 7 ++++++- .../Repository/MySQLRepository.php | 21 ++++++++++--------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/MySQLReplication/BinLog/BinLogSocketConnect.php b/src/MySQLReplication/BinLog/BinLogSocketConnect.php index 9fd34f2..5fc24fd 100644 --- a/src/MySQLReplication/BinLog/BinLogSocketConnect.php +++ b/src/MySQLReplication/BinLog/BinLogSocketConnect.php @@ -192,9 +192,14 @@ private function getBinlogStream(): void $this->executeSQL('SET @master_binlog_checksum = @@global.binlog_checksum'); } + if ($this->config->heartbeatPeriod > 0.00) { // master_heartbeat_period is in nanoseconds - $this->executeSQL('SET @master_heartbeat_period = ' . $this->config->heartbeatPeriod * 1000000000); + if(version_compare($this->repository->getVersion(),"8.4.0")>=0){ + $this->executeSQL('SET @source_heartbeat_period = ' . $this->config->heartbeatPeriod * 1000000000); + }else{ + $this->executeSQL('SET @master_heartbeat_period = ' . $this->config->heartbeatPeriod * 1000000000); + } $this->logger->info('Heartbeat period set to ' . $this->config->heartbeatPeriod . ' seconds'); } diff --git a/src/MySQLReplication/Repository/MySQLRepository.php b/src/MySQLReplication/Repository/MySQLRepository.php index ee7a779..07a3a8a 100644 --- a/src/MySQLReplication/Repository/MySQLRepository.php +++ b/src/MySQLReplication/Repository/MySQLRepository.php @@ -57,21 +57,22 @@ public function isCheckSum(): bool public function getVersion(): string { - $r = ''; - $versions = $this->getConnection() - ->fetchAllAssociative('SHOW VARIABLES LIKE "version%"'); - - foreach ($versions as $version) { - $r .= $version['Value']; - } + $res = $this->getConnection() + ->fetchAssociative('SHOW VARIABLES LIKE "version"'); - return $r; + return $res['Value']??""; } public function getMasterStatus(): MasterStatusDTO { - $data = $this->getConnection() - ->fetchAssociative('SHOW MASTER STATUS'); + var_dump($this->getVersion()); + if(version_compare($this->getVersion(),"8.4.0")>=0){ + $data = $this->getConnection() + ->fetchAssociative('SHOW BINARY LOG STATUS'); + }else{ + $data = $this->getConnection() + ->fetchAssociative('SHOW MASTER STATUS'); + } if (empty($data)) { throw new BinLogException( MySQLReplicationException::BINLOG_NOT_ENABLED, From c5b38e5d0dfc8eae47e8546112ea36e1cc851ee2 Mon Sep 17 00:00:00 2001 From: hetao Date: Sun, 5 May 2024 08:23:02 +0800 Subject: [PATCH 02/10] fix #115 for mysql 8.4.0 --- src/MySQLReplication/Repository/MySQLRepository.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/MySQLReplication/Repository/MySQLRepository.php b/src/MySQLReplication/Repository/MySQLRepository.php index 07a3a8a..19d9ec7 100644 --- a/src/MySQLReplication/Repository/MySQLRepository.php +++ b/src/MySQLReplication/Repository/MySQLRepository.php @@ -65,7 +65,6 @@ public function getVersion(): string public function getMasterStatus(): MasterStatusDTO { - var_dump($this->getVersion()); if(version_compare($this->getVersion(),"8.4.0")>=0){ $data = $this->getConnection() ->fetchAssociative('SHOW BINARY LOG STATUS'); From 6ca015c9e6ea3a3c0836fc5258a222b21e261ed2 Mon Sep 17 00:00:00 2001 From: hetao Date: Wed, 8 May 2024 17:49:53 +0800 Subject: [PATCH 03/10] fix #115 for mysql 8.4.0 phpunit test --- tests/Unit/Repository/MySQLRepositoryTest.php | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/tests/Unit/Repository/MySQLRepositoryTest.php b/tests/Unit/Repository/MySQLRepositoryTest.php index 5ad88ac..d126124 100644 --- a/tests/Unit/Repository/MySQLRepositoryTest.php +++ b/tests/Unit/Repository/MySQLRepositoryTest.php @@ -70,22 +70,14 @@ public function testShouldIsCheckSum(): void public function testShouldGetVersion(): void { - $expected = [ - [ - 'Value' => 'foo', - ], - [ - 'Value' => 'bar', - ], - [ - 'Value' => '123', - ], - ]; + $expected = [ + 'Value' => 'version', + ]; - $this->connection->method('fetchAllAssociative') - ->willReturn($expected); + $this->connection->method('fetchAssociative') + ->willReturn($expected); - self::assertEquals('foobar123', $this->mySQLRepositoryTest->getVersion()); + self::assertEquals('version', $this->mySQLRepositoryTest->getVersion()); } public function testShouldGetMasterStatus(): void From 418625f36dcdb9b3fcbb24e94783556a26ee8d6d Mon Sep 17 00:00:00 2001 From: hetao Date: Fri, 30 Aug 2024 11:19:40 +0800 Subject: [PATCH 04/10] fix issue #125 --- composer.json | 4 ++-- .../BinaryDataReader/BinaryDataReader.php | 22 ++++++++++++++----- src/MySQLReplication/Event/RotateEvent.php | 2 +- .../Event/RowEvent/RowEvent.php | 2 +- src/MySQLReplication/Event/XidEvent.php | 2 +- 5 files changed, 21 insertions(+), 11 deletions(-) diff --git a/composer.json b/composer.json index cae9b1c..99c1f74 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "ext-json": "*", "ext-sockets": "*", "doctrine/collections": "^2.1", - "doctrine/dbal": "^3.8", + "doctrine/dbal": "^4.0", "psr/log": "^3.0", "psr/simple-cache": "^3.0", "symfony/event-dispatcher": "^6.0|^7.0" @@ -26,7 +26,7 @@ "kubawerlos/php-cs-fixer-custom-fixers": "^3.19", "monolog/monolog": "^3.5", "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^10.5", + "phpunit/phpunit": "^11.0", "symplify/easy-coding-standard": "^12.1" }, "license": "MIT", diff --git a/src/MySQLReplication/BinaryDataReader/BinaryDataReader.php b/src/MySQLReplication/BinaryDataReader/BinaryDataReader.php index 3a8119f..ca521ee 100644 --- a/src/MySQLReplication/BinaryDataReader/BinaryDataReader.php +++ b/src/MySQLReplication/BinaryDataReader/BinaryDataReader.php @@ -114,16 +114,21 @@ public function readUInt24(): int return $data[1] + ($data[2] << 8) + ($data[3] << 16); } - public function readUInt64(): string + public function readUInt64(): string|int { return $this->unpackUInt64($this->read(self::UNSIGNED_INT64_LENGTH)); } - public function unpackUInt64(string $binary): string + public function unpackUInt64(string $binary): string|int { $data = self::unpack('V*', $binary); - return bcadd((string)$data[1], bcmul((string)$data[2], bcpow('2', '32'))); + $num = bcadd((string)$data[1], bcmul((string)$data[2], bcpow('2', '32'))); + if($num>PHP_INT_MAX || $numread(self::UNSIGNED_INT64_LENGTH)); - return bcadd((string)$data[1], (string)($data[2] << 32)); + $num = bcadd((string)$data[1], (string)($data[2] << 32)); + if($num>PHP_INT_MAX || $numunpackUInt64($this->read(self::UNSIGNED_INT48_LENGTH) . chr(0) . chr(0)); + return (string)$this->unpackUInt64($this->read(self::UNSIGNED_INT48_LENGTH) . chr(0) . chr(0)); } public function isComplete(int $size): bool diff --git a/src/MySQLReplication/Event/RotateEvent.php b/src/MySQLReplication/Event/RotateEvent.php index 0e0d2b9..58f3118 100644 --- a/src/MySQLReplication/Event/RotateEvent.php +++ b/src/MySQLReplication/Event/RotateEvent.php @@ -13,7 +13,7 @@ class RotateEvent extends EventCommon { public function makeRotateEventDTO(): RotateDTO { - $binFilePos = $this->binaryDataReader->readUInt64(); + $binFilePos = (string)$this->binaryDataReader->readUInt64(); $binFileName = $this->binaryDataReader->read( $this->eventInfo->getSizeNoHeader() - $this->getSizeToRemoveByVersion() ); diff --git a/src/MySQLReplication/Event/RowEvent/RowEvent.php b/src/MySQLReplication/Event/RowEvent/RowEvent.php index 66d04af..60bc24c 100644 --- a/src/MySQLReplication/Event/RowEvent/RowEvent.php +++ b/src/MySQLReplication/Event/RowEvent/RowEvent.php @@ -416,7 +416,7 @@ public function makeUpdateRowsDTO(): ?UpdateRowsDTO protected function findTableMap(): ?TableMap { - $tableId = $this->binaryDataReader->readTableId(); + $tableId = (string)$this->binaryDataReader->readTableId(); $this->binaryDataReader->advance(2); if (in_array( diff --git a/src/MySQLReplication/Event/XidEvent.php b/src/MySQLReplication/Event/XidEvent.php index 39611fa..a59f981 100644 --- a/src/MySQLReplication/Event/XidEvent.php +++ b/src/MySQLReplication/Event/XidEvent.php @@ -13,6 +13,6 @@ class XidEvent extends EventCommon { public function makeXidDTO(): XidDTO { - return new XidDTO($this->eventInfo, $this->binaryDataReader->readUInt64()); + return new XidDTO($this->eventInfo, (string)$this->binaryDataReader->readUInt64()); } } From 0c31198d20c8879c9169135d8fcc60fdd67a2acb Mon Sep 17 00:00:00 2001 From: hetao Date: Fri, 30 Aug 2024 11:22:37 +0800 Subject: [PATCH 05/10] change to hetao29 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 99c1f74..e7fe3b9 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "krowinski/php-mysql-replication", + "name": "hetao29/php-mysql-replication", "description": "Pure PHP Implementation of MySQL replication protocol. This allow you to receive event like insert, update, delete with their data and raw SQL queries.", "keywords": [ "mysql-replication", From f0fa73206617231db1ff8cc22ced193786525cdd Mon Sep 17 00:00:00 2001 From: hetao Date: Mon, 17 Feb 2025 10:36:24 +0800 Subject: [PATCH 06/10] Fix PHP 8.4 deprecation: Implicitly marking parameter as nullable is deprecated, the explicit nullable type must be used instead --- src/MySQLReplication/MySQLReplicationFactory.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/MySQLReplication/MySQLReplicationFactory.php b/src/MySQLReplication/MySQLReplicationFactory.php index 59e9604..43391ba 100644 --- a/src/MySQLReplication/MySQLReplicationFactory.php +++ b/src/MySQLReplication/MySQLReplicationFactory.php @@ -33,11 +33,11 @@ class MySQLReplicationFactory public function __construct( Config $config, - RepositoryInterface $repository = null, - CacheInterface $cache = null, - EventDispatcherInterface $eventDispatcher = null, - SocketInterface $socket = null, - LoggerInterface $logger = null + RepositoryInterface|null $repository = null, + CacheInterface|null $cache = null, + EventDispatcherInterface|null $eventDispatcher = null, + SocketInterface|null $socket = null, + LoggerInterface|null $logger = null ) { $config->validate(); From 09abaf4133e36d6e9051b990a1dde6e50f221f43 Mon Sep 17 00:00:00 2001 From: hetao Date: Mon, 21 Apr 2025 17:30:13 +0800 Subject: [PATCH 07/10] change to krowinski --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index e7fe3b9..99c1f74 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "hetao29/php-mysql-replication", + "name": "krowinski/php-mysql-replication", "description": "Pure PHP Implementation of MySQL replication protocol. This allow you to receive event like insert, update, delete with their data and raw SQL queries.", "keywords": [ "mysql-replication", From f9f6c79e6092c7bbc743a32edff851a8c91ed041 Mon Sep 17 00:00:00 2001 From: cyppe Date: Sun, 22 Jun 2025 14:04:05 +0200 Subject: [PATCH 08/10] Fix maintainer feedback: always return strings from readInt64/unpackUInt64 - Changed readInt64() to always return string instead of string|int - Changed unpackUInt64() to always return string instead of string|int - Changed readUInt64() return type to string for consistency - Applied code style fixes with composer cs:fix - Fixes failing tests: testShouldReadInt64 and testShouldPack64bit Addresses maintainer feedback from PR #116 regarding MySQL 8.4.0 compatibility. --- .../BinLog/BinLogSocketConnect.php | 10 ++++---- .../BinaryDataReader/BinaryDataReader.php | 20 ++++------------ src/MySQLReplication/Config/Config.php | 24 +++++++++---------- .../Repository/MySQLRepository.php | 2 +- tests/Unit/Repository/MySQLRepositoryTest.php | 12 +++++----- 5 files changed, 29 insertions(+), 39 deletions(-) diff --git a/src/MySQLReplication/BinLog/BinLogSocketConnect.php b/src/MySQLReplication/BinLog/BinLogSocketConnect.php index 68578c5..7755f38 100644 --- a/src/MySQLReplication/BinLog/BinLogSocketConnect.php +++ b/src/MySQLReplication/BinLog/BinLogSocketConnect.php @@ -195,11 +195,11 @@ private function getBinlogStream(): void if ($this->config->heartbeatPeriod > 0.00) { // master_heartbeat_period is in nanoseconds - if(version_compare($this->repository->getVersion(),"8.4.0")>=0){ - $this->executeSQL('SET @source_heartbeat_period = ' . $this->config->heartbeatPeriod * 1000000000); - }else{ - $this->executeSQL('SET @master_heartbeat_period = ' . $this->config->heartbeatPeriod * 1000000000); - } + if (version_compare($this->repository->getVersion(), '8.4.0') >= 0) { + $this->executeSQL('SET @source_heartbeat_period = ' . $this->config->heartbeatPeriod * 1000000000); + } else { + $this->executeSQL('SET @master_heartbeat_period = ' . $this->config->heartbeatPeriod * 1000000000); + } $this->logger->debug('Heartbeat period set to ' . $this->config->heartbeatPeriod . ' seconds'); } diff --git a/src/MySQLReplication/BinaryDataReader/BinaryDataReader.php b/src/MySQLReplication/BinaryDataReader/BinaryDataReader.php index ca521ee..a709767 100644 --- a/src/MySQLReplication/BinaryDataReader/BinaryDataReader.php +++ b/src/MySQLReplication/BinaryDataReader/BinaryDataReader.php @@ -114,21 +114,16 @@ public function readUInt24(): int return $data[1] + ($data[2] << 8) + ($data[3] << 16); } - public function readUInt64(): string|int + public function readUInt64(): string { return $this->unpackUInt64($this->read(self::UNSIGNED_INT64_LENGTH)); } - public function unpackUInt64(string $binary): string|int + public function unpackUInt64(string $binary): string { $data = self::unpack('V*', $binary); - $num = bcadd((string)$data[1], bcmul((string)$data[2], bcpow('2', '32'))); - if($num>PHP_INT_MAX || $numread(self::UNSIGNED_INT64_LENGTH)); - $num = bcadd((string)$data[1], (string)($data[2] << 32)); - if($num>PHP_INT_MAX || $numdatabasesOnly !== [] && !in_array($database, $this->databasesOnly, true)) - || ($this->databasesRegex !== [] && !self::matchNames($database, $this->databasesRegex)); + || ($this->databasesRegex !== [] && !self::matchNames($database, $this->databasesRegex)); } @@ -116,17 +116,6 @@ public function checkTablesOnly(string $table): bool || ($this->tablesRegex !== [] && !self::matchNames($table, $this->tablesRegex)); } - private static function matchNames(string $name, array $patterns): bool - { - foreach ($patterns as $pattern) { - if (preg_match($pattern, $name)) { - return true; - } - } - - return false; - } - public function checkEvent(int $type): bool { if ($this->eventsOnly !== [] && !in_array($type, $this->eventsOnly, true)) { @@ -144,4 +133,15 @@ public function jsonSerialize(): array { return get_class_vars(self::class); } + + private static function matchNames(string $name, array $patterns): bool + { + foreach ($patterns as $pattern) { + if (preg_match($pattern, $name)) { + return true; + } + } + + return false; + } } diff --git a/src/MySQLReplication/Repository/MySQLRepository.php b/src/MySQLReplication/Repository/MySQLRepository.php index 2645cef..9a4373a 100644 --- a/src/MySQLReplication/Repository/MySQLRepository.php +++ b/src/MySQLReplication/Repository/MySQLRepository.php @@ -60,7 +60,7 @@ public function getVersion(): string $res = $this->getConnection() ->fetchAssociative('SHOW VARIABLES LIKE "version"'); - return $res['Value']??""; + return $res['Value'] ?? ''; } public function getMasterStatus(): MasterStatusDTO diff --git a/tests/Unit/Repository/MySQLRepositoryTest.php b/tests/Unit/Repository/MySQLRepositoryTest.php index d126124..14f28fa 100644 --- a/tests/Unit/Repository/MySQLRepositoryTest.php +++ b/tests/Unit/Repository/MySQLRepositoryTest.php @@ -70,14 +70,14 @@ public function testShouldIsCheckSum(): void public function testShouldGetVersion(): void { - $expected = [ - 'Value' => 'version', - ]; + $expected = [ + 'Value' => 'version', + ]; - $this->connection->method('fetchAssociative') - ->willReturn($expected); + $this->connection->method('fetchAssociative') + ->willReturn($expected); - self::assertEquals('version', $this->mySQLRepositoryTest->getVersion()); + self::assertEquals('version', $this->mySQLRepositoryTest->getVersion()); } public function testShouldGetMasterStatus(): void From 249606950790196c350e5cd7d6e852f562d3f8ca Mon Sep 17 00:00:00 2001 From: cyppe Date: Sun, 22 Jun 2025 14:15:41 +0200 Subject: [PATCH 09/10] Fix DBAL 4.x compatibility issue in testShouldReconnect In DBAL 4.x, Doctrine\DBAL\Exception became an interface instead of a concrete class. The test was trying to instantiate 'new Exception('')' which fails with 'Cannot instantiate interface Doctrine\DBAL\Exception'. Fixed by using ConnectionException mock which implements the Exception interface, preserving the exact same test logic and coverage as the original DBAL 3.x version. --- tests/Unit/Repository/MySQLRepositoryTest.php | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/Unit/Repository/MySQLRepositoryTest.php b/tests/Unit/Repository/MySQLRepositoryTest.php index 14f28fa..1a9d9da 100644 --- a/tests/Unit/Repository/MySQLRepositoryTest.php +++ b/tests/Unit/Repository/MySQLRepositoryTest.php @@ -8,6 +8,7 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\Exception; +use Doctrine\DBAL\Exception\ConnectionException; use Doctrine\DBAL\Platforms\MySQLPlatform; use MySQLReplication\Repository\FieldDTOCollection; use MySQLReplication\Repository\MasterStatusDTO; @@ -96,13 +97,17 @@ public function testShouldGetMasterStatus(): void self::assertEquals(MasterStatusDTO::makeFromArray($expected), $this->mySQLRepositoryTest->getMasterStatus()); } - public function testShouldReconnect(): void + public function testShouldReconnect(): void { // just to cover private getConnection + $exception = $this->createMock(ConnectionException::class); + $this->connection->method('executeQuery') - ->willReturnCallback(static function () { - throw new Exception(''); - }); + ->willThrowException($exception); + + $this->connection->method('fetchAssociative') + ->willReturn(['Value' => 'NONE']); + $this->mySQLRepositoryTest->isCheckSum(); self::assertTrue(true); } From 603ac5d9eb9023b207789bd737173fa96aead4f2 Mon Sep 17 00:00:00 2001 From: cyppe Date: Sun, 22 Jun 2025 14:19:44 +0200 Subject: [PATCH 10/10] Fix MySQLRepository for DBAL 4.x compatibility Remove manual connection reconnection logic since DBAL 4.x handles reconnection automatically. The connect() method became protected in DBAL 4.x, causing 'Call to protected method' errors. Changes: - Removed manual ping/close/connect logic from getConnection() - DBAL 4.x automatically handles lost connections and reconnection - Kept ping() method as required by PingableConnection interface This fixes the repository to work with DBAL 4.x while maintaining the same functionality. --- src/MySQLReplication/Repository/MySQLRepository.php | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/MySQLReplication/Repository/MySQLRepository.php b/src/MySQLReplication/Repository/MySQLRepository.php index 9a4373a..b38c994 100644 --- a/src/MySQLReplication/Repository/MySQLRepository.php +++ b/src/MySQLReplication/Repository/MySQLRepository.php @@ -37,8 +37,8 @@ public function getFields(string $database, string $table): FieldDTOCollection `TABLE_SCHEMA` = ? AND `TABLE_NAME` = ? - ORDER BY - ORDINAL_POSITION + ORDER BY + ORDINAL_POSITION '; return FieldDTOCollection::makeFromArray( @@ -95,11 +95,8 @@ public function ping(Connection $connection): bool private function getConnection(): Connection { - if ($this->ping($this->connection) === false) { - $this->connection->close(); - $this->connection->connect(); - } - + // In DBAL 4.x, connections handle reconnection automatically + // No need for manual ping/reconnect logic return $this->connection; } }