Skip to content

Commit 1e380c6

Browse files
authored
Merge pull request #629 from HypeMC/service-migrations
Add service migrations
2 parents 49ecc56 + a03b6a3 commit 1e380c6

File tree

12 files changed

+444
-42
lines changed

12 files changed

+444
-42
lines changed

config/services.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
use Doctrine\Bundle\MigrationsBundle\EventListener\SchemaFilterListener;
88
use Doctrine\Bundle\MigrationsBundle\MigrationsFactory\ContainerAwareMigrationFactory;
9+
use Doctrine\Bundle\MigrationsBundle\MigrationsRepository\ServiceMigrationsRepository;
10+
use Doctrine\DBAL\Connection;
911
use Doctrine\Migrations\Configuration\Configuration;
1012
use Doctrine\Migrations\Configuration\Connection\ConnectionRegistryConnection;
1113
use Doctrine\Migrations\Configuration\Connection\ExistingConnection;
@@ -27,6 +29,7 @@
2729
use Doctrine\Migrations\Tools\Console\Command\UpToDateCommand;
2830
use Doctrine\Migrations\Tools\Console\Command\VersionCommand;
2931
use Doctrine\Migrations\Version\MigrationFactory;
32+
use Psr\Log\LoggerInterface;
3033

3134
return static function (ContainerConfigurator $container) {
3235
$container->services()
@@ -64,6 +67,17 @@
6467
service('service_container'),
6568
])
6669

70+
->set('doctrine.migrations.service_migrations_repository', ServiceMigrationsRepository::class)
71+
->args([
72+
abstract_arg('migrations locator'),
73+
])
74+
75+
->set('doctrine.migrations.connection', Connection::class)
76+
->factory([service('doctrine.migrations.dependency_factory'), 'getConnection'])
77+
78+
->set('doctrine.migrations.logger', LoggerInterface::class)
79+
->factory([service('doctrine.migrations.dependency_factory'), 'getLogger'])
80+
6781
->set('doctrine_migrations.diff_command', DiffCommand::class)
6882
->args([
6983
service('doctrine.migrations.dependency_factory'),

docs/index.rst

Lines changed: 55 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,12 @@ application:
4242
# config/packages/doctrine_migrations.yaml
4343
4444
doctrine_migrations:
45+
# Whether to enable fetching migrations from the service container.
46+
enable_service_migrations: false
47+
4548
# List of namespace/path pairs to search for migrations, at least one required
4649
migrations_paths:
47-
'App\Migrations': '%kernel.project_dir%/src/App'
50+
'App\Migrations': '%kernel.project_dir%/src/Migrations'
4851
'AnotherApp\Migrations': '/path/to/other/migrations'
4952
'SomeBundle\Migrations': '@SomeBundle/Migrations'
5053
@@ -234,11 +237,12 @@ Doctrine will then assume that this migration has already been run and will igno
234237
Migration Dependencies
235238
----------------------
236239

237-
Migrations can have dependencies on external services (such as geolocation, mailer, data processing services...) that
238-
can be used to have more powerful migrations. Those dependencies are not automatically injected into your migrations
239-
but need to be injected using custom migrations factories.
240+
Migrations can have dependencies on external services (such as geolocation, mailer or data processing services) to
241+
enable more advanced behavior. To inject dependencies into your migrations, you must enable loading migrations from
242+
the service container and register the migrations as services.
240243

241-
Here is an example on how to inject the service container into your migrations:
244+
If you are using Symfony's default service configuration, migration services are registered automatically
245+
once placed in the ``src`` directory:
242246

243247
.. configuration-block::
244248

@@ -247,62 +251,71 @@ Here is an example on how to inject the service container into your migrations:
247251
# config/packages/doctrine_migrations.yaml
248252
249253
doctrine_migrations:
250-
services:
251-
'Doctrine\Migrations\Version\MigrationFactory': 'App\Migrations\Factory\MigrationFactoryDecorator'
254+
enable_service_migrations: true
255+
migrations_paths:
256+
'App\Migrations': '%kernel.project_dir%/src/Migrations'
257+
258+
259+
If you are not using the default configuration, register your migration classes manually and make sure they are
260+
discoverable by the autoloader. If autoconfiguration is disabled, tag them manually with
261+
the ``doctrine_migrations.migration`` tag:
262+
263+
.. configuration-block::
264+
265+
.. code-block:: yaml
252266
253267
# config/services.yaml
254268
255269
services:
256-
App\Migrations\Factory\MigrationFactoryDecorator:
257-
decorates: 'doctrine.migrations.migrations_factory'
258-
arguments: ['@.inner', '@service_container']
270+
DoctrineMigrations\Version20180605025653:
271+
tags: [ 'doctrine_migrations.migration' ]
272+
arguments:
273+
$myService: '@App\Services\MyService'
259274
260275
261-
.. code-block:: php
276+
The connection and logger services are injected automatically through bindings. You may specify them explicitly
277+
if needed:
262278

263-
declare(strict_types=1);
279+
.. configuration-block::
264280

265-
namespace App\Migrations\Factory;
281+
.. code-block:: yaml
266282
267-
use Doctrine\Migrations\AbstractMigration;
268-
use Doctrine\Migrations\Version\MigrationFactory;
269-
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
270-
use Symfony\Component\DependencyInjection\ContainerInterface;
283+
# config/services.yaml
271284
272-
class MigrationFactoryDecorator implements MigrationFactory
273-
{
274-
private $migrationFactory;
275-
private $container;
285+
services:
286+
DoctrineMigrations\Version20180605025653:
287+
tags: [ 'doctrine_migrations.migration' ]
288+
arguments:
289+
- '@doctrine.migrations.connection'
290+
- '@doctrine.migrations.logger'
291+
- '@App\Services\MyService'
276292
277-
public function __construct(MigrationFactory $migrationFactory, ContainerInterface $container)
278-
{
279-
$this->migrationFactory = $migrationFactory;
280-
$this->container = $container;
281-
}
282293
283-
public function createVersion(string $migrationClassName): AbstractMigration
284-
{
285-
$instance = $this->migrationFactory->createVersion($migrationClassName);
294+
Then override the constructor in your migration class and add your dependencies:
286295

287-
if ($instance instanceof ContainerAwareInterface) {
288-
$instance->setContainer($this->container);
289-
}
296+
.. code-block:: php
290297
291-
return $instance;
292-
}
293-
}
298+
declare(strict_types=1);
294299
300+
namespace App\Migrations;
295301
296-
.. tip::
302+
use App\Services\MyService;
303+
use Doctrine\DBAL\Schema\Schema;
304+
use Doctrine\Migrations\AbstractMigration;
305+
306+
final class Version20180605025653 extends AbstractMigration
307+
{
308+
private MyService $myService;
297309
298-
If your migration class implements the interface ``Symfony\Component\DependencyInjection\ContainerAwareInterface``
299-
this bundle will automatically inject the default symfony container into your migration class
300-
(this because the ``MigrationFactoryDecorator`` shown in this example is the default migration factory used by this bundle).
310+
public function __construct(Connection $connection, LoggerInterface $logger, MyService $myService)
311+
{
312+
parent::__construct($connection, $logger);
301313
302-
.. caution::
314+
$this->myService = $myService;
315+
}
303316
304-
The interface ``Symfony\Component\DependencyInjection\ContainerAwareInterface`` has been deprecated in Symfony 6.4 and
305-
removed in 7.0. If you use this version or newer, there is currently no way to inject the service container into migrations.
317+
// ...
318+
}
306319
307320
308321
Generating Migrations Automatically
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Bundle\MigrationsBundle\DependencyInjection\CompilerPass;
6+
7+
use Doctrine\DBAL\Connection;
8+
use Psr\Log\LoggerInterface;
9+
use Symfony\Component\DependencyInjection\Argument\BoundArgument;
10+
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
11+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
12+
use Symfony\Component\DependencyInjection\ContainerBuilder;
13+
use Symfony\Component\DependencyInjection\Reference;
14+
use Symfony\Component\DependencyInjection\TypedReference;
15+
16+
/** @internal */
17+
final class RegisterMigrationsPass implements CompilerPassInterface
18+
{
19+
public function process(ContainerBuilder $container): void
20+
{
21+
if (! $container->hasDefinition('doctrine.migrations.service_migrations_repository')) {
22+
return;
23+
}
24+
25+
$migrationRefs = [];
26+
27+
foreach ($container->findTaggedServiceIds('doctrine_migrations.migration', true) as $id => $attributes) {
28+
$definition = $container->getDefinition($id);
29+
$definition->setBindings([
30+
Connection::class => new BoundArgument(new Reference('doctrine.migrations.connection'), false),
31+
LoggerInterface::class => new BoundArgument(new Reference('doctrine.migrations.logger'), false),
32+
]);
33+
34+
$migrationRefs[$id] = new TypedReference($id, $definition->getClass());
35+
}
36+
37+
$container->getDefinition('doctrine.migrations.service_migrations_repository')
38+
->replaceArgument(0, new ServiceLocatorArgument($migrationRefs));
39+
}
40+
}

src/DependencyInjection/Configuration.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ public function getConfigTreeBuilder(): TreeBuilder
3434
->fixXmlConfig('migration', 'migrations')
3535
->fixXmlConfig('migrations_path', 'migrations_paths')
3636
->children()
37+
->booleanNode('enable_service_migrations')
38+
->info('Whether to enable fetching migrations from the service container.')
39+
->defaultFalse()
40+
->end()
41+
3742
->arrayNode('migrations_paths')
3843
->info('A list of namespace/path pairs where to look for migrations.')
3944
->defaultValue([])

src/DependencyInjection/DoctrineMigrationsExtension.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66

77
use Doctrine\Bundle\MigrationsBundle\Collector\MigrationsCollector;
88
use Doctrine\Bundle\MigrationsBundle\Collector\MigrationsFlattener;
9+
use Doctrine\Migrations\AbstractMigration;
910
use Doctrine\Migrations\Metadata\Storage\MetadataStorage;
1011
use Doctrine\Migrations\Metadata\Storage\TableMetadataStorageConfiguration;
12+
use Doctrine\Migrations\MigrationsRepository;
1113
use Doctrine\Migrations\Version\MigrationFactory;
1214
use InvalidArgumentException;
1315
use RuntimeException;
@@ -49,6 +51,19 @@ public function load(array $configs, ContainerBuilder $container): void
4951

5052
$loader->load('services.php');
5153

54+
if ($config['enable_service_migrations']) {
55+
$container->registerForAutoconfiguration(AbstractMigration::class)
56+
->addTag('doctrine_migrations.migration');
57+
58+
if (! isset($config['services'][MigrationsRepository::class])) {
59+
$config['services'][MigrationsRepository::class] = 'doctrine.migrations.service_migrations_repository';
60+
}
61+
} else {
62+
$container->removeDefinition('doctrine.migrations.service_migrations_repository');
63+
$container->removeDefinition('doctrine.migrations.connection');
64+
$container->removeDefinition('doctrine.migrations.logger');
65+
}
66+
5267
$configurationDefinition = $container->getDefinition('doctrine.migrations.configuration');
5368

5469
foreach ($config['migrations_paths'] as $ns => $path) {

src/DoctrineMigrationsBundle.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Doctrine\Bundle\MigrationsBundle;
66

77
use Doctrine\Bundle\MigrationsBundle\DependencyInjection\CompilerPass\ConfigureDependencyFactoryPass;
8+
use Doctrine\Bundle\MigrationsBundle\DependencyInjection\CompilerPass\RegisterMigrationsPass;
89
use Symfony\Component\DependencyInjection\ContainerBuilder;
910
use Symfony\Component\HttpKernel\Bundle\Bundle;
1011

@@ -16,6 +17,7 @@ class DoctrineMigrationsBundle extends Bundle
1617
public function build(ContainerBuilder $container): void
1718
{
1819
$container->addCompilerPass(new ConfigureDependencyFactoryPass());
20+
$container->addCompilerPass(new RegisterMigrationsPass());
1921
}
2022

2123
public function getPath(): string
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Bundle\MigrationsBundle\MigrationsRepository;
6+
7+
use Doctrine\Migrations\AbstractMigration;
8+
use Doctrine\Migrations\Exception\MigrationClassNotFound;
9+
use Doctrine\Migrations\Metadata\AvailableMigration;
10+
use Doctrine\Migrations\Metadata\AvailableMigrationsSet;
11+
use Doctrine\Migrations\MigrationsRepository;
12+
use Doctrine\Migrations\Version\Version;
13+
use Symfony\Contracts\Service\ServiceProviderInterface;
14+
15+
/** @internal */
16+
final class ServiceMigrationsRepository implements MigrationsRepository
17+
{
18+
/** @var ServiceProviderInterface<AbstractMigration> */
19+
private $container;
20+
21+
/** @var array<string, AvailableMigration> */
22+
private $migrations = [];
23+
24+
/** @param ServiceProviderInterface<AbstractMigration> $container */
25+
public function __construct(ServiceProviderInterface $container)
26+
{
27+
$this->container = $container;
28+
}
29+
30+
public function hasMigration(string $version): bool
31+
{
32+
return isset($this->migrations[$version]) || $this->container->has($version);
33+
}
34+
35+
public function getMigration(Version $version): AvailableMigration
36+
{
37+
$this->loadMigrationFromContainer($version);
38+
39+
return $this->migrations[(string) $version];
40+
}
41+
42+
/**
43+
* Returns a non-sorted set of migrations.
44+
*/
45+
public function getMigrations(): AvailableMigrationsSet
46+
{
47+
foreach ($this->container->getProvidedServices() as $id) {
48+
$this->loadMigrationFromContainer(new Version($id));
49+
}
50+
51+
return new AvailableMigrationsSet($this->migrations);
52+
}
53+
54+
private function loadMigrationFromContainer(Version $version): void
55+
{
56+
$id = (string) $version;
57+
58+
if (isset($this->migrations[$id])) {
59+
return;
60+
}
61+
62+
if (! $this->container->has($id)) {
63+
throw MigrationClassNotFound::new($id);
64+
}
65+
66+
$this->migrations[$id] = new AvailableMigration($version, $this->container->get($id));
67+
}
68+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Bundle\MigrationsBundle\Tests\DependencyInjection\CompilerPass;
6+
7+
use Doctrine\Bundle\MigrationsBundle\DependencyInjection\CompilerPass\RegisterMigrationsPass;
8+
use Doctrine\Bundle\MigrationsBundle\MigrationsRepository\ServiceMigrationsRepository;
9+
use Doctrine\Bundle\MigrationsBundle\Tests\Fixtures\Migrations\Migration001;
10+
use Doctrine\DBAL\Connection;
11+
use PHPUnit\Framework\TestCase;
12+
use Psr\Log\LoggerInterface;
13+
use Symfony\Component\DependencyInjection\Argument\AbstractArgument;
14+
use Symfony\Component\DependencyInjection\Argument\BoundArgument;
15+
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
16+
use Symfony\Component\DependencyInjection\ContainerBuilder;
17+
use Symfony\Component\DependencyInjection\Reference;
18+
use Symfony\Component\DependencyInjection\TypedReference;
19+
20+
class RegisterMigrationsPassTest extends TestCase
21+
{
22+
public function testProcessWhenServiceMigrationsRepositoryIsRegistered(): void
23+
{
24+
$container = new ContainerBuilder();
25+
$container->register('doctrine.migrations.service_migrations_repository', ServiceMigrationsRepository::class)
26+
->addArgument(new AbstractArgument());
27+
$container->register(Migration001::class, Migration001::class)->addTag('doctrine_migrations.migration');
28+
29+
$pass = new RegisterMigrationsPass();
30+
$pass->process($container);
31+
32+
$argument = $container->getDefinition('doctrine.migrations.service_migrations_repository')->getArgument(0);
33+
self::assertEquals(
34+
new ServiceLocatorArgument([Migration001::class => new TypedReference(Migration001::class, Migration001::class)]),
35+
$argument
36+
);
37+
38+
self::assertEquals([
39+
Connection::class => new BoundArgument(new Reference('doctrine.migrations.connection'), false),
40+
LoggerInterface::class => new BoundArgument(new Reference('doctrine.migrations.logger'), false),
41+
], $container->getDefinition(Migration001::class)->getBindings());
42+
}
43+
44+
public function testProcessWhenServiceMigrationsRepositoryIsNotRegistered(): void
45+
{
46+
$container = new ContainerBuilder();
47+
48+
$pass = new RegisterMigrationsPass();
49+
$pass->process($container);
50+
51+
self::assertFalse($container->hasDefinition('doctrine.migrations.service_migrations_repository'));
52+
}
53+
}

0 commit comments

Comments
 (0)