Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

use Doctrine\Bundle\MigrationsBundle\EventListener\SchemaFilterListener;
use Doctrine\Bundle\MigrationsBundle\MigrationsFactory\ContainerAwareMigrationFactory;
use Doctrine\Bundle\MigrationsBundle\MigrationsRepository\ServiceMigrationsRepository;
use Doctrine\DBAL\Connection;
use Doctrine\Migrations\Configuration\Configuration;
use Doctrine\Migrations\Configuration\Connection\ConnectionRegistryConnection;
use Doctrine\Migrations\Configuration\Connection\ExistingConnection;
Expand All @@ -27,6 +29,7 @@
use Doctrine\Migrations\Tools\Console\Command\UpToDateCommand;
use Doctrine\Migrations\Tools\Console\Command\VersionCommand;
use Doctrine\Migrations\Version\MigrationFactory;
use Psr\Log\LoggerInterface;

return static function (ContainerConfigurator $container) {
$container->services()
Expand Down Expand Up @@ -64,6 +67,17 @@
service('service_container'),
])

->set('doctrine.migrations.service_migrations_repository', ServiceMigrationsRepository::class)
->args([
abstract_arg('migrations locator'),
])

->set('doctrine.migrations.connection', Connection::class)
->factory([service('doctrine.migrations.dependency_factory'), 'getConnection'])

->set('doctrine.migrations.logger', LoggerInterface::class)
->factory([service('doctrine.migrations.dependency_factory'), 'getLogger'])

->set('doctrine_migrations.diff_command', DiffCommand::class)
->args([
service('doctrine.migrations.dependency_factory'),
Expand Down
97 changes: 55 additions & 42 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,12 @@ application:
# config/packages/doctrine_migrations.yaml

doctrine_migrations:
# Whether to enable fetching migrations from the service container.
enable_service_migrations: false

# List of namespace/path pairs to search for migrations, at least one required
migrations_paths:
'App\Migrations': '%kernel.project_dir%/src/App'
'App\Migrations': '%kernel.project_dir%/src/Migrations'
'AnotherApp\Migrations': '/path/to/other/migrations'
'SomeBundle\Migrations': '@SomeBundle/Migrations'

Expand Down Expand Up @@ -234,11 +237,12 @@ Doctrine will then assume that this migration has already been run and will igno
Migration Dependencies
----------------------

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

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

.. configuration-block::

Expand All @@ -247,62 +251,71 @@ Here is an example on how to inject the service container into your migrations:
# config/packages/doctrine_migrations.yaml

doctrine_migrations:
services:
'Doctrine\Migrations\Version\MigrationFactory': 'App\Migrations\Factory\MigrationFactoryDecorator'
enable_service_migrations: true
migrations_paths:
'App\Migrations': '%kernel.project_dir%/src/Migrations'


If you are not using the default configuration, register your migration classes manually and make sure they are
discoverable by the autoloader. If autoconfiguration is disabled, tag them manually with
the ``doctrine_migrations.migration`` tag:

.. configuration-block::

.. code-block:: yaml

# config/services.yaml

services:
App\Migrations\Factory\MigrationFactoryDecorator:
decorates: 'doctrine.migrations.migrations_factory'
arguments: ['@.inner', '@service_container']
DoctrineMigrations\Version20180605025653:
tags: [ 'doctrine_migrations.migration' ]
arguments:
$myService: '@App\Services\MyService'


.. code-block:: php
The connection and logger services are injected automatically through bindings. You may specify them explicitly
if needed:

declare(strict_types=1);
.. configuration-block::

namespace App\Migrations\Factory;
.. code-block:: yaml

use Doctrine\Migrations\AbstractMigration;
use Doctrine\Migrations\Version\MigrationFactory;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
# config/services.yaml

class MigrationFactoryDecorator implements MigrationFactory
{
private $migrationFactory;
private $container;
services:
DoctrineMigrations\Version20180605025653:
tags: [ 'doctrine_migrations.migration' ]
arguments:
- '@doctrine.migrations.connection'
- '@doctrine.migrations.logger'
- '@App\Services\MyService'

public function __construct(MigrationFactory $migrationFactory, ContainerInterface $container)
{
$this->migrationFactory = $migrationFactory;
$this->container = $container;
}

public function createVersion(string $migrationClassName): AbstractMigration
{
$instance = $this->migrationFactory->createVersion($migrationClassName);
Then override the constructor in your migration class and add your dependencies:

if ($instance instanceof ContainerAwareInterface) {
$instance->setContainer($this->container);
}
.. code-block:: php

return $instance;
}
}
declare(strict_types=1);

namespace App\Migrations;

.. tip::
use App\Services\MyService;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

final class Version20180605025653 extends AbstractMigration
{
private MyService $myService;

If your migration class implements the interface ``Symfony\Component\DependencyInjection\ContainerAwareInterface``
this bundle will automatically inject the default symfony container into your migration class
(this because the ``MigrationFactoryDecorator`` shown in this example is the default migration factory used by this bundle).
public function __construct(Connection $connection, LoggerInterface $logger, MyService $myService)
{
parent::__construct($connection, $logger);

.. caution::
$this->myService = $myService;
}

The interface ``Symfony\Component\DependencyInjection\ContainerAwareInterface`` has been deprecated in Symfony 6.4 and
removed in 7.0. If you use this version or newer, there is currently no way to inject the service container into migrations.
// ...
}


Generating Migrations Automatically
Expand Down
40 changes: 40 additions & 0 deletions src/DependencyInjection/CompilerPass/RegisterMigrationsPass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

namespace Doctrine\Bundle\MigrationsBundle\DependencyInjection\CompilerPass;

use Doctrine\DBAL\Connection;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Argument\BoundArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\TypedReference;

/** @internal */
final class RegisterMigrationsPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
if (! $container->hasDefinition('doctrine.migrations.service_migrations_repository')) {
return;
}

$migrationRefs = [];

foreach ($container->findTaggedServiceIds('doctrine_migrations.migration', true) as $id => $attributes) {
$definition = $container->getDefinition($id);
$definition->setBindings([
Connection::class => new BoundArgument(new Reference('doctrine.migrations.connection'), false),
LoggerInterface::class => new BoundArgument(new Reference('doctrine.migrations.logger'), false),
]);

$migrationRefs[$id] = new TypedReference($id, $definition->getClass());
}

$container->getDefinition('doctrine.migrations.service_migrations_repository')
->replaceArgument(0, new ServiceLocatorArgument($migrationRefs));
}
}
5 changes: 5 additions & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ public function getConfigTreeBuilder(): TreeBuilder
->fixXmlConfig('migration', 'migrations')
->fixXmlConfig('migrations_path', 'migrations_paths')
->children()
->booleanNode('enable_service_migrations')
->info('Whether to enable fetching migrations from the service container.')
->defaultFalse()
->end()

->arrayNode('migrations_paths')
->info('A list of namespace/path pairs where to look for migrations.')
->defaultValue([])
Expand Down
15 changes: 15 additions & 0 deletions src/DependencyInjection/DoctrineMigrationsExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@

use Doctrine\Bundle\MigrationsBundle\Collector\MigrationsCollector;
use Doctrine\Bundle\MigrationsBundle\Collector\MigrationsFlattener;
use Doctrine\Migrations\AbstractMigration;
use Doctrine\Migrations\Metadata\Storage\MetadataStorage;
use Doctrine\Migrations\Metadata\Storage\TableMetadataStorageConfiguration;
use Doctrine\Migrations\MigrationsRepository;
use Doctrine\Migrations\Version\MigrationFactory;
use InvalidArgumentException;
use RuntimeException;
Expand Down Expand Up @@ -49,6 +51,19 @@ public function load(array $configs, ContainerBuilder $container): void

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

if ($config['enable_service_migrations']) {
$container->registerForAutoconfiguration(AbstractMigration::class)
->addTag('doctrine_migrations.migration');

if (! isset($config['services'][MigrationsRepository::class])) {
$config['services'][MigrationsRepository::class] = 'doctrine.migrations.service_migrations_repository';
}
} else {
$container->removeDefinition('doctrine.migrations.service_migrations_repository');
$container->removeDefinition('doctrine.migrations.connection');
$container->removeDefinition('doctrine.migrations.logger');
}

$configurationDefinition = $container->getDefinition('doctrine.migrations.configuration');

foreach ($config['migrations_paths'] as $ns => $path) {
Expand Down
2 changes: 2 additions & 0 deletions src/DoctrineMigrationsBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Doctrine\Bundle\MigrationsBundle;

use Doctrine\Bundle\MigrationsBundle\DependencyInjection\CompilerPass\ConfigureDependencyFactoryPass;
use Doctrine\Bundle\MigrationsBundle\DependencyInjection\CompilerPass\RegisterMigrationsPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;

Expand All @@ -16,6 +17,7 @@ class DoctrineMigrationsBundle extends Bundle
public function build(ContainerBuilder $container): void
{
$container->addCompilerPass(new ConfigureDependencyFactoryPass());
$container->addCompilerPass(new RegisterMigrationsPass());
}

public function getPath(): string
Expand Down
68 changes: 68 additions & 0 deletions src/MigrationsRepository/ServiceMigrationsRepository.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

declare(strict_types=1);

namespace Doctrine\Bundle\MigrationsBundle\MigrationsRepository;

use Doctrine\Migrations\AbstractMigration;
use Doctrine\Migrations\Exception\MigrationClassNotFound;
use Doctrine\Migrations\Metadata\AvailableMigration;
use Doctrine\Migrations\Metadata\AvailableMigrationsSet;
use Doctrine\Migrations\MigrationsRepository;
use Doctrine\Migrations\Version\Version;
use Symfony\Contracts\Service\ServiceProviderInterface;

/** @internal */
final class ServiceMigrationsRepository implements MigrationsRepository
{
/** @var ServiceProviderInterface<AbstractMigration> */
private $container;

/** @var array<string, AvailableMigration> */
private $migrations = [];

/** @param ServiceProviderInterface<AbstractMigration> $container */
public function __construct(ServiceProviderInterface $container)
{
$this->container = $container;
}

public function hasMigration(string $version): bool
{
return isset($this->migrations[$version]) || $this->container->has($version);
}

public function getMigration(Version $version): AvailableMigration
{
$this->loadMigrationFromContainer($version);

return $this->migrations[(string) $version];
}

/**
* Returns a non-sorted set of migrations.
*/
public function getMigrations(): AvailableMigrationsSet
{
foreach ($this->container->getProvidedServices() as $id) {
$this->loadMigrationFromContainer(new Version($id));
}

return new AvailableMigrationsSet($this->migrations);
}

private function loadMigrationFromContainer(Version $version): void
{
$id = (string) $version;

if (isset($this->migrations[$id])) {
return;
}

if (! $this->container->has($id)) {
throw MigrationClassNotFound::new($id);
}

$this->migrations[$id] = new AvailableMigration($version, $this->container->get($id));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

namespace Doctrine\Bundle\MigrationsBundle\Tests\DependencyInjection\CompilerPass;

use Doctrine\Bundle\MigrationsBundle\DependencyInjection\CompilerPass\RegisterMigrationsPass;
use Doctrine\Bundle\MigrationsBundle\MigrationsRepository\ServiceMigrationsRepository;
use Doctrine\Bundle\MigrationsBundle\Tests\Fixtures\Migrations\Migration001;
use Doctrine\DBAL\Connection;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Argument\AbstractArgument;
use Symfony\Component\DependencyInjection\Argument\BoundArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\TypedReference;

class RegisterMigrationsPassTest extends TestCase
{
public function testProcessWhenServiceMigrationsRepositoryIsRegistered(): void
{
$container = new ContainerBuilder();
$container->register('doctrine.migrations.service_migrations_repository', ServiceMigrationsRepository::class)
->addArgument(new AbstractArgument());
$container->register(Migration001::class, Migration001::class)->addTag('doctrine_migrations.migration');

$pass = new RegisterMigrationsPass();
$pass->process($container);

$argument = $container->getDefinition('doctrine.migrations.service_migrations_repository')->getArgument(0);
self::assertEquals(
new ServiceLocatorArgument([Migration001::class => new TypedReference(Migration001::class, Migration001::class)]),
$argument
);

self::assertEquals([
Connection::class => new BoundArgument(new Reference('doctrine.migrations.connection'), false),
LoggerInterface::class => new BoundArgument(new Reference('doctrine.migrations.logger'), false),
], $container->getDefinition(Migration001::class)->getBindings());
}

public function testProcessWhenServiceMigrationsRepositoryIsNotRegistered(): void
{
$container = new ContainerBuilder();

$pass = new RegisterMigrationsPass();
$pass->process($container);

self::assertFalse($container->hasDefinition('doctrine.migrations.service_migrations_repository'));
}
}
Loading