Skip to content

Commit

Permalink
Dispatch events for migration execution (#114)
Browse files Browse the repository at this point in the history
* Dispatch events for migration execution

* drop symfony dispatcher from dev deps

* Readme
  • Loading branch information
janedbal authored Feb 3, 2025
1 parent 0faab1e commit 97eea7c
Show file tree
Hide file tree
Showing 9 changed files with 180 additions and 23 deletions.
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
66 changes: 60 additions & 6 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,6 @@
<rule ref="SlevomatCodingStandard.Classes.RequireSelfReference"/>
<rule ref="SlevomatCodingStandard.Classes.TraitUseDeclaration" />
<rule ref="SlevomatCodingStandard.Classes.TraitUseSpacing" />
<rule ref="SlevomatCodingStandard.Classes.DisallowConstructorPropertyPromotion" />
<rule ref="SlevomatCodingStandard.Commenting.DocCommentSpacing">
<properties>
<property name="linesCountBeforeFirstContent" value="0"/>
Expand Down
21 changes: 21 additions & 0 deletions src/Event/MigrationExecutionFailedEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php declare(strict_types = 1);

namespace ShipMonk\Doctrine\Migration\Event;

use ShipMonk\Doctrine\Migration\Migration;
use ShipMonk\Doctrine\Migration\MigrationPhase;
use Throwable;

class MigrationExecutionFailedEvent
{

public function __construct(
public readonly Migration $migration,
public readonly string $version,
public readonly MigrationPhase $phase,
public readonly Throwable $exception,
)
{
}

}
19 changes: 19 additions & 0 deletions src/Event/MigrationExecutionStartedEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php declare(strict_types = 1);

namespace ShipMonk\Doctrine\Migration\Event;

use ShipMonk\Doctrine\Migration\Migration;
use ShipMonk\Doctrine\Migration\MigrationPhase;

class MigrationExecutionStartedEvent
{

public function __construct(
public readonly Migration $migration,
public readonly string $version,
public readonly MigrationPhase $phase,
)
{
}

}
19 changes: 19 additions & 0 deletions src/Event/MigrationExecutionSucceededEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php declare(strict_types = 1);

namespace ShipMonk\Doctrine\Migration\Event;

use ShipMonk\Doctrine\Migration\Migration;
use ShipMonk\Doctrine\Migration\MigrationPhase;

class MigrationExecutionSucceededEvent
{

public function __construct(
public readonly Migration $migration,
public readonly string $version,
public readonly MigrationPhase $phase,
)
{
}

}
39 changes: 26 additions & 13 deletions src/MigrationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Tools\SchemaTool;
use LogicException;
use Psr\EventDispatcher\EventDispatcherInterface;
use ShipMonk\Doctrine\Migration\Event\MigrationExecutionFailedEvent;
use ShipMonk\Doctrine\Migration\Event\MigrationExecutionStartedEvent;
use ShipMonk\Doctrine\Migration\Event\MigrationExecutionSucceededEvent;
use Throwable;
use function file_get_contents;
use function file_put_contents;
Expand All @@ -33,22 +37,26 @@ class MigrationService

private MigrationVersionProvider $versionProvider;

private MigrationAnalyzer $migrationsAnalyzer;
private MigrationAnalyzer $migrationAnalyzer;

private ?EventDispatcherInterface $eventDispatcher;

public function __construct(
EntityManagerInterface $entityManager,
MigrationConfig $config,
?MigrationExecutor $executor = null,
?MigrationVersionProvider $versionProvider = null,
?MigrationAnalyzer $migrationsAnalyzer = null,
?MigrationAnalyzer $migrationAnalyzer = null,
?EventDispatcherInterface $eventDispatcher = null,
)
{
$this->entityManager = $entityManager;
$this->connection = $entityManager->getConnection();
$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
Expand All @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
24 changes: 22 additions & 2 deletions tests/MigrationServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +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 function array_map;
use function file_get_contents;
use function glob;
Expand All @@ -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();

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -296,6 +314,7 @@ private function createMigrationService(
bool $transactional = false,
?MigrationVersionProvider $versionProvider = null,
?MigrationAnalyzer $statementAnalyzer = null,
?EventDispatcherInterface $eventDispatcher = null,
): MigrationService
{
$migrationsDir = $this->getMigrationsTestDir();
Expand Down Expand Up @@ -329,6 +348,7 @@ private function createMigrationService(
null,
$versionProvider,
$statementAnalyzer,
$eventDispatcher,
);
}

Expand Down

0 comments on commit 97eea7c

Please sign in to comment.