Skip to content

Commit d124847

Browse files
authored
Merge pull request #407 from chr-hertel/profiler-collector
Profiler collector
2 parents 64d6b61 + 576bef9 commit d124847

File tree

8 files changed

+514
-0
lines changed

8 files changed

+514
-0
lines changed

Collector/MigrationsCollector.php

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Bundle\MigrationsBundle\Collector;
6+
7+
use Doctrine\Migrations\DependencyFactory;
8+
use Doctrine\Migrations\Metadata\Storage\TableMetadataStorageConfiguration;
9+
use Symfony\Component\HttpFoundation\Request;
10+
use Symfony\Component\HttpFoundation\Response;
11+
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
12+
13+
class MigrationsCollector extends DataCollector
14+
{
15+
/** @var DependencyFactory */
16+
private $dependencyFactory;
17+
/** @var MigrationsFlattener */
18+
private $flattener;
19+
20+
public function __construct(DependencyFactory $dependencyFactory, MigrationsFlattener $migrationsFlattener)
21+
{
22+
$this->dependencyFactory = $dependencyFactory;
23+
$this->flattener = $migrationsFlattener;
24+
}
25+
26+
public function collect(Request $request, Response $response, \Throwable $exception = null)
27+
{
28+
$metadataStorage = $this->dependencyFactory->getMetadataStorage();
29+
$planCalculator = $this->dependencyFactory->getMigrationPlanCalculator();
30+
$statusCalculator = $this->dependencyFactory->getMigrationStatusCalculator();
31+
32+
$executedMigrations = $metadataStorage->getExecutedMigrations();
33+
$availableMigrations = $planCalculator->getMigrations();
34+
35+
$this->data['available_migrations'] = $this->flattener->flattenAvailableMigrations($availableMigrations, $executedMigrations);
36+
$this->data['executed_migrations'] = $this->flattener->flattenExecutedMigrations($executedMigrations, $availableMigrations);
37+
38+
$this->data['new_migrations'] = $this->flattener->flattenAvailableMigrations($statusCalculator->getNewMigrations());
39+
$this->data['unavailable_migrations'] = $this->flattener->flattenExecutedMigrations($statusCalculator->getExecutedUnavailableMigrations());
40+
41+
$this->data['storage'] = get_class($metadataStorage);
42+
$configuration = $this->dependencyFactory->getConfiguration();
43+
$storage = $configuration->getMetadataStorageConfiguration();
44+
if ($storage instanceof TableMetadataStorageConfiguration) {
45+
$this->data['table'] = $storage->getTableName();
46+
$this->data['column'] = $storage->getVersionColumnName();
47+
}
48+
49+
$connection = $this->dependencyFactory->getConnection();
50+
$this->data['driver'] = get_class($connection->getDriver());
51+
$this->data['name'] = $connection->getDatabase();
52+
53+
$this->data['namespaces'] = $configuration->getMigrationDirectories();
54+
}
55+
56+
public function getName()
57+
{
58+
return 'doctrine_migrations';
59+
}
60+
61+
public function getData()
62+
{
63+
return $this->data;
64+
}
65+
66+
public function reset()
67+
{
68+
$this->data = [];
69+
}
70+
}

Collector/MigrationsFlattener.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Bundle\MigrationsBundle\Collector;
6+
7+
use Doctrine\Migrations\Metadata\AvailableMigration;
8+
use Doctrine\Migrations\Metadata\AvailableMigrationsList;
9+
use Doctrine\Migrations\Metadata\ExecutedMigration;
10+
use Doctrine\Migrations\Metadata\ExecutedMigrationsList;
11+
12+
class MigrationsFlattener
13+
{
14+
public function flattenAvailableMigrations(AvailableMigrationsList $migrationsList, ?ExecutedMigrationsList $executedMigrations = null): array
15+
{
16+
return array_map(static function (AvailableMigration $migration) use ($executedMigrations) {
17+
$executedMigration = $executedMigrations && $executedMigrations->hasMigration($migration->getVersion())
18+
? $executedMigrations->getMigration($migration->getVersion())
19+
: null;
20+
21+
return [
22+
'version' => (string)$migration->getVersion(),
23+
'is_new' => !$executedMigration,
24+
'is_unavailable' => false,
25+
'description' => $migration->getMigration()->getDescription(),
26+
'executed_at' => $executedMigration ? $executedMigration->getExecutedAt() : null,
27+
'execution_time' => $executedMigration ? $executedMigration->getExecutionTime() : null,
28+
'file' => (new \ReflectionClass($migration->getMigration()))->getFileName(),
29+
];
30+
}, $migrationsList->getItems());
31+
}
32+
33+
public function flattenExecutedMigrations(ExecutedMigrationsList $migrationsList, ?AvailableMigrationsList $availableMigrations = null): array
34+
{
35+
return array_map(static function (ExecutedMigration $migration) use ($availableMigrations) {
36+
37+
$availableMigration = $availableMigrations && $availableMigrations->hasMigration($migration->getVersion())
38+
? $availableMigrations->getMigration($migration->getVersion())->getMigration()
39+
: null;
40+
41+
return [
42+
'version' => (string)$migration->getVersion(),
43+
'is_new' => false,
44+
'is_unavailable' => !$availableMigration,
45+
'description' => $availableMigration ? $availableMigration->getDescription() : null,
46+
'executed_at' => $migration->getExecutedAt(),
47+
'execution_time' => $migration->getExecutionTime(),
48+
'file' => $availableMigration ? (new \ReflectionClass($availableMigration))->getFileName() : null,
49+
];
50+
}, $migrationsList->getItems());
51+
}
52+
}

DependencyInjection/Configuration.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,10 @@ public function getConfigTreeBuilder(): TreeBuilder
147147
})
148148
->end()
149149
->end()
150+
->booleanNode('enable_profiler')
151+
->info('Use profiler to calculate and visualize migration status.')
152+
->defaultFalse()
153+
->end()
150154
->end();
151155

152156
return $treeBuilder;

DependencyInjection/DoctrineMigrationsExtension.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
namespace Doctrine\Bundle\MigrationsBundle\DependencyInjection;
66

7+
use Doctrine\Bundle\MigrationsBundle\Collector\MigrationsCollector;
8+
use Doctrine\Bundle\MigrationsBundle\Collector\MigrationsFlattener;
79
use Doctrine\Migrations\Metadata\Storage\MetadataStorage;
810
use Doctrine\Migrations\Metadata\Storage\TableMetadataStorageConfiguration;
911
use Doctrine\Migrations\Version\MigrationFactory;
@@ -71,6 +73,10 @@ public function load(array $configs, ContainerBuilder $container): void
7173
$configurationDefinition->addMethodCall('setAllOrNothing', [$config['all_or_nothing']]);
7274
$configurationDefinition->addMethodCall('setCheckDatabasePlatform', [$config['check_database_platform']]);
7375

76+
if ($config['enable_profiler']) {
77+
$this->registerCollector($container);
78+
}
79+
7480
$diDefinition = $container->getDefinition('doctrine.migrations.dependency_factory');
7581

7682
if (! isset($config['services'][MigrationFactory::class])) {
@@ -155,6 +161,24 @@ private function getBundlePath(string $bundleName, ContainerBuilder $container):
155161
return $bundleMetadata[$bundleName]['path'];
156162
}
157163

164+
private function registerCollector(ContainerBuilder $container): void
165+
{
166+
$flattenerDefinition = new Definition(MigrationsFlattener::class);
167+
$container->setDefinition('doctrine_migrations.migrations_flattener', $flattenerDefinition);
168+
169+
$collectorDefinition = new Definition(MigrationsCollector::class, [
170+
new Reference('doctrine.migrations.dependency_factory'),
171+
new Reference('doctrine_migrations.migrations_flattener'),
172+
]);
173+
$collectorDefinition
174+
->addTag('data_collector', [
175+
'template' => '@DoctrineMigrations/Collector/migrations.html.twig',
176+
'id' => 'doctrine_migrations',
177+
'priority' => '249',
178+
]);
179+
$container->setDefinition('doctrine_migrations.migrations_collector', $collectorDefinition);
180+
}
181+
158182
/**
159183
* Returns the base path for the XSD files.
160184
*

Resources/config/services.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@
140140

141141
<tag name="console.command" command="doctrine:migrations:version" />
142142
</service>
143+
143144
</services>
144145

145146
</container>

Resources/views/Collector/icon.svg

Lines changed: 7 additions & 0 deletions
Loading
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
2+
3+
{% import _self as helper %}
4+
5+
{% block toolbar %}
6+
{% set unavailable_migrations = collector.data.unavailable_migrations|length %}
7+
{% set new_migrations = collector.data.new_migrations|length %}
8+
{% if unavailable_migrations > 0 or new_migrations > 0 %}
9+
{% set executed_migrations = collector.data.executed_migrations|length %}
10+
{% set available_migrations = collector.data.available_migrations|length %}
11+
{% set status_color = unavailable_migrations > 0 ? 'yellow' : '' %}
12+
{% set status_color = new_migrations > 0 ? 'red' : status_color %}
13+
14+
{% set icon %}
15+
{{ include('@DoctrineMigrations/Collector/icon.svg') }}
16+
<span class="sf-toolbar-value">{{ new_migrations + unavailable_migrations }}</span>
17+
{% endset %}
18+
19+
{% set text %}
20+
<div class="sf-toolbar-info-piece">
21+
<b>Current</b>
22+
<span>{{ collector.data.executed_migrations|last.version|split('\\')|last }}</span>
23+
</div>
24+
<div class="sf-toolbar-info-piece">
25+
<b>Executed</b>
26+
<span class="sf-toolbar-status">{{ executed_migrations }}</span>
27+
</div>
28+
<div class="sf-toolbar-info-piece">
29+
<b>Executed Unavailable</b>
30+
<span class="sf-toolbar-status {{ unavailable_migrations > 0 ? 'sf-toolbar-status-yellow' }}">{{ unavailable_migrations }}</span>
31+
</div>
32+
<div class="sf-toolbar-info-piece">
33+
<b>Available</b>
34+
<span class="sf-toolbar-status">{{ available_migrations }}</span>
35+
</div>
36+
<div class="sf-toolbar-info-piece">
37+
<b>New</b>
38+
<span class="sf-toolbar-status {{ new_migrations > 0 ? 'sf-toolbar-status-red' }}">{{ new_migrations }}</span>
39+
</div>
40+
{% endset %}
41+
42+
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: status_color }) }}
43+
{% endif %}
44+
{% endblock %}
45+
46+
47+
{% block menu %}
48+
{% set unavailable_migrations = collector.data.unavailable_migrations|length %}
49+
{% set new_migrations = collector.data.new_migrations|length %}
50+
{% set label = unavailable_migrations > 0 ? 'label-status-warning' : '' %}
51+
{% set label = new_migrations > 0 ? 'label-status-error' : label %}
52+
<span class="label {{ label }}">
53+
<span class="icon">{{ include('@DoctrineMigrations/Collector/icon.svg') }}</span>
54+
<strong>Migrations</strong>
55+
{% if unavailable_migrations > 0 or new_migrations > 0 %}
56+
<span class="count">
57+
<span>{{ new_migrations + unavailable_migrations }}</span>
58+
</span>
59+
{% endif %}
60+
</span>
61+
{% endblock %}
62+
63+
{% block panel %}
64+
<h2>Doctrine Migrations</h2>
65+
<div class="metrics">
66+
<div class="metric">
67+
<span class="value">{{ collector.data.executed_migrations|length }}</span>
68+
<span class="label">Executed</span>
69+
</div>
70+
<div class="metric">
71+
<span class="value">{{ collector.data.unavailable_migrations|length }}</span>
72+
<span class="label">Executed Unavailable</span>
73+
</div>
74+
<div class="metric">
75+
<span class="value">{{ collector.data.available_migrations|length }}</span>
76+
<span class="label">Available</span>
77+
</div>
78+
<div class="metric">
79+
<span class="value">{{ collector.data.new_migrations|length }}</span>
80+
<span class="label">New</span>
81+
</div>
82+
</div>
83+
84+
<h3>Configuration</h3>
85+
<table>
86+
<thead>
87+
<tr>
88+
<th colspan="2" class="colored font-normal">Storage</th>
89+
</tr>
90+
</thead>
91+
<tr>
92+
<td class="font-normal">Type</td>
93+
<td class="font-normal">{{ collector.data.storage }}</td>
94+
</tr>
95+
{% if collector.data.table is defined %}
96+
<tr>
97+
<td class="font-normal">Table Name</td>
98+
<td class="font-normal">{{ collector.data.table }}</td>
99+
</tr>
100+
{% endif %}
101+
{% if collector.data.column is defined %}
102+
<tr>
103+
<td class="font-normal">Column Name</td>
104+
<td class="font-normal">{{ collector.data.column }}</td>
105+
</tr>
106+
{% endif %}
107+
</table>
108+
<table>
109+
<thead>
110+
<tr>
111+
<th colspan="2" class="colored font-normal">Database</th>
112+
</tr>
113+
</thead>
114+
<tr>
115+
<td class="font-normal">Driver</td>
116+
<td class="font-normal">{{ collector.data.driver }}</td>
117+
</tr>
118+
<tr>
119+
<td class="font-normal">Name</td>
120+
<td class="font-normal">{{ collector.data.name }}</td>
121+
</tr>
122+
</table>
123+
<table>
124+
<thead>
125+
<tr>
126+
<th colspan="2" class="colored font-normal">Migration Namespaces</th>
127+
</tr>
128+
</thead>
129+
{% for namespace, directory in collector.data.namespaces %}
130+
<tr>
131+
<td class="font-normal">{{ namespace }}</td>
132+
<td class="font-normal">{{ directory }}</td>
133+
</tr>
134+
{% endfor %}
135+
</table>
136+
137+
<h3>Migrations</h3>
138+
<table>
139+
<thead>
140+
<tr>
141+
<th class="colored font-normal">Version</th>
142+
<th class="colored font-normal">Description</th>
143+
<th class="colored font-normal">Status</th>
144+
<th class="colored font-normal">Executed at</th>
145+
<th class="colored font-normal">Execution time</th>
146+
</tr>
147+
</thead>
148+
{% for migration in collector.data.new_migrations %}
149+
{{ helper.render_migration(migration) }}
150+
{% endfor %}
151+
152+
{% for migration in collector.data.executed_migrations|reverse %}
153+
{{ helper.render_migration(migration) }}
154+
{% endfor %}
155+
</table>
156+
{% endblock %}
157+
158+
{% macro render_migration(migration) %}
159+
160+
<tr>
161+
<td class="font-normal">
162+
{% if migration.file %}
163+
<a href="{{ migration.file|file_link(1) }}" title="{{ migration.file }}">{{ migration.version }}</a>
164+
{% else %}
165+
{{ migration.version }}
166+
{% endif %}
167+
</td>
168+
<td class="font-normal">{{ migration.description }}</td>
169+
<td class="font-normal">
170+
{% if migration.is_new %}
171+
<span class="label status-error">NOT EXECUTED</span>
172+
{% elseif migration.is_unavailable %}
173+
<span class="label status-warning">UNAVAILABLE</span>
174+
{% else %}
175+
<span class="label status-success">EXECUTED</span>
176+
{% endif %}
177+
</td>
178+
<td class="font-normal">{{ migration.executed_at ? migration.executed_at|date : 'n/a' }}</td>
179+
<td class="font-normal">{{ migration.execution_time|default('n/a') }}</td>
180+
</tr>
181+
{% endmacro %}

0 commit comments

Comments
 (0)