Skip to content

Commit

Permalink
[feature] Added support for doctrine migrations 3 (#255)
Browse files Browse the repository at this point in the history
  • Loading branch information
edefimov committed Jan 26, 2021
1 parent c2b0321 commit 2a92e23
Show file tree
Hide file tree
Showing 8 changed files with 438 additions and 225 deletions.
15 changes: 13 additions & 2 deletions DependencyInjection/Configuration.php
Expand Up @@ -2,6 +2,7 @@

namespace Liip\MonitorBundle\DependencyInjection;

use Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle;
use Symfony\Component\Config\Definition\BaseNode;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
Expand Down Expand Up @@ -211,15 +212,25 @@ private function createGroupsNode()
})
->end()
->validate()
->always(function ($value) {
if (is_array($value) && !isset($value['configuration_file']) && !class_exists('Doctrine\\Bundle\\MigrationsBundle\\Command\\DoctrineCommand')) {
->ifArray()
->then(static function (array $value): array {
$isError = !isset($value['configuration_file'])
&& !class_exists(DoctrineMigrationsBundle::class);

if ($isError) {
throw new \InvalidArgumentException('You should explicitly define "configuration_file" parameter or install doctrine/doctrine-migrations-bundle to use empty parameter.');
}

return $value;
})
->end()
->end()
->beforeNormalization()
->ifString()
->then(static function (string $value): array {
return ['default' => $value];
})
->end()
->example(
[
'application_migrations' => [
Expand Down
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace Liip\MonitorBundle\DependencyInjection\DoctrineMigrations;

use Symfony\Component\DependencyInjection\ContainerBuilder;

/**
* Class AbstractDoctrineMigrationsLoader.
*/
abstract class AbstractDoctrineMigrationsLoader
{
/**
* Creates migration configuration service definition.
*
* @param ContainerBuilder $container DI Container
* @param string $connectionName Connection name for container service
* @param string|null $filename File name with migration configuration
*/
abstract public function createMigrationConfigurationService(
ContainerBuilder $container,
string $connectionName,
string $serviceId,
string $filename = null
): void;
}
@@ -0,0 +1,61 @@
<?php

declare(strict_types=1);

namespace Liip\MonitorBundle\DependencyInjection\DoctrineMigrations;

use Doctrine\Bundle\MigrationsBundle\Command\DoctrineCommand;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

/**
* Class DoctrineMigrationsLoader.
*/
final class DoctrineMigrationsLoader implements CompilerPassInterface
{
/**
* @var AbstractDoctrineMigrationsLoader
*/
private $loader;

/**
* DoctrineMigrationsLoader constructor.
*/
public function __construct()
{
$this->loader = class_exists(DoctrineCommand::class)
? new V2MigrationsLoader()
: new V3MigrationsLoader();
}

public function process(ContainerBuilder $container)
{
if (!($this->loader instanceof CompilerPassInterface)) {
return;
}

$this->loader->process($container);
}

public function loadMigrationChecks(ContainerBuilder $container, array $migrationChecksConfig, string $groupName): array
{
$services = [];
foreach ($migrationChecksConfig as $key => $config) {
try {
$serviceId = sprintf('liip_monitor.check.doctrine_migrations.configuration.%s.%s', $groupName, $key);
$this->loader->createMigrationConfigurationService(
$container,
$config['connection'],
$serviceId,
$config['configuration_file'] ?? null
);

$services[$key] = $serviceId;
} catch (\Exception $e) {
throw new \RuntimeException(sprintf('Invalid doctrine migration check under "%s.%s": %s', $groupName, $key, $e->getMessage()), $e->getCode(), $e);
}
}

return $services;
}
}
229 changes: 229 additions & 0 deletions DependencyInjection/DoctrineMigrations/V2MigrationsLoader.php
@@ -0,0 +1,229 @@
<?php

declare(strict_types=1);

namespace Liip\MonitorBundle\DependencyInjection\DoctrineMigrations;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver\PDO\SQLite\Driver;
use Doctrine\DBAL\Driver\PDOSqlite\Driver as LegacyDriver;
use Doctrine\Migrations\Configuration\AbstractFileConfiguration;
use Doctrine\Migrations\Configuration\Configuration as DoctrineMigrationConfiguration;
use Liip\MonitorBundle\DoctrineMigrations\Configuration;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;

/**
* Class V2MigrationsLoader.
*/
final class V2MigrationsLoader extends AbstractDoctrineMigrationsLoader implements CompilerPassInterface
{
/**
* Connection object needed for correct migration loading.
*
* @var Connection
*/
private $fakeConnection;

/**
* Tuple (migrationsConfiguration, tempConfiguration) for doctrine migrations check.
*
* @var array
*/
private $migrationConfigurationsServices = [];

public function process(ContainerBuilder $container)
{
foreach ($this->migrationConfigurationsServices as $services) {
[$configurationService, $configuration] = $services;
/** @var Definition $configurationService */
/** @var DoctrineMigrationConfiguration $configuration */
$versions = $this->getPredefinedMigrations($container, $configuration, $this->fakeConnection);
if ($versions) {
$configurationService->addMethodCall('registerMigrations', [$versions]);
}
}
}

public function createMigrationConfigurationService(
ContainerBuilder $container,
string $connectionName,
string $serviceId,
string $filename = null
): void {
if (!$container->has(Configuration::class)) {
$container->register(Configuration::class)
->setAbstract(true)
->setPublic(true)
->setArguments([null])
->addMethodCall('setContainer', [new Reference('service_container')]);
}

$configuration = $this->createTemporaryConfiguration($container, $this->getConnection(), $filename);

$serviceConfiguration = new ChildDefinition(Configuration::class);

$this->migrationConfigurationsServices[] = [$serviceConfiguration, $configuration];

$serviceConfiguration->replaceArgument(
0,
new Reference(sprintf('doctrine.dbal.%s_connection', $connectionName))
);

if ($configuration->getMigrationsNamespace()) {
$serviceConfiguration->addMethodCall(
'setMigrationsNamespace',
[$configuration->getMigrationsNamespace()]
);
}

if ($configuration->getMigrationsTableName()) {
$serviceConfiguration->addMethodCall(
'setMigrationsTableName',
[$configuration->getMigrationsTableName()]
);
}

if ($configuration->getMigrationsColumnName()) {
$serviceConfiguration->addMethodCall(
'setMigrationsColumnName',
[$configuration->getMigrationsColumnName()]
);
}

if ($configuration->getName()) {
$serviceConfiguration->addMethodCall('setName', [$configuration->getName()]);
}

if ($configuration->getMigrationsDirectory()) {
$directory = $configuration->getMigrationsDirectory();
$pathPlaceholders = ['kernel.root_dir', 'kernel.cache_dir', 'kernel.logs_dir'];
foreach ($pathPlaceholders as $parameter) {
$kernelDir = realpath($container->getParameter($parameter));
if (0 === strpos(realpath($directory), $kernelDir)) {
$directory = str_replace($kernelDir, "%{$parameter}%", $directory);
break;
}
}

$serviceConfiguration->addMethodCall(
'setMigrationsDirectory',
[$directory]
);
}

$serviceConfiguration->addMethodCall('configure', []);

if ($configuration->areMigrationsOrganizedByYear()) {
$serviceConfiguration->addMethodCall('setMigrationsAreOrganizedByYear', [true]);
} elseif ($configuration->areMigrationsOrganizedByYearAndMonth()) {
$serviceConfiguration->addMethodCall('setMigrationsAreOrganizedByYearAndMonth', [true]);
}

$container->setDefinition($serviceId, $serviceConfiguration);
}

/**
* Creates in-memory migration configuration for setting up container service.
*
* @param ContainerBuilder $container The container
* @param Connection $connection Fake connection
* @param string $filename Migrations configuration file
*/
private function createTemporaryConfiguration(
ContainerBuilder $container,
Connection $connection,
string $filename = null
): DoctrineMigrationConfiguration {
if (null === $filename) {
// this is configured from migrations bundle
return new DoctrineMigrationConfiguration($connection);
}

// -------
// This part must be in sync with Doctrine\Migrations\Tools\Console\Helper\ConfigurationHelper::loadConfig
$map = [
'xml' => '\XmlConfiguration',
'yaml' => '\YamlConfiguration',
'yml' => '\YamlConfiguration',
'php' => '\ArrayConfiguration',
'json' => '\JsonConfiguration',
];
// --------

$filename = $container->getParameterBag()->resolveValue($filename);
$info = pathinfo($filename);
// check we can support this file type
if (empty($map[$info['extension']])) {
throw new \InvalidArgumentException('Given config file type is not supported');
}

$class = 'Doctrine\Migrations\Configuration';
$class .= $map[$info['extension']];
// -------

/** @var AbstractFileConfiguration $configuration */
$configuration = new $class($connection);
$configuration->load($filename);
$configuration->validate();

return $configuration;
}

private function getConnection(): Connection
{
if (null === $this->fakeConnection) {
if (!class_exists(Connection::class)) {
throw new \InvalidArgumentException(sprintf('Can not configure doctrine migration checks, because of absence of "%s" class', Connection::class));
}

$driver = class_exists(Driver::class)
? new Driver()
: new LegacyDriver();

$this->fakeConnection = new Connection([], $driver);
}

return $this->fakeConnection;
}

/**
* Return key-value array with migration version as key and class as a value defined in config file.
*
* @param ContainerBuilder $container The container
* @param DoctrineMigrationConfiguration $config Current configuration
* @param Connection $connection Fake connections
*
* @return string[]
*/
private function getPredefinedMigrations(ContainerBuilder $container, DoctrineMigrationConfiguration $config, Connection $connection): array
{
$result = [];

$diff = new Configuration($connection);

if ($namespace = $config->getMigrationsNamespace()) {
$diff->setMigrationsNamespace($config->getMigrationsNamespace());
}

if ($dir = $config->getMigrationsDirectory()) {
$diff->setMigrationsDirectory($dir);
}

$diff->setContainer($container);
$diff->configure();

foreach ($config->getMigrations() as $version) {
$result[$version->getVersion()] = get_class($version->getMigration());
}

foreach ($diff->getAvailableVersions() as $version) {
unset($result[$version]);
}

return $result;
}
}

0 comments on commit 2a92e23

Please sign in to comment.