From 79aab5fac744b48f62cade451f6ee2e0001d616a Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Fri, 31 Jan 2025 13:40:53 +0100 Subject: [PATCH 1/3] Dispatch events for migration execution --- composer.json | 4 +- composer.lock | 142 +++++++++++++++++- phpcs.xml.dist | 1 - src/Event/MigrationExecutionFailedEvent.php | 21 +++ src/Event/MigrationExecutionStartedEvent.php | 19 +++ .../MigrationExecutionSucceededEvent.php | 19 +++ src/MigrationService.php | 39 +++-- tests/MigrationServiceTest.php | 24 ++- 8 files changed, 246 insertions(+), 23 deletions(-) create mode 100644 src/Event/MigrationExecutionFailedEvent.php create mode 100644 src/Event/MigrationExecutionStartedEvent.php create mode 100644 src/Event/MigrationExecutionSucceededEvent.php diff --git a/composer.json b/composer.json index 8e83963..72586a8 100644 --- a/composer.json +++ b/composer.json @@ -8,6 +8,7 @@ "php": "^8.1", "doctrine/dbal": "^3.6.0 || ^4.0.0", "doctrine/orm": "^3.0.0", + "psr/event-dispatcher": "^1.0", "symfony/console": "^5.4.0 || ^6.0.0 || ^7.0.0" }, "require-dev": { @@ -21,7 +22,8 @@ "psr/log": "^3", "shipmonk/composer-dependency-analyser": "^1.7.0", "shipmonk/phpstan-rules": "^4.0.0", - "slevomat/coding-standard": "^8.15.0" + "slevomat/coding-standard": "^8.15.0", + "symfony/event-dispatcher-contracts": "^3.5" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index 1ba70c4..ea30e94 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": "c5029b45f134182ecb44a875f560bbe5", + "content-hash": "2ed2f646e52a4e69c7adeb1f36fb51c9", "packages": [ { "name": "doctrine/collections", @@ -863,6 +863,56 @@ }, "time": "2021-11-05T16:47:00+00:00" }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, { "name": "psr/log", "version": "3.0.2", @@ -4577,16 +4627,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.10.1", + "version": "3.11.3", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "8f90f7a53ce271935282967f53d0894f8f1ff877" + "reference": "ba05f990e79cbe69b9f35c8c1ac8dca7eecc3a10" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/8f90f7a53ce271935282967f53d0894f8f1ff877", - "reference": "8f90f7a53ce271935282967f53d0894f8f1ff877", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/ba05f990e79cbe69b9f35c8c1ac8dca7eecc3a10", + "reference": "ba05f990e79cbe69b9f35c8c1ac8dca7eecc3a10", "shasum": "" }, "require": { @@ -4651,9 +4701,89 @@ { "url": "https://opencollective.com/php_codesniffer", "type": "open_collective" + }, + { + "url": "https://thanks.dev/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-01-23T17:04:15+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/7642f5e970b672283b7823222ae8ef8bbc160b9f", + "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "time": "2024-05-22T21:24:41+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "theseer/tokenizer", diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 5a3dd8c..70dd772 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -278,7 +278,6 @@ - diff --git a/src/Event/MigrationExecutionFailedEvent.php b/src/Event/MigrationExecutionFailedEvent.php new file mode 100644 index 0000000..93391fd --- /dev/null +++ b/src/Event/MigrationExecutionFailedEvent.php @@ -0,0 +1,21 @@ +entityManager = $entityManager; @@ -48,7 +55,8 @@ public function __construct( $this->config = $config; $this->executor = $executor ?? new MigrationDefaultExecutor($this->connection); $this->versionProvider = $versionProvider ?? new MigrationDefaultVersionProvider(); - $this->migrationsAnalyzer = $migrationsAnalyzer ?? new MigrationDefaultAnalyzer(); + $this->migrationAnalyzer = $migrationAnalyzer ?? new MigrationDefaultAnalyzer(); + $this->eventDispatcher = $eventDispatcher; } public function getConfig(): MigrationConfig @@ -67,17 +75,22 @@ public function executeMigration(string $version, MigrationPhase $phase): Migrat { $migration = $this->getMigration($version); - if ($migration instanceof TransactionalMigration) { - try { - $this->connection->beginTransaction(); + $this->eventDispatcher?->dispatch(new MigrationExecutionStartedEvent($migration, $version, $phase)); + + try { + if ($migration instanceof TransactionalMigration) { + $run = $this->connection->transactional(function () use ($migration, $version, $phase): MigrationRun { + return $this->doExecuteMigration($migration, $version, $phase); + }); + } else { $run = $this->doExecuteMigration($migration, $version, $phase); - $this->connection->commit(); - } catch (Throwable $e) { - $this->connection->rollBack(); - throw $e; } - } else { - $run = $this->doExecuteMigration($migration, $version, $phase); + + $this->eventDispatcher?->dispatch(new MigrationExecutionSucceededEvent($migration, $version, $phase)); + + } catch (Throwable $e) { + $this->eventDispatcher?->dispatch(new MigrationExecutionFailedEvent($migration, $version, $phase, $e)); + throw $e; } return $run; @@ -231,7 +244,7 @@ private function excludeTablesFromSchema(Schema $schema): void */ public function generateMigrationFile(array $sqls): MigrationFile { - $statements = $this->migrationsAnalyzer->analyze($sqls); + $statements = $this->migrationAnalyzer->analyze($sqls); $statementsBefore = $statementsAfter = []; foreach ($statements as $statement) { diff --git a/tests/MigrationServiceTest.php b/tests/MigrationServiceTest.php index bfa1e37..e3b3cef 100644 --- a/tests/MigrationServiceTest.php +++ b/tests/MigrationServiceTest.php @@ -5,6 +5,9 @@ use Doctrine\ORM\EntityManagerInterface; use LogicException; use PHPUnit\Framework\TestCase; +use ShipMonk\Doctrine\Migration\Event\MigrationExecutionStartedEvent; +use ShipMonk\Doctrine\Migration\Event\MigrationExecutionSucceededEvent; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use function array_map; use function file_get_contents; use function glob; @@ -22,9 +25,24 @@ class MigrationServiceTest extends TestCase public function testInitGenerationExecution(): void { + $invokedCount = self::exactly(4); + $eventDispatcher = $this->createMock(EventDispatcherInterface::class); + $eventDispatcher->expects($invokedCount) + ->method('dispatch') + ->willReturnCallback(static function (object $event) use ($invokedCount): object { + match ($invokedCount->numberOfInvocations()) { + 1 => self::assertInstanceOf(MigrationExecutionStartedEvent::class, $event, $event::class), + 2 => self::assertInstanceOf(MigrationExecutionSucceededEvent::class, $event, $event::class), + 3 => self::assertInstanceOf(MigrationExecutionStartedEvent::class, $event, $event::class), + 4 => self::assertInstanceOf(MigrationExecutionSucceededEvent::class, $event, $event::class), + default => self::fail('Unexpected event'), + }; + return $event; + }); + [$entityManager] = $this->createEntityManagerAndLogger(); $connection = $entityManager->getConnection(); - $service = $this->createMigrationService($entityManager); + $service = $this->createMigrationService($entityManager, eventDispatcher: $eventDispatcher); $migrationTableName = $service->getConfig()->getMigrationTableName(); @@ -134,7 +152,7 @@ public function getNextVersion(): string }; [$entityManager, $logger] = $this->createEntityManagerAndLogger(); - $migrationsService = $this->createMigrationService($entityManager, [], false, $versionProvider); + $migrationsService = $this->createMigrationService($entityManager, versionProvider: $versionProvider); $migrationsService->initializeMigrationTable(); $logger->clean(); @@ -296,6 +314,7 @@ private function createMigrationService( bool $transactional = false, ?MigrationVersionProvider $versionProvider = null, ?MigrationAnalyzer $statementAnalyzer = null, + ?EventDispatcherInterface $eventDispatcher = null, ): MigrationService { $migrationsDir = $this->getMigrationsTestDir(); @@ -329,6 +348,7 @@ private function createMigrationService( null, $versionProvider, $statementAnalyzer, + $eventDispatcher, ); } From b78f34e0d5aec7fcb9df514b3d85cb21e99e33b6 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Fri, 31 Jan 2025 13:46:56 +0100 Subject: [PATCH 2/3] drop symfony dispatcher from dev deps --- composer.json | 3 +- composer.lock | 78 +--------------------------------- tests/MigrationServiceTest.php | 2 +- 3 files changed, 3 insertions(+), 80 deletions(-) diff --git a/composer.json b/composer.json index 72586a8..75a813d 100644 --- a/composer.json +++ b/composer.json @@ -22,8 +22,7 @@ "psr/log": "^3", "shipmonk/composer-dependency-analyser": "^1.7.0", "shipmonk/phpstan-rules": "^4.0.0", - "slevomat/coding-standard": "^8.15.0", - "symfony/event-dispatcher-contracts": "^3.5" + "slevomat/coding-standard": "^8.15.0" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index ea30e94..6db4e0e 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": "2ed2f646e52a4e69c7adeb1f36fb51c9", + "content-hash": "5c41d9dac8545d8b9e69f47298be8651", "packages": [ { "name": "doctrine/collections", @@ -4709,82 +4709,6 @@ ], "time": "2025-01-23T17:04:15+00:00" }, - { - "name": "symfony/event-dispatcher-contracts", - "version": "v3.5.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/7642f5e970b672283b7823222ae8ef8bbc160b9f", - "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f", - "shasum": "" - }, - "require": { - "php": ">=8.1", - "psr/event-dispatcher": "^1" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/contracts", - "name": "symfony/contracts" - }, - "branch-alias": { - "dev-main": "3.5-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Contracts\\EventDispatcher\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Generic abstractions related to dispatching event", - "homepage": "https://symfony.com", - "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" - ], - "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.1" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-25T14:20:29+00:00" - }, { "name": "theseer/tokenizer", "version": "1.2.3", diff --git a/tests/MigrationServiceTest.php b/tests/MigrationServiceTest.php index e3b3cef..8396914 100644 --- a/tests/MigrationServiceTest.php +++ b/tests/MigrationServiceTest.php @@ -5,9 +5,9 @@ use Doctrine\ORM\EntityManagerInterface; use LogicException; use PHPUnit\Framework\TestCase; +use Psr\EventDispatcher\EventDispatcherInterface; use ShipMonk\Doctrine\Migration\Event\MigrationExecutionStartedEvent; use ShipMonk\Doctrine\Migration\Event\MigrationExecutionSucceededEvent; -use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use function array_map; use function file_get_contents; use function glob; From 2b9efd389dc1ade1d1feb5692ff26c6065b3d7a6 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Fri, 31 Jan 2025 21:13:31 +0100 Subject: [PATCH 3/3] Readme --- README.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7b9425d..7e81693 100644 --- a/README.md +++ b/README.md @@ -177,12 +177,23 @@ Implement `executeQuery()` to run checks or other code before/after each query. Interface of this method mimics interface of `Doctrine\DBAL\Connection::executeQuery()`. #### Alter generated migration SQLs: + You can implement custom `MigrationAnalyzer` and register it as a service. This allows you to alter generated SQLs (e.g. add `ALGORITHM=INSTANT`) and assign them to proper phase. +#### Hook to migration execution: + +If you pass `Psr\EventDispatcher\EventDispatcherInterface` to `MigrationService`, you can hook into migration execution. +Dispatched events are: +- `MigrationExecutionStartedEvent` +- `MigrationExecutionSucceededEvent` +- `MigrationExecutionFailedEvent` + +All events have `MigrationPhase` enum and `Migration` instance available. + #### Run all queries within transaction: -You can change your template (or a single migration) to extend; `TransactionalMigration`. +You can change your template (or a single migration) to extend `TransactionalMigration`. That causes each phases to be executed within migration. Be aware that many databases (like MySQL) does not support transaction over DDL operations (ALTER and such).