MigrationsLoader::createMigrationObjects()   C
last analyzed

Complexity

Conditions 12
Paths 52

Size

Total Lines 49
Code Lines 25

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 49
rs 5.1474
cc 12
eloc 25
nc 52
nop 2

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace RDV\Bundle\MigrationBundle\Migration\Loader;
4
5
use Doctrine\DBAL\Connection;
6
use Symfony\Component\DependencyInjection\ContainerInterface;
7
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
8
use Symfony\Component\Finder\Finder;
9
use Symfony\Component\Finder\SplFileInfo;
10
use Symfony\Component\HttpKernel\Bundle\BundleInterface;
11
use Symfony\Component\HttpKernel\KernelInterface;
12
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
13
14
use RDV\Bundle\MigrationBundle\Migration\Migration;
15
use RDV\Bundle\MigrationBundle\Migration\MigrationState;
16
use RDV\Bundle\MigrationBundle\Migration\Installation;
17
use RDV\Bundle\MigrationBundle\Migration\OrderedMigrationInterface;
18
use RDV\Bundle\MigrationBundle\Migration\UpdateBundleVersionMigration;
19
use RDV\Bundle\MigrationBundle\Event\MigrationEvents;
20
use RDV\Bundle\MigrationBundle\Event\PostMigrationEvent;
21
use RDV\Bundle\MigrationBundle\Event\PreMigrationEvent;
22
23
/**
24
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
25
 */
26
class MigrationsLoader
27
{
28
    const DEFAULT_MIGRATIONS_PATH = 'Migrations/Schema';
29
30
    /**
31
     * @var KernelInterface
32
     *
33
     */
34
    protected $kernel;
35
36
    /**
37
     * @var Connection
38
     */
39
    protected $connection;
40
41
    /**
42
     * @var ContainerInterface
43
     */
44
    protected $container;
45
46
    /**
47
     * @var EventDispatcherInterface
48
     */
49
    protected $eventDispatcher;
50
51
    /**
52
     * @var array An array with already loaded bundle migration versions
53
     *             key =   bundle name
54
     *             value = latest loaded version
55
     */
56
    protected $loadedVersions;
57
58
    /**
59
     * @var array An array with bundles we must work from
60
     */
61
    protected $bundles;
62
63
    /**
64
     * @var array An array with excluded bundles
65
     */
66
    protected $excludeBundles;
67
68
    /**
69
     * @var string
70
     */
71
    protected $migrationPath = self::DEFAULT_MIGRATIONS_PATH;
72
73
    /**
74
     * @param KernelInterface          $kernel
75
     * @param Connection               $connection
76
     * @param ContainerInterface       $container
77
     * @param EventDispatcherInterface $eventDispatcher
78
     */
79
    public function __construct(
80
        KernelInterface $kernel,
81
        Connection $connection,
82
        ContainerInterface $container,
83
        EventDispatcherInterface $eventDispatcher
84
    ) {
85
        $this->kernel          = $kernel;
86
        $this->connection      = $connection;
87
        $this->container       = $container;
88
        $this->eventDispatcher = $eventDispatcher;
89
    }
90
91
    /**
92
     * @param array $bundles
93
     */
94
    public function setBundles($bundles)
95
    {
96
        $this->bundles = $bundles;
97
    }
98
99
    /**
100
     * @param array $excludeBundles
101
     */
102
    public function setExcludeBundles($excludeBundles)
103
    {
104
        $this->excludeBundles = $excludeBundles;
105
    }
106
107
    /**
108
     * @param string $migrationPath
109
     * @return $this
110
     */
111
    public function setMigrationPath($migrationPath)
112
    {
113
        $this->migrationPath = $migrationPath;
114
115
        return $this;
116
    }
117
118
    /**
119
     * @return MigrationState[]
120
     */
121
    public function getMigrations()
122
    {
123
        $result = [];
124
125
        // process "pre" migrations
126
        $preEvent = new PreMigrationEvent($this->connection);
127
        $this->eventDispatcher->dispatch(MigrationEvents::PRE_UP, $preEvent);
128
        $preMigrations = $preEvent->getMigrations();
129
        foreach ($preMigrations as $migration) {
130
            $result[] = new MigrationState($migration);
131
        }
132
        $this->loadedVersions = $preEvent->getLoadedVersions();
133
134
        // process main migrations
135
        $migrationDirectories = $this->getMigrationDirectories();
136
        $this->filterMigrations($migrationDirectories);
137
        $this->createMigrationObjects(
138
            $result,
139
            $this->loadMigrationScripts($migrationDirectories)
140
        );
141
142
        $result[] = new MigrationState(new UpdateBundleVersionMigration($result));
143
144
        // process "post" migrations
145
        $postEvent = new PostMigrationEvent($this->connection);
146
        $this->eventDispatcher->dispatch(MigrationEvents::POST_UP, $postEvent);
147
        $postMigrations = $postEvent->getMigrations();
148
        foreach ($postMigrations as $migration) {
149
            $result[] = new MigrationState($migration);
150
        }
151
152
        return $result;
153
    }
154
155
    /**
156
     * Gets a list of all directories contain migration scripts
157
     *
158
     * @return array
159
     *      key   = bundle name
160
     *      value = array
161
     *      .    key   = a migration version (actually it equals the name of migration directory)
162
     *      .            or empty string for root migration directory
163
     *      .    value = full path to a migration directory
164
     */
165
    protected function getMigrationDirectories()
166
    {
167
        $result = [];
168
169
        $bundles = $this->getBundleList();
170
        foreach ($bundles as $bundleName => $bundle) {
171
            $bundleMigrationPath = $this->getBundleMigrationPath($bundle->getPath());
172
173
            if (is_dir($bundleMigrationPath)) {
174
                $bundleMigrationDirectories = [];
175
176
                // get directories contain versioned migration scripts
177
                $finder = new Finder();
178
                $finder->directories()->depth(0)->in($bundleMigrationPath);
179
                /** @var SplFileInfo $directory */
180
                foreach ($finder as $directory) {
181
                    $bundleMigrationDirectories[$directory->getRelativePathname()] = $directory->getPathname();
182
                }
183
                // add root migration directory (it may contains an installation script)
184
                $bundleMigrationDirectories[''] = $bundleMigrationPath;
185
                // sort them by version number (the oldest version first)
186
                if (!empty($bundleMigrationDirectories)) {
187
                    uksort(
188
                        $bundleMigrationDirectories,
189
                        function ($a, $b) {
190
                            return version_compare($a, $b);
191
                        }
192
                    );
193
                }
194
195
                $result[$bundleName] = $bundleMigrationDirectories;
196
            }
197
        }
198
199
        return $result;
200
    }
201
202
    /**
203
     * @param string $bundlePath
204
     * @return string
205
     */
206
    protected function getBundleMigrationPath($bundlePath)
207
    {
208
        return realpath(str_replace('/', DIRECTORY_SEPARATOR, $bundlePath . '/' . $this->migrationPath));
209
    }
210
211
212
    /**
213
     * Finds migration files and call "include_once" for each file
214
     *
215
     * @param array $migrationDirectories
216
     *               key   = bundle name
217
     *               value = array
218
     *               .    key   = a migration version or empty string for root migration directory
219
     *               .    value = full path to a migration directory
220
     *
221
     * @return array loaded files
222
     *               'migrations' => array
223
     *               .      key   = full file path
224
     *               .      value = array
225
     *               .            'bundleName' => bundle name
226
     *               .            'version'    => migration version
227
     *               'installers' => array
228
     *               .      key   = full file path
229
     *               .      value = bundle name
230
     *               'bundles'    => string[] names of bundles
231
     */
232
    protected function loadMigrationScripts(array $migrationDirectories)
233
    {
234
        $migrations = [];
235
        $installers = [];
236
237
        foreach ($migrationDirectories as $bundleName => $bundleMigrationDirectories) {
238
            foreach ($bundleMigrationDirectories as $migrationVersion => $migrationPath) {
239
                $fileFinder = new Finder();
240
                $fileFinder->depth(0)->files()->name('*.php')->in($migrationPath);
241
                foreach ($fileFinder as $file) {
242
                    /** @var SplFileInfo $file */
243
                    $filePath = $file->getPathname();
244
                    include_once $filePath;
245
                    if (empty($migrationVersion)) {
246
                        $installers[$filePath] = $bundleName;
247
                    } else {
248
                        $migrations[$filePath] = ['bundleName' => $bundleName, 'version' => $migrationVersion];
249
                    }
250
                }
251
            }
252
        }
253
254
        return [
255
            'migrations' => $migrations,
256
            'installers' => $installers,
257
            'bundles'    => array_keys($migrationDirectories),
258
        ];
259
    }
260
261
    /**
262
     * Creates an instances of all classes implement migration scripts
263
     *
264
     * @param MigrationState[] $result
265
     * @param array            $files Files contain migration scripts
266
     *                                'migrations' => array
267
     *                                .      key   = full file path
268
     *                                .      value = array
269
     *                                .            'bundleName' => bundle name
270
     *                                .            'version'    => migration version
271
     *                                'installers' => array
272
     *                                .      key   = full file path
273
     *                                .      value = bundle name
274
     *                                'bundles'    => string[] names of bundles
275
     *
276
     * @throws \RuntimeException if a migration script contains more than one class
277
     */
278
    protected function createMigrationObjects(&$result, $files)
279
    {
280
        // load migration objects
281
        list($migrations, $installers) = $this->loadMigrationObjects($files);
282
283
        // remove versioned migrations covered by installers
284
        foreach ($installers as $installer) {
285
            $installerBundleName = $installer['bundleName'];
286
            $installerVersion    = $installer['version'];
287
            foreach ($files['migrations'] as $sourceFile => $migration) {
288
                if ($migration['bundleName'] === $installerBundleName
289
                    && version_compare($migration['version'], $installerVersion) < 1
290
                ) {
291
                    unset($migrations[$sourceFile]);
292
                }
293
            }
294
        }
295
296
        // group migration by bundle & version then sort them within same version
297
        $groupedMigrations = $this->groupAndSortMigrations($files, $migrations);
298
299
        // add migration objects to result tacking into account bundles order
300
        foreach ($files['bundles'] as $bundleName) {
301
            // add installers to the result
302
            foreach ($files['installers'] as $sourceFile => $installerBundleName) {
303
                if ($installerBundleName === $bundleName && isset($migrations[$sourceFile])) {
304
                    /** @var Installation $installer */
305
                    $installer = $migrations[$sourceFile];
306
                    $result[]  = new MigrationState(
307
                        $installer,
308
                        $installerBundleName,
309
                        $installer->getMigrationVersion()
310
                    );
311
                }
312
            }
313
            // add migrations to the result
314
            if (isset($groupedMigrations[$bundleName])) {
315
                foreach ($groupedMigrations[$bundleName] as $version => $versionedMigrations) {
316
                    foreach ($versionedMigrations as $migration) {
317
                        $result[] = new MigrationState(
318
                            $migration,
319
                            $bundleName,
320
                            $version
321
                        );
322
                    }
323
                }
324
            }
325
        }
326
    }
327
328
    /**
329
     * Groups migrations by bundle and version
330
     * Sorts grouped migrations within the same version
331
     *
332
     * @param array $files
333
     * @param array $migrations
334
     *
335
     * @return array
336
     */
337
    protected function groupAndSortMigrations($files, $migrations)
338
    {
339
        $groupedMigrations = [];
340
        foreach ($files['migrations'] as $sourceFile => $migration) {
341
            if (isset($migrations[$sourceFile])) {
342
                $bundleName = $migration['bundleName'];
343
                $version    = $migration['version'];
344
                if (!isset($groupedMigrations[$bundleName])) {
345
                    $groupedMigrations[$bundleName] = [];
346
                }
347
                if (!isset($groupedMigrations[$bundleName][$version])) {
348
                    $groupedMigrations[$bundleName][$version] = [];
349
                }
350
                $groupedMigrations[$bundleName][$version][] = $migrations[$sourceFile];
351
            }
352
        }
353
354
        foreach ($groupedMigrations as $bundleName => $versions) {
355
            foreach ($versions as $version => $versionedMigrations) {
356
                if (count($versionedMigrations) > 1) {
357
                    usort(
358
                        $groupedMigrations[$bundleName][$version],
359
                        function ($a, $b) {
360
                            $aOrder = 0;
361
                            if ($a instanceof OrderedMigrationInterface) {
362
                                $aOrder = $a->getOrder();
363
                            }
364
365
                            $bOrder = 0;
366
                            if ($b instanceof OrderedMigrationInterface) {
367
                                $bOrder = $b->getOrder();
368
                            }
369
370
                            if ($aOrder === $bOrder) {
371
                                return 0;
372
                            } elseif ($aOrder < $bOrder) {
373
                                return -1;
374
                            } else {
375
                                return 1;
376
                            }
377
                        }
378
                    );
379
                }
380
            }
381
        }
382
383
        return $groupedMigrations;
384
    }
385
386
387
    /**
388
     * Loads migration objects
389
     *
390
     * @param $files
391
     *
392
     * @return array
393
     * @throws \RuntimeException
394
     */
395
    protected function loadMigrationObjects($files)
396
    {
397
        $migrations = [];
398
        $installers = [];
399
        $declared   = get_declared_classes();
400
401
        foreach ($declared as $className) {
402
            $reflClass  = new \ReflectionClass($className);
403
            $sourceFile = $reflClass->getFileName();
404
            if (isset($files['migrations'][$sourceFile])) {
405
                if (is_subclass_of($className, 'RDV\Bundle\MigrationBundle\Migration\Migration')) {
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of returns inconsistent results on some PHP versions for interfaces; you could instead use ReflectionClass::implementsInterface.
Loading history...
406
                    $migration = new $className;
407
                    if (isset($migrations[$sourceFile])) {
408
                        throw new \RuntimeException('A migration script must contains only one class.');
409
                    }
410
                    if ($migration instanceof ContainerAwareInterface) {
411
                        $migration->setContainer($this->container);
412
                    }
413
                    $migrations[$sourceFile] = $migration;
414
                }
415
            } elseif (isset($files['installers'][$sourceFile])) {
416
                if (is_subclass_of($className, 'RDV\Bundle\MigrationBundle\Migration\Installation')) {
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of returns inconsistent results on some PHP versions for interfaces; you could instead use ReflectionClass::implementsInterface.
Loading history...
417
                    /** @var \RDV\Bundle\MigrationBundle\Migration\Installation $installer */
418
                    $installer = new $className;
419
                    if (isset($migrations[$sourceFile])) {
420
                        throw new \RuntimeException('An installation  script must contains only one class.');
421
                    }
422
                    if ($installer instanceof ContainerAwareInterface) {
423
                        $installer->setContainer($this->container);
424
                    }
425
                    $migrations[$sourceFile] = $installer;
426
                    $installers[$sourceFile] = [
427
                        'bundleName' => $files['installers'][$sourceFile],
428
                        'version'    => $installer->getMigrationVersion(),
429
                    ];
430
                }
431
            }
432
        }
433
434
        return [
435
            $migrations,
436
            $installers
437
        ];
438
    }
439
440
441
    /**
442
     * Removes already installed migrations
443
     *
444
     * @param array $migrationDirectories
445
     *      key   = bundle name
446
     *      value = array
447
     *      .    key   = a migration version or empty string for root migration directory
448
     *      .    value = full path to a migration directory
449
     */
450
    protected function filterMigrations(array &$migrationDirectories)
451
    {
452
        if (!empty($this->loadedVersions)) {
453
            foreach ($migrationDirectories as $bundleName => $bundleMigrationDirectories) {
454
                $loadedVersion = !empty($this->loadedVersions[$bundleName])
455
                    ? $this->loadedVersions[$bundleName]
456
                    : null;
457
                if ($loadedVersion) {
458
                    foreach (array_keys($bundleMigrationDirectories) as $migrationVersion) {
459
                        if (empty($migrationVersion) || version_compare($migrationVersion, $loadedVersion) < 1) {
460
                            unset($migrationDirectories[$bundleName][$migrationVersion]);
461
                        }
462
                    }
463
                }
464
            }
465
        }
466
    }
467
468
    /**
469
     * @return BundleInterface[] key = bundle name
470
     */
471
    protected function getBundleList()
472
    {
473
        $bundles = $this->kernel->getBundles();
474
        if (!empty($this->bundles)) {
475
            $includedBundles = [];
476
            foreach ($this->bundles as $bundleName) {
477
                if (!empty($bundles[$bundleName])) {
478
                    $includedBundles[$bundleName] = $bundles[$bundleName];
479
                }
480
            }
481
            $bundles = $includedBundles;
482
        }
483
        if (!empty($this->excludeBundles)) {
484
            foreach ($this->excludeBundles as $excludeBundle) {
485
                unset($bundles[$excludeBundle]);
486
            }
487
        }
488
489
        return $bundles;
490
    }
491
}
492