This commit is contained in:
Marek
2026-03-24 00:04:55 +01:00
commit c5229e48ed
4225 changed files with 511461 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
branches:
- "3.0.x"
- "3.1.x"
- "3.2.x"
- "3.3.x"
- "3.4.x"
- "3.5.x"
- "3.6.x"
- "4.0.x"
maintained_branches: ["3.5.x", "3.6.x", "4.0.x"]
doc_dir: "docs/"
dev_branch: "4.0.x"

View File

@@ -0,0 +1,19 @@
Copyright (c) 2006-2013 Doctrine Project
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,9 @@
DoctrineMigrationsBundle
========================
This bundle integrates the [Doctrine Migrations library](http://www.doctrine-project.org/projects/migrations.html)
into Symfony applications. Database migrations help you version the changes in
your database schema and apply them in a predictable way on every server running
the application.
[Read the documentation of this bundle](https://symfony.com/doc/current/bundles/DoctrineMigrationsBundle/index.html).

View File

@@ -0,0 +1,94 @@
# Upgrade
## Upgrade to 3.5
## Final classes
Some classes have been marked as `@final` because they are not supposed to be
extended. They will be `final`, and most of them will be marked with
`@internal` in 4.0.0.
## From 2.x to 3.0.0
- The configuration for the migration namespace and directory changed as follows:
Before
```yaml
doctrine_migrations:
dir_name: '%kernel.project_dir%/src/Migrations'
namespace: DoctrineMigrations
```
After
```yaml
doctrine_migrations:
migrations_paths:
'DoctrineMigrations': '%kernel.project_dir%/src/Migrations'
```
- The configuration for the metadata table definition changed as follows:
Before
```yaml
doctrine_migrations:
table_name: 'migration_versions'
column_name: 'version'
column_length: 14
executed_at_column_name: 'executed_at'
```
After
```yaml
doctrine_migrations:
storage:
table_storage:
table_name: 'migration_versions'
version_column_name: 'version'
version_column_length: 191
executed_at_column_name: 'executed_at'
```
If your project did not originally specify its own table definition configuration, you will need to configure the table name after the upgrade:
```yaml
doctrine_migrations:
storage:
table_storage:
table_name: 'migration_versions'
```
and then run the `doctrine:migrations:sync-metadata-storage` command.
- The migration name has been dropped:
Before
```yaml
doctrine_migrations:
name: 'Application Migrations'
```
After
The parameter `name` has been dropped.
- The default for `table_name` changed from `migration_versions` to `doctrine_migration_versions`. If you did not
specify the `table_name` option, you now need to declare it explicitly to not lose migration data.
```yaml
doctrine_migrations:
storage:
table_storage:
table_name: 'migration_versions'
```
### Underlying doctrine/migrations library
Upgrading this bundle to `3.0` will also update the `doctrine/migrations` library to the version `3.0`.
Backward incompatible changes in `doctrine/migrations` 3.0
are documented in the dedicated [UPGRADE](https://github.com/doctrine/migrations/blob/3.0.x/UPGRADE.md) document.
- The container is not automatically injected anymore when a migration implements `ContainerAwareInterface`. Custom
migration factories should be used to inject additional dependencies into migrations.

View File

@@ -0,0 +1,61 @@
{
"name": "doctrine/doctrine-migrations-bundle",
"description": "Symfony DoctrineMigrationsBundle",
"license": "MIT",
"type": "symfony-bundle",
"keywords": [
"DBAL",
"Migrations",
"Schema"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Doctrine Project",
"homepage": "https://www.doctrine-project.org"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"homepage": "https://www.doctrine-project.org",
"require": {
"php": "^7.2 || ^8.0",
"doctrine/doctrine-bundle": "^2.4 || ^3.0",
"doctrine/migrations": "^3.2",
"symfony/deprecation-contracts": "^2.1 || ^3",
"symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0 || ^8.0"
},
"require-dev": {
"composer/semver": "^3.0",
"doctrine/coding-standard": "^12 || ^14",
"doctrine/orm": "^2.6 || ^3",
"phpstan/phpstan": "^1.4 || ^2",
"phpstan/phpstan-deprecation-rules": "^1 || ^2",
"phpstan/phpstan-phpunit": "^1 || ^2",
"phpstan/phpstan-strict-rules": "^1.1 || ^2",
"phpstan/phpstan-symfony": "^1.3 || ^2",
"phpunit/phpunit": "^8.5 || ^9.5",
"symfony/phpunit-bridge": "^6.3 || ^7 || ^8",
"symfony/var-exporter": "^5.4 || ^6 || ^7 || ^8"
},
"autoload": {
"psr-4": {
"Doctrine\\Bundle\\MigrationsBundle\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Doctrine\\Bundle\\MigrationsBundle\\Tests\\": "tests"
}
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
}
}
}

View File

@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8" ?>
<xsd:schema xmlns="http://symfony.com/schema/dic/doctrine/migrations/3.0"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://symfony.com/schema/dic/doctrine/migrations/3.0"
elementFormDefault="qualified"
>
<xsd:element name="config">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="migrations-path" maxOccurs="unbounded">
<xsd:complexType>
<xsd:simpleContent>
<xsd:extension base="xsd:string">
<xsd:attribute name="namespace" type="xsd:string"/>
</xsd:extension>
</xsd:simpleContent>
</xsd:complexType>
</xsd:element>
<xsd:element name="services" maxOccurs="unbounded" minOccurs="0">
<xsd:complexType>
<xsd:simpleContent>
<xsd:extension base="xsd:string">
<xsd:attribute name="service" type="xsd:string"/>
</xsd:extension>
</xsd:simpleContent>
</xsd:complexType>
</xsd:element>
<xsd:element name="migration" type="xsd:string" maxOccurs="unbounded" minOccurs="0"/>
<xsd:element name="storage" minOccurs="0">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="table-storage" minOccurs="0">
<xsd:complexType>
<xsd:attribute name="table-name" type="xsd:string"/>
<xsd:attribute name="version-column-name" type="xsd:string"/>
<xsd:attribute name="version-column-length" type="xsd:positiveInteger"/>
<xsd:attribute name="executed-at-column-name" type="xsd:string"/>
<xsd:attribute name="execution-time-column-name" type="xsd:string"/>
</xsd:complexType>
</xsd:element>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string"/>
<xsd:attribute name="em" type="xsd:string"/>
<xsd:attribute name="connection" type="xsd:string"/>
<xsd:attribute name="sorter" type="xsd:string"/>
<xsd:attribute name="all_or_nothing" type="xsd:boolean"/>
<xsd:attribute name="check_database_platform" type="xsd:boolean"/>
<xsd:attribute name="custom_template" type="xsd:string"/>
<xsd:attribute name="organize-migrations">
<xsd:simpleType>
<xsd:restriction base="xsd:string">
<xsd:enumeration value="BY_YEAR"/>
<xsd:enumeration value="BY_YEAR_AND_MONTH"/>
<xsd:enumeration value="false"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:attribute>
</xsd:complexType>
</xsd:element>
</xsd:schema>

View File

@@ -0,0 +1,175 @@
<?php
declare(strict_types=1);
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
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;
use Doctrine\Migrations\Configuration\EntityManager\ExistingEntityManager;
use Doctrine\Migrations\Configuration\EntityManager\ManagerRegistryEntityManager;
use Doctrine\Migrations\Configuration\Migration\ExistingConfiguration;
use Doctrine\Migrations\DependencyFactory;
use Doctrine\Migrations\Tools\Console\Command\CurrentCommand;
use Doctrine\Migrations\Tools\Console\Command\DiffCommand;
use Doctrine\Migrations\Tools\Console\Command\DumpSchemaCommand;
use Doctrine\Migrations\Tools\Console\Command\ExecuteCommand;
use Doctrine\Migrations\Tools\Console\Command\GenerateCommand;
use Doctrine\Migrations\Tools\Console\Command\LatestCommand;
use Doctrine\Migrations\Tools\Console\Command\ListCommand;
use Doctrine\Migrations\Tools\Console\Command\MigrateCommand;
use Doctrine\Migrations\Tools\Console\Command\RollupCommand;
use Doctrine\Migrations\Tools\Console\Command\StatusCommand;
use Doctrine\Migrations\Tools\Console\Command\SyncMetadataCommand;
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()
->set('doctrine.migrations.dependency_factory', DependencyFactory::class)
->args([
service('doctrine.migrations.configuration_loader'),
abstract_arg('loader service'),
service('logger')->nullOnInvalid(),
])
->set('doctrine.migrations.configuration_loader', ExistingConfiguration::class)
->args([service('doctrine.migrations.configuration')])
->set('doctrine.migrations.connection_loader', ExistingConnection::class)
->set('doctrine.migrations.em_loader', ExistingEntityManager::class)
->set('doctrine.migrations.entity_manager_registry_loader', ManagerRegistryEntityManager::class)
->args([service('doctrine')])
->factory([ManagerRegistryEntityManager::class, 'withSimpleDefault'])
->set('doctrine.migrations.connection_registry_loader', ConnectionRegistryConnection::class)
->args([service('doctrine')])
->factory([ConnectionRegistryConnection::class, 'withSimpleDefault'])
->set('doctrine.migrations.configuration', Configuration::class)
->set('doctrine.migrations.migrations_factory', MigrationFactory::class)
->factory([service('doctrine.migrations.dependency_factory'), 'getMigrationFactory'])
->set('doctrine.migrations.container_aware_migrations_factory', ContainerAwareMigrationFactory::class)
->decorate('doctrine.migrations.migrations_factory')
->args([
service('doctrine.migrations.container_aware_migrations_factory.inner'),
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'),
'doctrine:migrations:diff',
])
->tag('console.command', ['command' => 'doctrine:migrations:diff'])
->set('doctrine_migrations.sync_metadata_command', SyncMetadataCommand::class)
->args([
service('doctrine.migrations.dependency_factory'),
'doctrine:migrations:sync-metadata-storage',
])
->tag('console.command', ['command' => 'doctrine:migrations:sync-metadata-storage'])
->set('doctrine_migrations.versions_command', ListCommand::class)
->args([
service('doctrine.migrations.dependency_factory'),
'doctrine:migrations:versions',
])
->tag('console.command', ['command' => 'doctrine:migrations:list'])
->set('doctrine_migrations.current_command', CurrentCommand::class)
->args([
service('doctrine.migrations.dependency_factory'),
'doctrine:migrations:current',
])
->tag('console.command', ['command' => 'doctrine:migrations:current'])
->set('doctrine_migrations.dump_schema_command', DumpSchemaCommand::class)
->args([
service('doctrine.migrations.dependency_factory'),
'doctrine:migrations:dump-schema',
])
->tag('console.command', ['command' => 'doctrine:migrations:dump-schema'])
->set('doctrine_migrations.execute_command', ExecuteCommand::class)
->args([
service('doctrine.migrations.dependency_factory'),
'doctrine:migrations:execute',
])
->tag('console.command', ['command' => 'doctrine:migrations:execute'])
->set('doctrine_migrations.generate_command', GenerateCommand::class)
->args([
service('doctrine.migrations.dependency_factory'),
'doctrine:migrations:generate',
])
->tag('console.command', ['command' => 'doctrine:migrations:generate'])
->set('doctrine_migrations.latest_command', LatestCommand::class)
->args([
service('doctrine.migrations.dependency_factory'),
'doctrine:migrations:latest',
])
->tag('console.command', ['command' => 'doctrine:migrations:latest'])
->set('doctrine_migrations.migrate_command', MigrateCommand::class)
->args([
service('doctrine.migrations.dependency_factory'),
'doctrine:migrations:migrate',
])
->tag('console.command', ['command' => 'doctrine:migrations:migrate'])
->set('doctrine_migrations.rollup_command', RollupCommand::class)
->args([
service('doctrine.migrations.dependency_factory'),
'doctrine:migrations:rollup',
])
->tag('console.command', ['command' => 'doctrine:migrations:rollup'])
->set('doctrine_migrations.status_command', StatusCommand::class)
->args([
service('doctrine.migrations.dependency_factory'),
'doctrine:migrations:status',
])
->tag('console.command', ['command' => 'doctrine:migrations:status'])
->set('doctrine_migrations.up_to_date_command', UpToDateCommand::class)
->args([
service('doctrine.migrations.dependency_factory'),
'doctrine:migrations:up-to-date',
])
->tag('console.command', ['command' => 'doctrine:migrations:up-to-date'])
->set('doctrine_migrations.version_command', VersionCommand::class)
->args([
service('doctrine.migrations.dependency_factory'),
'doctrine:migrations:version',
])
->tag('console.command', ['command' => 'doctrine:migrations:version'])
->set('doctrine_migrations.schema_filter_listener', SchemaFilterListener::class)
// The "doctrine.dbal.schema_filter" tag is dynamically added for each connection
->tag('kernel.event_listener', ['event' => 'console.command', 'method' => 'onConsoleCommand']);
};

View File

@@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\MigrationsBundle\Collector;
use Doctrine\DBAL\Exception;
use Doctrine\Migrations\DependencyFactory;
use Doctrine\Migrations\Metadata\Storage\TableMetadataStorageConfiguration;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
use Symfony\Component\VarDumper\Cloner\Data;
use Throwable;
use function count;
use function get_class;
/** @final */
class MigrationsCollector extends DataCollector
{
/** @var DependencyFactory */
private $dependencyFactory;
/** @var MigrationsFlattener */
private $flattener;
public function __construct(DependencyFactory $dependencyFactory, MigrationsFlattener $migrationsFlattener)
{
$this->dependencyFactory = $dependencyFactory;
$this->flattener = $migrationsFlattener;
}
/** @return void */
public function collect(Request $request, Response $response, ?Throwable $exception = null)
{
if ($this->data !== []) {
return;
}
$metadataStorage = $this->dependencyFactory->getMetadataStorage();
$planCalculator = $this->dependencyFactory->getMigrationPlanCalculator();
try {
$executedMigrations = $metadataStorage->getExecutedMigrations();
} catch (Exception $dbalException) {
$this->dependencyFactory->getLogger()->error(
'error while trying to collect executed migrations',
['exception' => $dbalException]
);
return;
}
$availableMigrations = $planCalculator->getMigrations();
$this->data['available_migrations_count'] = count($availableMigrations);
$unavailableMigrations = $executedMigrations->unavailableSubset($availableMigrations);
$this->data['unavailable_migrations_count'] = count($unavailableMigrations);
$newMigrations = $availableMigrations->newSubset($executedMigrations);
$this->data['new_migrations'] = $this->flattener->flattenAvailableMigrations($newMigrations);
$this->data['executed_migrations'] = $this->flattener->flattenExecutedMigrations($executedMigrations, $availableMigrations);
$this->data['storage'] = get_class($metadataStorage);
$configuration = $this->dependencyFactory->getConfiguration();
$storage = $configuration->getMetadataStorageConfiguration();
if ($storage instanceof TableMetadataStorageConfiguration) {
$this->data['table'] = $storage->getTableName();
$this->data['column'] = $storage->getVersionColumnName();
}
$connection = $this->dependencyFactory->getConnection();
$this->data['driver'] = get_class($connection->getDriver());
$this->data['name'] = $connection->getDatabase();
$this->data['namespaces'] = $configuration->getMigrationDirectories();
}
/** @return string */
public function getName()
{
return 'doctrine_migrations';
}
/** @return array<string, mixed>|Data */
public function getData()
{
return $this->data;
}
/** @return void */
public function reset()
{
$this->data = [];
}
}

View File

@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\MigrationsBundle\Collector;
use DateTimeImmutable;
use Doctrine\Migrations\Metadata\AvailableMigration;
use Doctrine\Migrations\Metadata\AvailableMigrationsList;
use Doctrine\Migrations\Metadata\ExecutedMigration;
use Doctrine\Migrations\Metadata\ExecutedMigrationsList;
use ReflectionClass;
use function array_map;
/** @final */
class MigrationsFlattener
{
/**
* @return array{
* version: string,
* is_new: true,
* is_unavailable: bool,
* description: string,
* executed_at: null,
* execution_time: null,
* file: string|false,
* }[]
*/
public function flattenAvailableMigrations(AvailableMigrationsList $migrationsList): array
{
return array_map(static function (AvailableMigration $migration) {
return [
'version' => (string) $migration->getVersion(),
'is_new' => true,
'is_unavailable' => false,
'description' => $migration->getMigration()->getDescription(),
'executed_at' => null,
'execution_time' => null,
'file' => (new ReflectionClass($migration->getMigration()))->getFileName(),
];
}, $migrationsList->getItems());
}
/**
* @return array{
* version: string,
* is_new: false,
* is_unavailable: bool,
* description: string|null,
* executed_at: DateTimeImmutable|null,
* execution_time: float|null,
* file: string|false|null,
* }[]
*/
public function flattenExecutedMigrations(ExecutedMigrationsList $migrationsList, AvailableMigrationsList $availableMigrations): array
{
return array_map(static function (ExecutedMigration $migration) use ($availableMigrations) {
$availableMigration = $availableMigrations->hasMigration($migration->getVersion())
? $availableMigrations->getMigration($migration->getVersion())->getMigration()
: null;
return [
'version' => (string) $migration->getVersion(),
'is_new' => false,
'is_unavailable' => $availableMigration === null,
'description' => $availableMigration !== null ? $availableMigration->getDescription() : null,
'executed_at' => $migration->getExecutedAt(),
'execution_time' => $migration->getExecutionTime(),
'file' => $availableMigration !== null ? (new ReflectionClass($availableMigration))->getFileName() : null,
];
}, $migrationsList->getItems());
}
}

View File

@@ -0,0 +1,116 @@
<?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;
/** @final */
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))
));
}
}
}

View File

@@ -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));
}
}

View File

@@ -0,0 +1,178 @@
<?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;
/** @final */
class Configuration implements ConfigurationInterface
{
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()
->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([])
->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;
}
}

View File

@@ -0,0 +1,227 @@
<?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\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;
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\PhpFileLoader;
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;
/** @final */
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__ . '/../../config/');
$loader = new PhpFileLoader($container, $locator);
$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) {
$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])) {
$container->removeDefinition('doctrine_migrations.schema_filter_listener');
} else {
$filterDefinition = $container->getDefinition('doctrine_migrations.schema_filter_listener');
$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) {
$filterDefinition->addArgument('doctrine_migration_versions');
} else {
$storageDefinition->addMethodCall('setTableName', [$storageConfiguration['table_name']]);
$filterDefinition->addArgument($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')]);
// Add tag to the filter for each Doctrine connection, so the table is ignored for multiple connections
if ($container->hasParameter('doctrine.connections')) {
/** @var array<string, string> $connections */
$connections = $container->getParameter('doctrine.connections');
foreach (array_keys($connections) as $connection) {
$filterDefinition->addTag('doctrine.dbal.schema_filter', ['connection' => $connection]);
}
}
}
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);
}
public function getXsdValidationBasePath(): string
{
return __DIR__ . '/../../config/schema';
}
public function getNamespace(): string
{
return 'http://symfony.com/schema/dic/doctrine/migrations/3.0';
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
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;
use function dirname;
/** @final */
class DoctrineMigrationsBundle extends Bundle
{
public function build(ContainerBuilder $container): void
{
$container->addCompilerPass(new ConfigureDependencyFactoryPass());
$container->addCompilerPass(new RegisterMigrationsPass());
}
public function getPath(): string
{
return dirname(__DIR__);
}
}

View File

@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\MigrationsBundle\EventListener;
use Doctrine\DBAL\Schema\AbstractAsset;
use Doctrine\DBAL\Schema\Name\OptionallyQualifiedName;
use Doctrine\ORM\Tools\Console\Command\SchemaTool\UpdateCommand;
use Doctrine\ORM\Tools\Console\Command\ValidateSchemaCommand;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
/**
* Acts as a schema filter that hides the migration metadata table except
* when the execution context is that of command inside the migrations
* namespace.
*/
final class SchemaFilterListener
{
/** @var string */
private $configurationTableName;
public function __construct(string $configurationTableName)
{
$this->configurationTableName = $configurationTableName;
}
/** @var bool */
private $enabled = false;
/** @param AbstractAsset<OptionallyQualifiedName>|string $asset */
public function __invoke($asset): bool
{
if (! $this->enabled) {
return true;
}
if ($asset instanceof AbstractAsset) {
$asset = $asset->getName();
}
return $asset !== $this->configurationTableName;
}
public function onConsoleCommand(ConsoleCommandEvent $event): void
{
$command = $event->getCommand();
if (! $command instanceof ValidateSchemaCommand && ! $command instanceof UpdateCommand) {
return;
}
$this->enabled = true;
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\MigrationsBundle\MigrationsFactory;
use Doctrine\Migrations\AbstractMigration;
use Doctrine\Migrations\Version\MigrationFactory;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use function trigger_deprecation;
/** @deprecated This class is not compatible with Symfony >= 7 */
class ContainerAwareMigrationFactory implements MigrationFactory
{
/** @var ContainerInterface */
private $container;
/** @var MigrationFactory */
private $migrationFactory;
public function __construct(MigrationFactory $migrationFactory, ContainerInterface $container)
{
$this->container = $container;
$this->migrationFactory = $migrationFactory;
}
public function createVersion(string $migrationClassName): AbstractMigration
{
$migration = $this->migrationFactory->createVersion($migrationClassName);
if ($migration instanceof ContainerAwareInterface) {
trigger_deprecation('doctrine/doctrine-migrations-bundle', '3.3', 'Migration "%s" implements "%s" to gain access to the application\'s service container. This method is deprecated and won\'t work with Symfony 7.', $migrationClassName, ContainerAwareInterface::class);
$migration->setContainer($this->container);
}
return $migration;
}
}

View File

@@ -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));
}
}

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-versions" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<rect x="10" y="5" width="10" height="14" rx="2"></rect>
<line x1="7" y1="7" x2="7" y2="17"></line>
<line x1="4" y1="8" x2="4" y2="16"></line>
</svg>

After

Width:  |  Height:  |  Size: 446 B

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<polygon fill="#AAA" points="0 0 24 0 24 7 17.5 7 16.5 3 15.5 7 11 7 12 3 3 3 4 7 0 7" />
<polygon fill="#AAA" points="0 8.5 4.5 8.5 6 15.5 0 15.5" />
<polygon fill="#AAA" points="10.5 8.5 15 8.5 13.5 15.5 9 15.5" />
<polygon fill="#AAA" points="18 8.5 24 8.5 24 15.5 19.5 15.5" />
<polygon fill="#AAA" points="0 17 6.5 17 7.5 21 8.5 17 13 17 12 21 21 21 20 17 24 17 24 24 0 24" />
</svg>

After

Width:  |  Height:  |  Size: 493 B

View File

@@ -0,0 +1,252 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block toolbar %}
{% if collector.data.unavailable_migrations_count is defined %}
{% set unavailable_migrations = collector.data.unavailable_migrations_count %}
{% set new_migrations = collector.data.new_migrations|length %}
{% if unavailable_migrations > 0 or new_migrations > 0 %}
{% set executed_migrations = collector.data.executed_migrations|length %}
{% set available_migrations = collector.data.available_migrations_count %}
{% set status_color = unavailable_migrations > 0 ? 'yellow' : '' %}
{% set status_color = new_migrations > 0 ? 'red' : status_color %}
{% set icon %}
{% if profiler_markup_version < 3 %}
{{ include('@DoctrineMigrations/Collector/icon.svg') }}
{% else %}
{{ include('@DoctrineMigrations/Collector/icon-v3.svg') }}
{% endif %}
<span class="sf-toolbar-value">{{ new_migrations + unavailable_migrations }}</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-group">
<div class="sf-toolbar-info-piece">
<b>Current Migration</b>
<span>{{ executed_migrations > 0 ? collector.data.executed_migrations|last.version|split('\\')|last : 'n/a' }}</span>
</div>
</div>
<div class="sf-toolbar-info-group">
<div class="sf-toolbar-info-piece">
<span class="sf-toolbar-header">
<b>Database Migrations</b>
</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Executed</b>
<span class="sf-toolbar-status">{{ executed_migrations }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Unavailable</b>
<span class="sf-toolbar-status {{ unavailable_migrations > 0 ? 'sf-toolbar-status-yellow' }}">{{ unavailable_migrations }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Available</b>
<span class="sf-toolbar-status">{{ available_migrations }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>New</b>
<span class="sf-toolbar-status {{ new_migrations > 0 ? 'sf-toolbar-status-red' }}">{{ new_migrations }}</span>
</div>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: status_color }) }}
{% endif %}
{% endif %}
{% endblock %}
{% block menu %}
{% if collector.data.unavailable_migrations_count is defined %}
{% set unavailable_migrations = collector.data.unavailable_migrations_count %}
{% set new_migrations = collector.data.new_migrations|length %}
{% set label = unavailable_migrations > 0 ? 'label-status-warning' : '' %}
{% set label = new_migrations > 0 ? 'label-status-error' : label %}
<span class="label {{ label }}">
<span class="icon">
{% if profiler_markup_version < 3 %}
{{ include('@DoctrineMigrations/Collector/icon.svg') }}
{% else %}
{{ include('@DoctrineMigrations/Collector/icon-v3.svg') }}
{% endif %}
</span>
<strong>Migrations</strong>
{% if unavailable_migrations > 0 or new_migrations > 0 %}
<span class="count">
<span>{{ new_migrations + unavailable_migrations }}</span>
</span>
{% endif %}
</span>
{% endif %}
{% endblock %}
{% block panel %}
{% set num_executed_migrations = collector.data.executed_migrations|length %}
{% set num_unavailable_migrations = collector.data.unavailable_migrations_count %}
{% set num_available_migrations = collector.data.available_migrations_count %}
{% set num_new_migrations = collector.data.new_migrations|length %}
<h2>Doctrine Migrations</h2>
<div class="metrics">
<div class="metric">
<span class="value">{{ num_executed_migrations }}</span>
<span class="label">Executed</span>
</div>
{% if profiler_markup_version >= 3 %}
<div class="metric-group">
{% endif %}
<div class="metric">
<span class="value">{{ num_unavailable_migrations }}</span>
<span class="label">Unavailable</span>
</div>
<div class="metric">
<span class="value">{{ num_available_migrations }}</span>
<span class="label">Available</span>
</div>
<div class="metric">
<span class="value">{{ num_new_migrations }}</span>
<span class="label">New</span>
</div>
{% if profiler_markup_version >= 3 %}
</div> {# closes the <div class="metric-group"> #}
{% endif %}
</div>
<div class="sf-tabs">
<div class="tab">
<h3 class="tab-title">
Migrations
<span class="badge {{ num_new_migrations > 0 ? 'status-error' : num_unavailable_migrations > 0 ? 'status-warning' }}">
{{ num_new_migrations > 0 ? num_new_migrations : num_unavailable_migrations > 0 ? num_unavailable_migrations : num_executed_migrations }}
</span>
</h3>
<div class="tab-content">
{{ _self.render_migration_details(collector, profiler_markup_version) }}
</div>
</div>
<div class="tab">
<h3 class="tab-title">Configuration</h3>
<div class="tab-content">
{{ _self.render_configuration_details(collector, profiler_markup_version) }}
</div>
</div>
</div>
{% endblock %}
{% macro render_migration_details(collector) %}
<table>
<thead>
<tr>
<th class="colored font-normal">Version</th>
<th class="colored font-normal">Description</th>
<th class="colored font-normal">Status</th>
<th class="colored font-normal">Executed at</th>
<th class="colored font-normal text-right">Execution time</th>
</tr>
</thead>
{% for migration in collector.data.new_migrations %}
{{ _self.render_migration(migration) }}
{% endfor %}
{% for migration in collector.data.executed_migrations|reverse %}
{{ _self.render_migration(migration) }}
{% endfor %}
</table>
{% endmacro %}
{% macro render_configuration_details(collector) %}
<table>
<thead>
<tr>
<th colspan="2" class="colored font-normal">Storage</th>
</tr>
</thead>
<tr>
<td class="font-normal">Type</td>
<td class="font-normal">{{ collector.data.storage }}</td>
</tr>
{% if collector.data.table is defined %}
<tr>
<td class="font-normal">Table Name</td>
<td class="font-normal">{{ collector.data.table }}</td>
</tr>
{% endif %}
{% if collector.data.column is defined %}
<tr>
<td class="font-normal">Column Name</td>
<td class="font-normal">{{ collector.data.column }}</td>
</tr>
{% endif %}
</table>
<table>
<thead>
<tr>
<th colspan="2" class="colored font-normal">Database</th>
</tr>
</thead>
<tr>
<td class="font-normal">Driver</td>
<td class="font-normal">{{ collector.data.driver }}</td>
</tr>
<tr>
<td class="font-normal">Name</td>
<td class="font-normal">{{ collector.data.name }}</td>
</tr>
</table>
<table>
<thead>
<tr>
<th colspan="2" class="colored font-normal">Migration Namespaces</th>
</tr>
</thead>
{% for namespace, directory in collector.data.namespaces %}
<tr>
<td class="font-normal">{{ namespace }}</td>
<td class="font-normal">{{ directory }}</td>
</tr>
{% endfor %}
</table>
{% endmacro %}
{% macro render_migration(migration, profiler_markup_version) %}
<tr>
<td class="font-normal">
{% if migration.file %}
<a href="{{ migration.file|file_link(1) }}" title="{{ migration.file }}">{{ migration.version }}</a>
{% else %}
{{ migration.version }}
{% endif %}
</td>
<td class="font-normal">{{ migration.description }}</td>
<td class="font-normal align-right">
{% if migration.is_new %}
<span class="{{ profiler_markup_version >= 3 ? 'badge badge-error' : 'label status-error' }}">NOT EXECUTED</span>
{% elseif migration.is_unavailable %}
<span class="{{ profiler_markup_version >= 3 ? 'badge badge-warning' : 'label status-warning' }}">UNAVAILABLE</span>
{% else %}
<span class="{{ profiler_markup_version >= 3 ? 'badge badge-success' : 'label status-success' }}">EXECUTED</span>
{% endif %}
</td>
<td class="font-normal">{{ migration.executed_at ? migration.executed_at|date('M j, Y H:i') : 'n/a' }}</td>
<td class="font-normal text-right">
{% if migration.execution_time is null %}
n/a
{% elseif migration.execution_time < 1 %}
{{ (migration.execution_time * 1000)|number_format(0) }} ms
{% else %}
{{ migration.execution_time|number_format(2) }} seconds
{% endif %}
</td>
</tr>
{% endmacro %}