diff --git a/composer.json b/composer.json index cfb4e323..2aeb6883 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,7 @@ ], "require": { "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", - "doctrine/dbal": "^4.0.0", + "doctrine/dbal": "^4.4.0", "doctrine/migrations": "^3.3.2", "patchlevel/hydrator": "^1.8.0", "patchlevel/worker": "^1.4.0", diff --git a/composer.lock b/composer.lock index 5641691b..ebf28ee5 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "eaaa35929b4073dbe5946b8486ca1a64", + "content-hash": "040555186133771a1ea827cd0aade8d4", "packages": [ { "name": "brick/math", @@ -4652,11 +4652,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.45", + "version": "2.1.46", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/f8cdfd9421b7edb7686a2d150a234870464eac70", - "reference": "f8cdfd9421b7edb7686a2d150a234870464eac70", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/a193923fc2d6325ef4e741cf3af8c3e8f54dbf25", + "reference": "a193923fc2d6325ef4e741cf3af8c3e8f54dbf25", "shasum": "" }, "require": { @@ -4701,7 +4701,7 @@ "type": "github" } ], - "time": "2026-03-30T13:22:02+00:00" + "time": "2026-04-01T09:25:14+00:00" }, { "name": "phpstan/phpstan-phpunit", @@ -6815,16 +6815,16 @@ }, { "name": "symfony/filesystem", - "version": "v8.0.6", + "version": "v8.0.8", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "7bf9162d7a0dff98d079b72948508fa48018a770" + "reference": "66b769ae743ce2d13e435528fbef4af03d623e5a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/7bf9162d7a0dff98d079b72948508fa48018a770", - "reference": "7bf9162d7a0dff98d079b72948508fa48018a770", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/66b769ae743ce2d13e435528fbef4af03d623e5a", + "reference": "66b769ae743ce2d13e435528fbef4af03d623e5a", "shasum": "" }, "require": { @@ -6861,7 +6861,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v8.0.6" + "source": "https://github.com/symfony/filesystem/tree/v8.0.8" }, "funding": [ { @@ -6881,7 +6881,7 @@ "type": "tidelift" } ], - "time": "2026-02-25T16:59:43+00:00" + "time": "2026-03-30T15:14:47+00:00" }, { "name": "symfony/messenger", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index f7e40e35..b84c95a1 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -312,6 +312,12 @@ parameters: count: 1 path: tests/Integration/Store/StreamDoctrineDbalStoreTest.php + - + message: '#^Parameter \#1 \$table of class Patchlevel\\EventSourcing\\Subscription\\Cleanup\\Dbal\\DropTableTask constructor expects non\-empty\-string, string given\.$#' + identifier: argument.type + count: 1 + path: tests/Integration/Subscription/Subscriber/ProfileProjectionWithCleanup.php + - message: '#^Call to static method PHPUnit\\Framework\\Assert\:\:assertArrayHasKey\(\) with 0 and array\{Patchlevel\\EventSourcing\\Subscription\\Subscription\} will always evaluate to true\.$#' identifier: staticMethod.alreadyNarrowedType @@ -336,18 +342,6 @@ parameters: count: 1 path: tests/Unit/Aggregate/AggregateRootTest.php - - - message: '#^Cannot access offset 0 on iterable\\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 2 - path: tests/Unit/CommandBus/AggregateHandlerProviderTest.php - - - - message: '#^Cannot call method callable\(\) on mixed\.$#' - identifier: method.nonObject - count: 2 - path: tests/Unit/CommandBus/AggregateHandlerProviderTest.php - - message: '#^Parameter \#2 \$aggregateClass of class Patchlevel\\EventSourcing\\CommandBus\\Handler\\CreateAggregateHandler constructor expects class\-string\, string given\.$#' identifier: argument.type @@ -366,18 +360,6 @@ parameters: count: 5 path: tests/Unit/CommandBus/InstantRetryCommandBusTest.php - - - message: '#^Cannot access offset 0 on iterable\\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 2 - path: tests/Unit/CommandBus/ServiceHandlerProviderTest.php - - - - message: '#^Cannot call method callable\(\) on mixed\.$#' - identifier: method.nonObject - count: 2 - path: tests/Unit/CommandBus/ServiceHandlerProviderTest.php - - message: '#^Parameter \#1 \$data of static method Patchlevel\\EventSourcing\\Tests\\Unit\\Fixture\\Message\:\:fromArray\(\) expects array\{id\: string, text\: string, createdAt\: string\}, array\ given\.$#' identifier: argument.type diff --git a/src/Subscription/Cleanup/Dbal/DbalCleanupTaskHandler.php b/src/Subscription/Cleanup/Dbal/DbalCleanupTaskHandler.php index 110268b5..6fe084a9 100644 --- a/src/Subscription/Cleanup/Dbal/DbalCleanupTaskHandler.php +++ b/src/Subscription/Cleanup/Dbal/DbalCleanupTaskHandler.php @@ -9,6 +9,8 @@ use Patchlevel\EventSourcing\Subscription\Cleanup\CleanupTaskHandler; use Patchlevel\EventSourcing\Subscription\Cleanup\CleanupTaskNotSupported; +use function strtolower; + final class DbalCleanupTaskHandler implements CleanupTaskHandler { public function __construct( @@ -19,13 +21,27 @@ public function __construct( public function __invoke(object $task): void { if ($task instanceof DropTableTask) { - $this->connection($task->connectionName)->createSchemaManager()->dropTable($task->table); + $schemaManager = $this->connection($task->connectionName)->createSchemaManager(); + if ($schemaManager->tablesExist([$task->table])) { + $schemaManager->dropTable($task->table); + } return; } if ($task instanceof DropIndexTask) { - $this->connection($task->connectionName)->createSchemaManager()->dropIndex($task->index, $task->table); + $schemaManager = $this->connection($task->connectionName)->createSchemaManager(); + + if (!$schemaManager->tablesExist([$task->table])) { + return; + } + + foreach ($schemaManager->introspectTableIndexesByUnquotedName($task->table) as $index) { + if (strtolower($index->getObjectName()->toString()) === strtolower($task->index)) { + $schemaManager->dropIndex($task->index, $task->table); + break; + } + } return; } diff --git a/src/Subscription/Cleanup/Dbal/DropIndexTask.php b/src/Subscription/Cleanup/Dbal/DropIndexTask.php index 6dcad506..5e657d26 100644 --- a/src/Subscription/Cleanup/Dbal/DropIndexTask.php +++ b/src/Subscription/Cleanup/Dbal/DropIndexTask.php @@ -6,6 +6,11 @@ final class DropIndexTask { + /** + * @param non-empty-string $index + * @param non-empty-string $table + * @param non-empty-string|null $connectionName + */ public function __construct( public readonly string $index, public readonly string $table, diff --git a/src/Subscription/Cleanup/Dbal/DropTableTask.php b/src/Subscription/Cleanup/Dbal/DropTableTask.php index 73a363da..62d5231c 100644 --- a/src/Subscription/Cleanup/Dbal/DropTableTask.php +++ b/src/Subscription/Cleanup/Dbal/DropTableTask.php @@ -6,6 +6,10 @@ final class DropTableTask { + /** + * @param non-empty-string $table + * @param non-empty-string|null $connectionName + */ public function __construct( public readonly string $table, public readonly string|null $connectionName = null, diff --git a/tests/Unit/Subscription/Cleanup/Dbal/DbalCleanupTaskHandlerTest.php b/tests/Unit/Subscription/Cleanup/Dbal/DbalCleanupTaskHandlerTest.php index 04a6252d..373cb25f 100644 --- a/tests/Unit/Subscription/Cleanup/Dbal/DbalCleanupTaskHandlerTest.php +++ b/tests/Unit/Subscription/Cleanup/Dbal/DbalCleanupTaskHandlerTest.php @@ -6,6 +6,7 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\Schema\AbstractSchemaManager; +use Doctrine\DBAL\Schema\Index; use Doctrine\Persistence\ConnectionRegistry; use Patchlevel\EventSourcing\Subscription\Cleanup\CleanupTaskNotSupported; use Patchlevel\EventSourcing\Subscription\Cleanup\Dbal\ConnectionNameNotSupported; @@ -53,6 +54,7 @@ public function testHandleNoSupportedTask(): void public function testHandleDropTable(): void { $schemaManager = $this->createMock(AbstractSchemaManager::class); + $schemaManager->expects($this->once())->method('tablesExist')->with(['test'])->willReturn(true); $schemaManager->expects($this->once())->method('dropTable')->with('test'); $connection = $this->createMock(Connection::class); @@ -66,9 +68,32 @@ public function testHandleDropTable(): void $handler(new DropTableTask('test')); } + public function testHandleDropTableIfNotExists(): void + { + $schemaManager = $this->createMock(AbstractSchemaManager::class); + $schemaManager->expects($this->once())->method('tablesExist')->with(['test'])->willReturn(false); + $schemaManager->expects($this->never())->method('dropTable'); + + $connection = $this->createMock(Connection::class); + $connection + ->expects($this->once()) + ->method('createSchemaManager') + ->willReturn($schemaManager); + + $handler = new DbalCleanupTaskHandler($connection); + + $handler(new DropTableTask('test')); + } + public function testHandleDropIndex(): void { $schemaManager = $this->createMock(AbstractSchemaManager::class); + $schemaManager->expects($this->once())->method('tablesExist')->with(['bar'])->willReturn(true); + $schemaManager + ->expects($this->once()) + ->method('introspectTableIndexesByUnquotedName') + ->with('bar') + ->willReturn([new Index('foo', ['id'])]); $schemaManager->expects($this->once())->method('dropIndex')->with('foo', 'bar'); $connection = $this->createMock(Connection::class); @@ -82,9 +107,50 @@ public function testHandleDropIndex(): void $handler(new DropIndexTask('foo', 'bar')); } + public function testHandleDropIndexIfTableNotExists(): void + { + $schemaManager = $this->createMock(AbstractSchemaManager::class); + $schemaManager->expects($this->once())->method('tablesExist')->with(['bar'])->willReturn(false); + $schemaManager->expects($this->never())->method('introspectTableIndexesByUnquotedName'); + $schemaManager->expects($this->never())->method('dropIndex'); + + $connection = $this->createMock(Connection::class); + $connection + ->expects($this->once()) + ->method('createSchemaManager') + ->willReturn($schemaManager); + + $handler = new DbalCleanupTaskHandler($connection); + + $handler(new DropIndexTask('foo', 'bar')); + } + + public function testHandleDropIndexIfIndexNotExists(): void + { + $schemaManager = $this->createMock(AbstractSchemaManager::class); + $schemaManager->expects($this->once())->method('tablesExist')->with(['bar'])->willReturn(true); + $schemaManager + ->expects($this->once()) + ->method('introspectTableIndexesByUnquotedName') + ->with('bar') + ->willReturn([new Index('baz', ['id'])]); + $schemaManager->expects($this->never())->method('dropIndex'); + + $connection = $this->createMock(Connection::class); + $connection + ->expects($this->once()) + ->method('createSchemaManager') + ->willReturn($schemaManager); + + $handler = new DbalCleanupTaskHandler($connection); + + $handler(new DropIndexTask('foo', 'bar')); + } + public function testHandleWithConnectionRegistry(): void { $schemaManager = $this->createMock(AbstractSchemaManager::class); + $schemaManager->expects($this->once())->method('tablesExist')->with(['test'])->willReturn(true); $schemaManager->expects($this->once())->method('dropTable')->with('test'); $connection = $this->createMock(Connection::class);