Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migration dependency injection in Symfony 7.0 #521

Open
TomBrouws opened this issue Dec 14, 2023 · 11 comments
Open

Migration dependency injection in Symfony 7.0 #521

TomBrouws opened this issue Dec 14, 2023 · 11 comments

Comments

@TomBrouws
Copy link

The current documentation (https://symfony.com/bundles/DoctrineMigrationsBundle/current/index.html#migration-dependencies) refers to using ContainerAwareInterface in order to inject the entire container into a migration. In Symfony 6.4 this interface is deprecated, and it is removed in 7.0. (symfony/symfony-docs#18440)

Is there a recommended way to tackle dependency injection for migrations in Symfony 7.0? Are code changes needed in this library in order to replicate the old functionality, or is an update of the docs sufficient?

@derrabus
Copy link
Member

Is there a recommended way to tackle dependency injection for migrations in Symfony 7.0?

No. As you said, the upstream interface is gone. We don't have a replacement (yet?).

Are code changes needed in this library in order to replicate the old functionality,

Yes.

or is an update of the docs sufficient?

The docs change would currently be to remove the mention of it from the docs or at least add a note that the feature is gone when upgrading to Symfony 7.

@derrabus
Copy link
Member

That being said, injecting the whole container is highly discouraged by Symfony. If we were to build a replacement, we should either:

  • Make it possible (not mandatory!) to register migrations as services
  • Inject a service locator instead, e.g. by allowing migrations to be service subscribers.

@TomBrouws
Copy link
Author

TomBrouws commented Dec 14, 2023

Thanks for your reply. Good to know there's no replacement yet.

injecting the whole container is highly discouraged by Symfony

Yes, by 'replicate' I meant in a way that is idiomatic in the newest Symfony version, so your suggestions make sense.

@demcy
Copy link

demcy commented Jan 18, 2024

Please, give an working example of custom migration factories that should be used to inject additional dependencies into migrations.

@derrabus
Copy link
Member

Please, give an working example of custom migration factories that should be used to inject additional dependencies into migrations.

Nobody can give you a working example for a feature that doesn't exist.

@hugo-fasone
Copy link

hugo-fasone commented Apr 21, 2024

I'm somewhat perplexed by the changes since Symfony 7.0. Since the ContainerAwareInterface is no longer available, does this mean there's no way to inject or access services within migrations?

As a newcomer to Symfony, my understanding is that the framework heavily relies on "services" and dependency injection. However, it seems there's no way to utilize these services within migrations since they aren't registered as services. This limitation might be a significant drawback for adopting Symfony 7.0 in my scenario.

I've noticed that this issue hasn't generated much discussion or feedback, which surprises me given its potential impact.

Could anyone clarify how we're supposed to handle this in Symfony 7? What are the suggested workarounds?

@derrabus
Copy link
Member

Since the ContainerAwareInterface is no longer available, does this mean there's no way to inject or access services within migrations?

That is correct.

Could anyone clarify how we're supposed to handle this in Symfony 7?

You cannot.

What are the suggested workarounds?

There are none.

This bundle does not support loading services into migrations until someone builds that feature.

@ninsuo
Copy link

ninsuo commented Jun 22, 2024

Hi peeps,

I've met this problem today and there's a working solution, by creating a custom migration factory.

In my case, I wanted to inject doctrine in order to migrate a table from a database from another, but you may obviously adapt this to your own requirements.

config/packages/doctrine_migrations.yaml

doctrine_migrations:
    services:
        Doctrine\Migrations\Version\MigrationFactory: 'App\Doctrine\Migrations\DoctrineAwareMigrationFactory'

src/Contract/DoctrineAwareMigrationInterface.php

<?php

namespace App\Contract;

use Doctrine\Persistence\ManagerRegistry;

interface DoctrineAwareMigrationInterface
{
    public function setDoctrine(ManagerRegistry $doctrine): void;
}

src/Doctrine/Migrations/DoctrineAwareMigrationFactory.php

<?php

namespace App\Doctrine\Migrations;

use Doctrine\DBAL\Connection;
use Doctrine\Migrations\AbstractMigration;
use Doctrine\Migrations\Version\MigrationFactory;
use Doctrine\Persistence\ManagerRegistry;
use App\Contract\DoctrineAwareMigrationInterface;
use Psr\Log\LoggerInterface;

class DoctrineAwareMigrationFactory implements MigrationFactory
{
    private Connection $connection;
    private LoggerInterface $logger;
    private ManagerRegistry $doctrine;

    public function __construct(Connection $connection, LoggerInterface $logger, ManagerRegistry $doctrine)
    {
        $this->connection = $connection;
        $this->logger = $logger;
        $this->doctrine = $doctrine;
    }

    public function createVersion(string $migrationClassName): AbstractMigration
    {
        $migration = new $migrationClassName(
            $this->connection,
            $this->logger
        );

        if ($migration instanceof DoctrineAwareMigrationInterface) {
            $migration->setDoctrine($this->doctrine);
        }

        return $migration;
    }
}

Now, you can implement the DoctrineAwareMigrationInterface and inject the service.

migrations/Version20240622142931.php

<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
use Doctrine\Persistence\ManagerRegistry;
use App\Contract\DoctrineAwareMigrationInterface;

/**
 * Auto-generated Migration: Please modify to your needs!
 */
final class Version20240622142931 extends AbstractMigration implements DoctrineAwareMigrationInterface
{
    private ManagerRegistry $doctrine;

    public function setDoctrine(ManagerRegistry $doctrine): void
    {
        $this->doctrine = $doctrine;
    }

    public function getDescription(): string
    {
        return '';
    }

    public function up(Schema $schema): void
    {
         // Enjoy $this->doctrine :-)
    }

    public function down(Schema $schema): void
    {
    }

    public function isTransactional(): bool
    {
        return false;
    }
}

@Davidshb
Copy link

Hi @ninsuo,

Instead of importing the Connection and Logger in order to rewrite the class, I suggest you to use the decorator pattern with the attribute AsDecorator :

you will find the symfony doc here.

<?php

namespace App\Migrations\Factory;

use App\Contract\EntityManagerAwareInterface;
use App\Contract\TranslationServiceAwareInterface;
use App\Service\Helper\TranslationService;
use Doctrine\Migrations\AbstractMigration;
use Doctrine\Migrations\Version\MigrationFactory;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;

#[AsDecorator('doctrine.migrations.migrations_factory')]
readonly class MigrationFactoryDecorator implements MigrationFactory
{

    public function __construct(
        private MigrationFactory $migrationFactory,
        private EntityManagerInterface $entityManager,
        private TranslationService $translationService,
    ) {
    }

    public function createVersion(string $migrationClassName): AbstractMigration
    {
        $instance = $this->migrationFactory->createVersion($migrationClassName);

        if ($instance instanceof EntityManagerAwareInterface) {
            $instance->setEntityManager($this->entityManager);
        }

        if ($instance instanceof TranslationServiceAwareInterface) {
            $instance->setTranslationService($this->translationService);
        }

        return $instance;
    }
}

@krossekrabbe

This comment has been minimized.

@d-ph
Copy link

d-ph commented Sep 24, 2024

@krossekrabbe (and other time-conscious devs)

Just inject all necessary services to the decorator using #[AutowireLocator], and inject the locator to (potentially all) migration versions. No one is going to pay you for spending time defining Aware interfaces for something that must surely be a temporary situation (i.e. the lack of support of symfony 7).

https://symfony.com/doc/current/service_container/service_subscribers_locators.html#the-autowirelocator-and-autowireiterator-attributes

// untested "proof-of-concept"

    public function __construct(
        private MigrationFactory $migrationFactory,
        #[AutowireLocator([
            EntityManagerInterface::class,
            TranslationService::class,
            MyYetAnotherService::class,
        ])]
        private ContainerInterface $locator,
    ) {
    }

    public function createVersion(string $migrationClassName): AbstractMigration
    {
        $instance = $this->migrationFactory->createVersion($migrationClassName);
        $instance->setMiniServiceLocator($this->locator);

        return $instance;
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants