The start of something beautiful

This commit is contained in:
2024-09-11 22:48:07 -06:00
parent 45acea47f3
commit f5997ee5ec
5614 changed files with 630696 additions and 0 deletions
@@ -0,0 +1,115 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\MigrationsBundle\DependencyInjection\CompilerPass;
use Doctrine\Migrations\DependencyFactory;
use InvalidArgumentException;
use RuntimeException;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use function array_keys;
use function assert;
use function count;
use function implode;
use function is_array;
use function is_string;
use function sprintf;
class ConfigureDependencyFactoryPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
if (! $container->has('doctrine')) {
throw new RuntimeException('DoctrineMigrationsBundle requires DoctrineBundle to be enabled.');
}
$diDefinition = $container->getDefinition('doctrine.migrations.dependency_factory');
$preferredConnection = $container->getParameter('doctrine.migrations.preferred_connection');
assert(is_string($preferredConnection) || $preferredConnection === null);
// explicitly use configured connection
if ($preferredConnection !== null) {
$this->validatePreferredConnection($container, $preferredConnection);
$loaderDefinition = $container->getDefinition('doctrine.migrations.connection_registry_loader');
$loaderDefinition->setArgument(1, $preferredConnection);
$diDefinition->setFactory([DependencyFactory::class, 'fromConnection']);
$diDefinition->setArgument(1, new Reference('doctrine.migrations.connection_registry_loader'));
return;
}
$preferredEm = $container->getParameter('doctrine.migrations.preferred_em');
assert(is_string($preferredEm) || $preferredEm === null);
// explicitly use configured entity manager
if ($preferredEm !== null) {
$this->validatePreferredEm($container, $preferredEm);
$loaderDefinition = $container->getDefinition('doctrine.migrations.entity_manager_registry_loader');
$loaderDefinition->setArgument(1, $preferredEm);
$diDefinition->setFactory([DependencyFactory::class, 'fromEntityManager']);
$diDefinition->setArgument(1, new Reference('doctrine.migrations.entity_manager_registry_loader'));
return;
}
// try to use any/default entity manager
if (
$container->hasParameter('doctrine.entity_managers')
&& is_array($container->getParameter('doctrine.entity_managers'))
&& count($container->getParameter('doctrine.entity_managers')) > 0
) {
$diDefinition->setFactory([DependencyFactory::class, 'fromEntityManager']);
$diDefinition->setArgument(1, new Reference('doctrine.migrations.entity_manager_registry_loader'));
return;
}
// fallback on any/default connection
$diDefinition->setFactory([DependencyFactory::class, 'fromConnection']);
$diDefinition->setArgument(1, new Reference('doctrine.migrations.connection_registry_loader'));
}
private function validatePreferredConnection(ContainerBuilder $container, string $preferredConnection): void
{
/** @var array<string, string> $allowedConnections */
$allowedConnections = $container->getParameter('doctrine.connections');
if (! isset($allowedConnections[$preferredConnection])) {
throw new InvalidArgumentException(sprintf(
'The "%s" connection is not defined. Did you mean one of the following: %s',
$preferredConnection,
implode(', ', array_keys($allowedConnections))
));
}
}
private function validatePreferredEm(ContainerBuilder $container, string $preferredEm): void
{
if (
! $container->hasParameter('doctrine.entity_managers')
|| ! is_array($container->getParameter('doctrine.entity_managers'))
|| count($container->getParameter('doctrine.entity_managers')) === 0
) {
throw new InvalidArgumentException(sprintf(
'The "%s" entity manager is not defined. It seems that you do not have configured any entity manager in the DoctrineBundle.',
$preferredEm
));
}
/** @var array<string, string> $allowedEms */
$allowedEms = $container->getParameter('doctrine.entity_managers');
if (! isset($allowedEms[$preferredEm])) {
throw new InvalidArgumentException(sprintf(
'The "%s" entity manager is not defined. Did you mean one of the following: %s',
$preferredEm,
implode(', ', array_keys($allowedEms))
));
}
}
}
@@ -0,0 +1,180 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\MigrationsBundle\DependencyInjection;
use ReflectionClass;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use function array_filter;
use function array_keys;
use function constant;
use function count;
use function in_array;
use function is_string;
use function strlen;
use function strpos;
use function strtoupper;
use function substr;
/**
* DoctrineMigrationsExtension configuration structure.
*/
class Configuration implements ConfigurationInterface
{
/**
* Generates the configuration tree.
*
* @return TreeBuilder The config tree builder
*/
public function getConfigTreeBuilder(): TreeBuilder
{
$treeBuilder = new TreeBuilder('doctrine_migrations');
$rootNode = $treeBuilder->getRootNode();
$organizeMigrationModes = $this->getOrganizeMigrationsModes();
$rootNode
->fixXmlConfig('migration', 'migrations')
->fixXmlConfig('migrations_path', 'migrations_paths')
->children()
->arrayNode('migrations_paths')
->info('A list of namespace/path pairs where to look for migrations.')
->defaultValue([])
->useAttributeAsKey('namespace')
->prototype('scalar')->end()
->end()
->arrayNode('services')
->info('A set of services to pass to the underlying doctrine/migrations library, allowing to change its behaviour.')
->useAttributeAsKey('service')
->defaultValue([])
->validate()
->ifTrue(static function (array $v): bool {
return count(array_filter(array_keys($v), static function (string $doctrineService): bool {
return strpos($doctrineService, 'Doctrine\Migrations\\') !== 0;
})) !== 0;
})
->thenInvalid('Valid services for the DoctrineMigrationsBundle must be in the "Doctrine\Migrations" namespace.')
->end()
->prototype('scalar')->end()
->end()
->arrayNode('factories')
->info('A set of callables to pass to the underlying doctrine/migrations library as services, allowing to change its behaviour.')
->useAttributeAsKey('factory')
->defaultValue([])
->validate()
->ifTrue(static function (array $v): bool {
return count(array_filter(array_keys($v), static function (string $doctrineService): bool {
return strpos($doctrineService, 'Doctrine\Migrations\\') !== 0;
})) !== 0;
})
->thenInvalid('Valid callables for the DoctrineMigrationsBundle must be in the "Doctrine\Migrations" namespace.')
->end()
->prototype('scalar')->end()
->end()
->arrayNode('storage')
->addDefaultsIfNotSet()
->info('Storage to use for migration status metadata.')
->children()
->arrayNode('table_storage')
->addDefaultsIfNotSet()
->info('The default metadata storage, implemented as a table in the database.')
->children()
->scalarNode('table_name')->defaultValue(null)->cannotBeEmpty()->end()
->scalarNode('version_column_name')->defaultValue(null)->end()
->scalarNode('version_column_length')->defaultValue(null)->end()
->scalarNode('executed_at_column_name')->defaultValue(null)->end()
->scalarNode('execution_time_column_name')->defaultValue(null)->end()
->end()
->end()
->end()
->end()
->arrayNode('migrations')
->info('A list of migrations to load in addition to the one discovered via "migrations_paths".')
->prototype('scalar')->end()
->defaultValue([])
->end()
->scalarNode('connection')
->info('Connection name to use for the migrations database.')
->defaultValue(null)
->end()
->scalarNode('em')
->info('Entity manager name to use for the migrations database (available when doctrine/orm is installed).')
->defaultValue(null)
->end()
->scalarNode('all_or_nothing')
->info('Run all migrations in a transaction.')
->defaultValue(false)
->end()
->scalarNode('check_database_platform')
->info('Adds an extra check in the generated migrations to allow execution only on the same platform as they were initially generated on.')
->defaultValue(true)
->end()
->scalarNode('custom_template')
->info('Custom template path for generated migration classes.')
->defaultValue(null)
->end()
->scalarNode('organize_migrations')
->defaultValue(false)
->info('Organize migrations mode. Possible values are: "BY_YEAR", "BY_YEAR_AND_MONTH", false')
->validate()
->ifTrue(static function ($v) use ($organizeMigrationModes): bool {
if ($v === false) {
return false;
}
return ! is_string($v) || ! in_array(strtoupper($v), $organizeMigrationModes, true);
})
->thenInvalid('Invalid organize migrations mode value %s')
->end()
->validate()
->ifString()
->then(static function ($v) {
return constant('Doctrine\Migrations\Configuration\Configuration::VERSIONS_ORGANIZATION_' . strtoupper($v));
})
->end()
->end()
->booleanNode('enable_profiler')
->info('Whether or not to enable the profiler collector to calculate and visualize migration status. This adds some queries overhead.')
->defaultFalse()
->end()
->booleanNode('transactional')
->info('Whether or not to wrap migrations in a single transaction.')
->defaultTrue()
->end()
->end();
return $treeBuilder;
}
/**
* Find organize migrations modes for their names
*
* @return string[]
*/
private function getOrganizeMigrationsModes(): array
{
$constPrefix = 'VERSIONS_ORGANIZATION_';
$prefixLen = strlen($constPrefix);
$refClass = new ReflectionClass('Doctrine\Migrations\Configuration\Configuration');
$constsArray = array_keys($refClass->getConstants());
$namesArray = [];
foreach ($constsArray as $constant) {
if (strpos($constant, $constPrefix) !== 0) {
continue;
}
$namesArray[] = substr($constant, $prefixLen);
}
return $namesArray;
}
}
@@ -0,0 +1,201 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\MigrationsBundle\DependencyInjection;
use Doctrine\Bundle\MigrationsBundle\Collector\MigrationsCollector;
use Doctrine\Bundle\MigrationsBundle\Collector\MigrationsFlattener;
use Doctrine\Migrations\Metadata\Storage\MetadataStorage;
use Doctrine\Migrations\Metadata\Storage\TableMetadataStorageConfiguration;
use Doctrine\Migrations\Version\MigrationFactory;
use InvalidArgumentException;
use RuntimeException;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\DependencyInjection\Reference;
use function array_keys;
use function assert;
use function explode;
use function implode;
use function interface_exists;
use function is_array;
use function sprintf;
use function strlen;
use function substr;
class DoctrineMigrationsExtension extends Extension
{
/**
* Responds to the migrations configuration parameter.
*
* {@inheritDoc}
*/
public function load(array $configs, ContainerBuilder $container): void
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$locator = new FileLocator(__DIR__ . '/../Resources/config/');
$loader = new XmlFileLoader($container, $locator);
$loader->load('services.xml');
$configurationDefinition = $container->getDefinition('doctrine.migrations.configuration');
foreach ($config['migrations_paths'] as $ns => $path) {
$path = $this->checkIfBundleRelativePath($path, $container);
$configurationDefinition->addMethodCall('addMigrationsDirectory', [$ns, $path]);
}
foreach ($config['migrations'] as $migrationClass) {
$configurationDefinition->addMethodCall('addMigrationClass', [$migrationClass]);
}
if ($config['organize_migrations'] !== false) {
$configurationDefinition->addMethodCall('setMigrationOrganization', [$config['organize_migrations']]);
}
if ($config['custom_template'] !== null) {
$configurationDefinition->addMethodCall('setCustomTemplate', [$config['custom_template']]);
}
$configurationDefinition->addMethodCall('setAllOrNothing', [$config['all_or_nothing']]);
$configurationDefinition->addMethodCall('setCheckDatabasePlatform', [$config['check_database_platform']]);
if ($config['enable_profiler']) {
$this->registerCollector($container);
}
$configurationDefinition->addMethodCall('setTransactional', [$config['transactional']]);
$diDefinition = $container->getDefinition('doctrine.migrations.dependency_factory');
if (! isset($config['services'][MigrationFactory::class])) {
$config['services'][MigrationFactory::class] = 'doctrine.migrations.migrations_factory';
}
foreach ($config['services'] as $doctrineId => $symfonyId) {
$diDefinition->addMethodCall('setDefinition', [$doctrineId, new ServiceClosureArgument(new Reference($symfonyId))]);
}
foreach ($config['factories'] as $doctrineId => $symfonyId) {
$diDefinition->addMethodCall('setDefinition', [$doctrineId, new Reference($symfonyId)]);
}
if (! isset($config['services'][MetadataStorage::class])) {
$storageConfiguration = $config['storage']['table_storage'];
$storageDefinition = new Definition(TableMetadataStorageConfiguration::class);
$container->setDefinition('doctrine.migrations.storage.table_storage', $storageDefinition);
$container->setAlias('doctrine.migrations.metadata_storage', 'doctrine.migrations.storage.table_storage');
if ($storageConfiguration['table_name'] !== null) {
$storageDefinition->addMethodCall('setTableName', [$storageConfiguration['table_name']]);
}
if ($storageConfiguration['version_column_name'] !== null) {
$storageDefinition->addMethodCall('setVersionColumnName', [$storageConfiguration['version_column_name']]);
}
if ($storageConfiguration['version_column_length'] !== null) {
$storageDefinition->addMethodCall('setVersionColumnLength', [$storageConfiguration['version_column_length']]);
}
if ($storageConfiguration['executed_at_column_name'] !== null) {
$storageDefinition->addMethodCall('setExecutedAtColumnName', [$storageConfiguration['executed_at_column_name']]);
}
if ($storageConfiguration['execution_time_column_name'] !== null) {
$storageDefinition->addMethodCall('setExecutionTimeColumnName', [$storageConfiguration['execution_time_column_name']]);
}
$configurationDefinition->addMethodCall('setMetadataStorageConfiguration', [new Reference('doctrine.migrations.storage.table_storage')]);
}
if ($config['em'] !== null && $config['connection'] !== null) {
throw new InvalidArgumentException(
'You cannot specify both "connection" and "em" in the DoctrineMigrationsBundle configurations.'
);
}
$container->setParameter('doctrine.migrations.preferred_em', $config['em']);
$container->setParameter('doctrine.migrations.preferred_connection', $config['connection']);
if (interface_exists(ContainerAwareInterface::class)) {
return;
}
$container->removeDefinition('doctrine.migrations.container_aware_migrations_factory');
}
private function checkIfBundleRelativePath(string $path, ContainerBuilder $container): string
{
if (isset($path[0]) && $path[0] === '@') {
$pathParts = explode('/', $path);
$bundleName = substr($pathParts[0], 1);
$bundlePath = $this->getBundlePath($bundleName, $container);
return $bundlePath . substr($path, strlen('@' . $bundleName));
}
return $path;
}
private function getBundlePath(string $bundleName, ContainerBuilder $container): string
{
$bundleMetadata = $container->getParameter('kernel.bundles_metadata');
assert(is_array($bundleMetadata));
if (! isset($bundleMetadata[$bundleName])) {
throw new RuntimeException(sprintf(
'The bundle "%s" has not been registered, available bundles: %s',
$bundleName,
implode(', ', array_keys($bundleMetadata))
));
}
return $bundleMetadata[$bundleName]['path'];
}
private function registerCollector(ContainerBuilder $container): void
{
$flattenerDefinition = new Definition(MigrationsFlattener::class);
$container->setDefinition('doctrine_migrations.migrations_flattener', $flattenerDefinition);
$collectorDefinition = new Definition(MigrationsCollector::class, [
new Reference('doctrine.migrations.dependency_factory'),
new Reference('doctrine_migrations.migrations_flattener'),
]);
$collectorDefinition
->addTag('data_collector', [
'template' => '@DoctrineMigrations/Collector/migrations.html.twig',
'id' => 'doctrine_migrations',
'priority' => '249',
]);
$container->setDefinition('doctrine_migrations.migrations_collector', $collectorDefinition);
}
/**
* Returns the base path for the XSD files.
*
* @return string The XSD base path
*/
public function getXsdValidationBasePath(): string
{
return __DIR__ . '/../Resources/config/schema';
}
public function getNamespace(): string
{
return 'http://symfony.com/schema/dic/doctrine/migrations/3.0';
}
}