Completed
Pull Request — master (#245)
by Alexis
02:33
created

FixturesLoader   C

Complexity

Total Complexity 58

Size/Duplication

Total Lines 470
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 10

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 58
c 3
b 0
f 0
lcom 1
cbo 10
dl 0
loc 470
rs 6.3005

20 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A getExecutorClass() 0 6 3
B getNameParameter() 0 16 5
A getCachedMetadatas() 0 9 2
A getFixtureLastModified() 0 14 2
A isBackupUpToDate() 0 14 3
A copySqliteBackup() 0 18 1
B purgeDatabase() 0 26 4
A createSqliteSchema() 0 15 2
C loadFixtures() 0 58 11
A cleanDatabase() 0 17 4
A locateResources() 0 17 4
A loadFixtureFiles() 0 19 3
A getFixtureLoader() 0 16 4
A loadFixtureClass() 0 18 4
A postFixtureSetup() 0 3 1
A postFixtureRestore() 0 3 1
A preFixtureRestore() 0 3 1
A postReferenceSave() 0 3 1
A preReferenceSave() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like FixturesLoader often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use FixturesLoader, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * This file is part of the Liip/FunctionalTestBundle
5
 *
6
 * (c) Lukas Kahwe Smith <[email protected]>
7
 *
8
 * This source file is subject to the MIT license that is bundled
9
 * with this source code in the file LICENSE.
10
 */
11
12
namespace Liip\FunctionalTestBundle\Utils;
13
14
use Symfony\Component\DependencyInjection\ContainerInterface;
15
use Symfony\Bridge\Doctrine\ManagerRegistry;
16
use Symfony\Bundle\DoctrineFixturesBundle\Common\DataFixtures\Loader;
17
use Doctrine\Common\Persistence\ObjectManager;
18
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
19
use Doctrine\Common\DataFixtures\Executor\AbstractExecutor;
20
use Doctrine\Common\DataFixtures\ProxyReferenceRepository;
21
use Doctrine\DBAL\Connection;
22
use Doctrine\DBAL\Driver\PDOSqlite\Driver as SqliteDriver;
23
use Doctrine\DBAL\Platforms\MySqlPlatform;
24
use Doctrine\ORM\EntityManager;
25
use Doctrine\ORM\Tools\SchemaTool;
26
use Nelmio\Alice\Fixtures;
27
28
class FixturesLoader
29
{
30
    /** @var \Symfony\Component\DependencyInjection\ContainerInterface $container */
31
    private $container;
32
33
    /**
34
     * @var array
35
     */
36
    private static $cachedMetadatas = array();
37
38
    public function __construct(ContainerInterface $container)
39
    {
40
        $this->container = $container;
41
    }
42
43
    /**
44
     * @param string $type
45
     *
46
     * @return string
47
     */
48
    private function getExecutorClass($type)
49
    {
50
        return 'PHPCR' === $type && class_exists('Doctrine\Bundle\PHPCRBundle\DataFixtures\PHPCRExecutor')
51
            ? 'Doctrine\Bundle\PHPCRBundle\DataFixtures\PHPCRExecutor'
52
            : 'Doctrine\\Common\\DataFixtures\\Executor\\'.$type.'Executor';
53
    }
54
55
    /**
56
     * Get file path of the SQLite database.
57
     *
58
     * @param Connection $connection
59
     *
60
     * @return string $name
61
     */
62
    private function getNameParameter(Connection $connection)
63
    {
64
        $params = $connection->getParams();
65
66
        if (isset($params['master'])) {
67
            $params = $params['master'];
68
        }
69
70
        $name = isset($params['path']) ? $params['path'] : (isset($params['dbname']) ? $params['dbname'] : false);
71
72
        if (!$name) {
73
            throw new \InvalidArgumentException("Connection does not contain a 'path' or 'dbname' parameter and cannot be dropped.");
74
        }
75
76
        return $name;
77
    }
78
79
    /**
80
     * Purge SQLite database.
81
     *
82
     * @param ObjectManager $om
83
     * @param string        $omName The name of object manager to use
84
     */
85
    private function getCachedMetadatas(ObjectManager $om, $omName)
86
    {
87
        if (!isset(self::$cachedMetadatas[$omName])) {
88
            self::$cachedMetadatas[$omName] = $om->getMetadataFactory()->getAllMetadata();
89
            usort(self::$cachedMetadatas[$omName], function ($a, $b) { return strcmp($a->name, $b->name); });
90
        }
91
92
        return self::$cachedMetadatas[$omName];
93
    }
94
95
    /**
96
     * This function finds the time when the data blocks of a class definition
97
     * file were being written to, that is, the time when the content of the
98
     * file was changed.
99
     *
100
     * @param string $class The fully qualified class name of the fixture class to
101
     *                      check modification date on.
102
     *
103
     * @return \DateTime|null
104
     */
105
    protected function getFixtureLastModified($class)
106
    {
107
        $lastModifiedDateTime = null;
108
109
        $reflClass = new \ReflectionClass($class);
110
        $classFileName = $reflClass->getFileName();
111
112
        if (file_exists($classFileName)) {
113
            $lastModifiedDateTime = new \DateTime();
114
            $lastModifiedDateTime->setTimestamp(filemtime($classFileName));
115
        }
116
117
        return $lastModifiedDateTime;
118
    }
119
120
    /**
121
     * Determine if the Fixtures that define a database backup have been
122
     * modified since the backup was made.
123
     *
124
     * @param array  $classNames The fixture classnames to check
125
     * @param string $backup     The fixture backup SQLite database file path
126
     *
127
     * @return bool TRUE if the backup was made since the modifications to the
128
     *              fixtures; FALSE otherwise
129
     */
130
    protected function isBackupUpToDate(array $classNames, $backup)
131
    {
132
        $backupLastModifiedDateTime = new \DateTime();
133
        $backupLastModifiedDateTime->setTimestamp(filemtime($backup));
134
135
        foreach ($classNames as &$className) {
136
            $fixtureLastModifiedDateTime = $this->getFixtureLastModified($className);
137
            if ($backupLastModifiedDateTime < $fixtureLastModifiedDateTime) {
138
                return false;
139
            }
140
        }
141
142
        return true;
143
    }
144
145
    /**
146
     * Copy SQLite backup file.
147
     *
148
     * @param ObjectManager            $om
149
     * @param string                   $executorClass
150
     * @param ProxyReferenceRepository $referenceRepository
151
     * @param string                   $backup              Path of the source file.
152
     * @param string                   $name                Path of the destination file.
153
     */
154
    private function copySqliteBackup($om, $executorClass,
155
                                      $referenceRepository, $backup, $name)
156
    {
157
        $om->flush();
158
        $om->clear();
159
160
        $this->preFixtureRestore($om, $referenceRepository);
161
162
        copy($backup, $name);
163
164
        $executor = new $executorClass($om);
165
        $executor->setReferenceRepository($referenceRepository);
166
        $executor->getReferenceRepository()->load($backup);
167
168
        $this->postFixtureRestore();
169
170
        return $executor;
171
    }
172
173
    /**
174
     * Purge database.
175
     *
176
     * @param ObjectManager            $om
177
     * @param string                   $type
178
     * @param int                      $purgeMode
179
     * @param string                   $executorClass
180
     * @param ProxyReferenceRepository $referenceRepository
181
     */
182
    private function purgeDatabase(ObjectManager $om, $type, $purgeMode,
183
                                   $executorClass,
184
                                   ProxyReferenceRepository $referenceRepository)
185
    {
186
        $purgerClass = 'Doctrine\\Common\\DataFixtures\\Purger\\'.$type.'Purger';
187
        if ('PHPCR' === $type) {
188
            $purger = new $purgerClass($om);
189
            $initManager = $this->container->has('doctrine_phpcr.initializer_manager')
190
                ? $this->container->get('doctrine_phpcr.initializer_manager')
191
                : null;
192
193
            $executor = new $executorClass($om, $purger, $initManager);
194
        } else {
195
            $purger = new $purgerClass();
196
            if (null !== $purgeMode) {
197
                $purger->setPurgeMode($purgeMode);
198
            }
199
200
            $executor = new $executorClass($om, $purger);
201
        }
202
203
        $executor->setReferenceRepository($referenceRepository);
204
        $executor->purge();
205
206
        return $executor;
207
    }
208
209
    /**
210
     * Purge database.
211
     *
212
     * @param ObjectManager            $om
213
     * @param array                    $metadatas
214
     * @param string                   $executorClass
215
     * @param ProxyReferenceRepository $referenceRepository
216
     */
217
    private function createSqliteSchema(ObjectManager $om,
218
                                        $metadatas, $executorClass,
219
                                        ProxyReferenceRepository $referenceRepository)
220
    {
221
        // TODO: handle case when using persistent connections. Fail loudly?
222
        $schemaTool = new SchemaTool($om);
0 ignored issues
show
Compatibility introduced by
$om of type object<Doctrine\Common\Persistence\ObjectManager> is not a sub-type of object<Doctrine\ORM\EntityManagerInterface>. It seems like you assume a child interface of the interface Doctrine\Common\Persistence\ObjectManager to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
223
        $schemaTool->dropDatabase();
224
        if (!empty($metadatas)) {
225
            $schemaTool->createSchema($metadatas);
226
        }
227
        $this->postFixtureSetup();
228
229
        $executor = new $executorClass($om);
230
        $executor->setReferenceRepository($referenceRepository);
231
    }
232
233
    /**
234
     * Set the database to the provided fixtures.
235
     *
236
     * Drops the current database and then loads fixtures using the specified
237
     * classes. The parameter is a list of fully qualified class names of
238
     * classes that implement Doctrine\Common\DataFixtures\FixtureInterface
239
     * so that they can be loaded by the DataFixtures Loader::addFixture
240
     *
241
     * When using SQLite this method will automatically make a copy of the
242
     * loaded schema and fixtures which will be restored automatically in
243
     * case the same fixture classes are to be loaded again. Caveat: changes
244
     * to references and/or identities may go undetected.
245
     *
246
     * Depends on the doctrine data-fixtures library being available in the
247
     * class path.
248
     *
249
     * @param array  $classNames   List of fully qualified class names of fixtures to load
250
     * @param string $omName       The name of object manager to use
251
     * @param string $registryName The service id of manager registry to use
252
     * @param int    $purgeMode    Sets the ORM purge mode
253
     *
254
     * @return null|AbstractExecutor
255
     */
256
    public function loadFixtures(array $classNames, $omName = null, $registryName = 'doctrine', $purgeMode = null)
257
    {
258
        /** @var ManagerRegistry $registry */
259
        $registry = $this->container->get($registryName);
260
        $om = $registry->getManager($omName);
261
        $type = $registry->getName();
262
263
        $executorClass = $this->getExecutorClass($type);
264
        $referenceRepository = new ProxyReferenceRepository($om);
265
266
        $cacheDriver = $om->getMetadataFactory()->getCacheDriver();
267
268
        if ($cacheDriver) {
269
            $cacheDriver->deleteAll();
270
        }
271
272
        if ('ORM' === $type) {
273
            $connection = $om->getConnection();
274
            if ($connection->getDriver() instanceof SqliteDriver) {
275
                $name = $this->getNameParameter($connection);
276
                $metadatas = $this->getCachedMetadatas($om, $omName);
277
278
                if ($this->container->getParameter('liip_functional_test.cache_sqlite_db')) {
279
                    $backup = $this->container->getParameter('kernel.cache_dir').'/test_'.md5(serialize($metadatas).serialize($classNames)).'.db';
280
                    if (file_exists($backup) && file_exists($backup.'.ser') && $this->isBackupUpToDate($classNames, $backup)) {
281
                        $executor = $this->copySqliteBackup($om,
282
                            $executorClass, $referenceRepository,
283
                            $backup, $name);
284
285
                        return $executor;
286
                    }
287
                }
288
289
                $this->createSqliteSchema($om, $metadatas,
290
                    $executorClass, $referenceRepository);
291
            }
292
        }
293
294
        if (empty($executor)) {
0 ignored issues
show
Bug introduced by
The variable $executor seems only to be defined at a later point. As such the call to empty() seems to always evaluate to true.

This check marks calls to isset(...) or empty(...) that are found before the variable itself is defined. These will always have the same result.

This is likely the result of code being shifted around. Consider removing these calls.

Loading history...
295
            $executor = $this->purgeDatabase($om, $type, $purgeMode,
296
                $executorClass, $referenceRepository);
297
        }
298
299
        $loader = $this->getFixtureLoader($classNames);
300
301
        $executor->execute($loader->getFixtures(), true);
302
303
        if (isset($name) && isset($backup)) {
304
            $this->preReferenceSave($om, $executor, $backup);
305
306
            $executor->getReferenceRepository()->save($backup);
307
            copy($name, $backup);
308
309
            $this->postReferenceSave($om, $executor, $backup);
310
        }
311
312
        return $executor;
313
    }
314
315
    /**
316
     * Clean database.
317
     *
318
     * @param ManagerRegistry $registry
319
     * @param EntityManager   $om
320
     */
321
    private function cleanDatabase(ManagerRegistry $registry, EntityManager $om)
322
    {
323
        $connection = $om->getConnection();
324
325
        $mysql = ($registry->getName() === 'ORM'
326
            && $connection->getDatabasePlatform() instanceof MySqlPlatform);
327
328
        if ($mysql) {
329
            $connection->query('SET FOREIGN_KEY_CHECKS=0');
330
        }
331
332
        $this->loadFixtures(array());
333
334
        if ($mysql) {
335
            $connection->query('SET FOREIGN_KEY_CHECKS=1');
336
        }
337
    }
338
339
    /**
340
     * Locate fixture files.
341
     *
342
     * @param array $paths
343
     *
344
     * @return array $files
345
     */
346
    private function locateResources($paths)
347
    {
348
        $files = array();
349
350
        $kernel = $this->container->get('kernel');
351
352
        foreach ($paths as $path) {
353
            if ($path[0] !== '@' && file_exists($path) === true) {
354
                $files[] = $path;
355
                continue;
356
            }
357
358
            $files[] = $kernel->locateResource($path);
359
        }
360
361
        return $files;
362
    }
363
364
    /**
365
     * @param array  $paths        Either symfony resource locators (@ BundleName/etc) or actual file paths
366
     * @param bool   $append
367
     * @param null   $omName
368
     * @param string $registryName
369
     *
370
     * @return array
371
     *
372
     * @throws \BadMethodCallException
373
     */
374
    public function loadFixtureFiles(array $paths = array(), $append = false, $omName = null, $registryName = 'doctrine')
375
    {
376
        if (!class_exists('Nelmio\Alice\Fixtures')) {
377
            throw new \BadMethodCallException('nelmio/alice should be installed to use this method.');
378
        }
379
380
        /** @var ManagerRegistry $registry */
381
        $registry = $this->container->get($registryName);
382
        /** @var EntityManager $om */
383
        $om = $registry->getManager($omName);
384
385
        if ($append === false) {
386
            $this->cleanDatabase($registry, $om);
387
        }
388
389
        $files = $this->locateResources($paths);
390
391
        return Fixtures::load($files, $om);
392
    }
393
394
    /**
395
     * Retrieve Doctrine DataFixtures loader.
396
     *
397
     * @param array $classNames
398
     *
399
     * @return Loader
400
     */
401
    protected function getFixtureLoader(array $classNames)
402
    {
403
        $loaderClass = class_exists('Symfony\Bridge\Doctrine\DataFixtures\ContainerAwareLoader')
404
            ? 'Symfony\Bridge\Doctrine\DataFixtures\ContainerAwareLoader'
405
            : (class_exists('Doctrine\Bundle\FixturesBundle\Common\DataFixtures\Loader')
406
                ? 'Doctrine\Bundle\FixturesBundle\Common\DataFixtures\Loader'
407
                : 'Symfony\Bundle\DoctrineFixturesBundle\Common\DataFixtures\Loader');
408
409
        $loader = new $loaderClass($this->container);
410
411
        foreach ($classNames as $className) {
412
            $this->loadFixtureClass($loader, $className);
413
        }
414
415
        return $loader;
416
    }
417
418
    /**
419
     * Load a data fixture class.
420
     *
421
     * @param Loader $loader
422
     * @param string $className
423
     */
424
    protected function loadFixtureClass($loader, $className)
425
    {
426
        $fixture = new $className();
427
428
        if ($loader->hasFixture($fixture)) {
429
            unset($fixture);
430
431
            return;
432
        }
433
434
        $loader->addFixture($fixture);
435
436
        if ($fixture instanceof DependentFixtureInterface) {
437
            foreach ($fixture->getDependencies() as $dependency) {
438
                $this->loadFixtureClass($loader, $dependency);
439
            }
440
        }
441
    }
442
443
    /**
444
     * Callback function to be executed after Schema creation.
445
     * Use this to execute acl:init or other things necessary.
446
     */
447
    protected function postFixtureSetup()
448
    {
449
    }
450
451
    /**
452
     * Callback function to be executed after Schema restore.
453
     *
454
     * @return WebTestCase
455
     */
456
    protected function postFixtureRestore()
457
    {
458
    }
459
460
    /**
461
     * Callback function to be executed before Schema restore.
462
     *
463
     * @param ObjectManager            $manager             The object manager
464
     * @param ProxyReferenceRepository $referenceRepository The reference repository
465
     *
466
     * @return WebTestCase
467
     */
468
    protected function preFixtureRestore(ObjectManager $manager, ProxyReferenceRepository $referenceRepository)
0 ignored issues
show
Unused Code introduced by
The parameter $manager is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $referenceRepository is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
469
    {
470
    }
471
472
    /**
473
     * Callback function to be executed after save of references.
474
     *
475
     * @param ObjectManager    $manager        The object manager
476
     * @param AbstractExecutor $executor       Executor of the data fixtures
477
     * @param string           $backupFilePath Path of file used to backup the references of the data fixtures
478
     *
479
     * @return WebTestCase
480
     */
481
    protected function postReferenceSave(ObjectManager $manager, AbstractExecutor $executor, $backupFilePath)
0 ignored issues
show
Unused Code introduced by
The parameter $manager is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $executor is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $backupFilePath is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
482
    {
483
    }
484
485
    /**
486
     * Callback function to be executed before save of references.
487
     *
488
     * @param ObjectManager    $manager        The object manager
489
     * @param AbstractExecutor $executor       Executor of the data fixtures
490
     * @param string           $backupFilePath Path of file used to backup the references of the data fixtures
491
     *
492
     * @return WebTestCase
493
     */
494
    protected function preReferenceSave(ObjectManager $manager, AbstractExecutor $executor, $backupFilePath)
0 ignored issues
show
Unused Code introduced by
The parameter $manager is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $executor is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $backupFilePath is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
495
    {
496
    }
497
}
498