Completed
Push — refactor-loadFixtures ( 5772b7...91c308 )
by Alexis
07:55
created

WebTestCase::cacheSqliteDatabase()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 19
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 6.2017

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 19
ccs 7
cts 11
cp 0.6364
rs 8.8571
cc 5
eloc 12
nc 3
nop 6
crap 6.2017
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\Test;
13
14
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase as BaseWebTestCase;
15
use Symfony\Bundle\FrameworkBundle\Console\Application;
16
use Symfony\Bundle\FrameworkBundle\Client;
17
use Symfony\Component\Console\Input\ArrayInput;
18
use Symfony\Component\Console\Output\OutputInterface;
19
use Symfony\Component\Console\Output\StreamOutput;
20
use Symfony\Component\DomCrawler\Crawler;
21
use Symfony\Component\BrowserKit\Cookie;
22
use Symfony\Component\HttpKernel\Kernel;
23
use Symfony\Component\HttpFoundation\Response;
24
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
25
use Symfony\Component\Security\Core\User\UserInterface;
26
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
27
use Symfony\Component\DependencyInjection\ContainerInterface;
28
use Symfony\Component\HttpFoundation\Session\Session;
29
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
30
use Symfony\Bridge\Doctrine\ManagerRegistry;
31
use Symfony\Bundle\DoctrineFixturesBundle\Common\DataFixtures\Loader;
32
use Doctrine\Common\Persistence\ObjectManager;
33
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
34
use Doctrine\Common\DataFixtures\Executor\AbstractExecutor;
35
use Doctrine\Common\DataFixtures\ProxyReferenceRepository;
36
use Doctrine\DBAL\Connection;
37
use Doctrine\DBAL\Driver\PDOSqlite\Driver as SqliteDriver;
38
use Doctrine\DBAL\Platforms\MySqlPlatform;
39
use Doctrine\ORM\Tools\SchemaTool;
40
use Nelmio\Alice\Fixtures;
41
42
/**
43
 * @author Lea Haensenberger
44
 * @author Lukas Kahwe Smith <[email protected]>
45
 * @author Benjamin Eberlei <[email protected]>
46
 */
47
abstract class WebTestCase extends BaseWebTestCase
48
{
49
    protected $environment = 'test';
50
    protected $containers;
51
    protected $kernelDir;
52
    // 5 * 1024 * 1024 KB
53
    protected $maxMemory = 5242880;
54
55
    // RUN COMMAND
56
    protected $verbosityLevel;
57
    protected $decorated;
58
59
    /**
60
     * @var array
61
     */
62
    private $firewallLogins = array();
63
64
    /**
65
     * @var array
66
     */
67
    private static $cachedMetadatas = array();
68
69 1
    protected static function getKernelClass()
0 ignored issues
show
Coding Style introduced by
getKernelClass uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
70
    {
71 1
        $dir = isset($_SERVER['KERNEL_DIR']) ? $_SERVER['KERNEL_DIR'] : self::getPhpUnitXmlDir();
72
73 1
        list($appname) = explode('\\', get_called_class());
74
75 1
        $class = $appname.'Kernel';
76 1
        $file = $dir.'/'.strtolower($appname).'/'.$class.'.php';
77 1
        if (!file_exists($file)) {
78 1
            return parent::getKernelClass();
79
        }
80
        require_once $file;
81
82
        return $class;
83
    }
84
85
    /**
86
     * Creates a mock object of a service identified by its id.
87
     *
88
     * @param string $id
89
     *
90
     * @return \PHPUnit_Framework_MockObject_MockBuilder
91
     */
92 2
    protected function getServiceMockBuilder($id)
93
    {
94
        $service = $this->getContainer()->get($id);
95 2
        $class = get_class($service);
96
97
        return $this->getMockBuilder($class)->disableOriginalConstructor();
98
    }
99
100
    /**
101
     * Builds up the environment to run the given command.
102
     *
103
     * @param string $name
104
     * @param array  $params
105
     * @param bool   $reuseKernel
106
     *
107
     * @return string
108
     */
109 12
    protected function runCommand($name, array $params = array(), $reuseKernel = false)
110
    {
111 12
        array_unshift($params, $name);
112
113 12
        if (!$reuseKernel) {
114 12
            if (null !== static::$kernel) {
115 9
                static::$kernel->shutdown();
116 9
            }
117
118 12
            $kernel = static::$kernel = $this->createKernel(array('environment' => $this->environment));
119 12
            $kernel->boot();
120 12
        } else {
121 2
            $kernel = $this->getContainer()->get('kernel');
122
        }
123
124 12
        $application = new Application($kernel);
125 12
        $application->setAutoExit(false);
126
127
        // @codeCoverageIgnoreStart
128
        if ('20301' === Kernel::VERSION_ID) {
129
            $params = $this->configureVerbosityForSymfony20301($params);
130
        }
131
        // @codeCoverageIgnoreEnd
132
133 12
        $input = new ArrayInput($params);
134 12
        $input->setInteractive(false);
135
136 12
        $fp = fopen('php://temp/maxmemory:'.$this->maxMemory, 'r+');
137 12
        $output = new StreamOutput($fp, $this->getVerbosityLevel(), $this->getDecorated());
138
139 11
        $application->run($input, $output);
140
141 11
        rewind($fp);
142
143 11
        return stream_get_contents($fp);
144
    }
145
146
    /**
147
     * Retrieves the output verbosity level.
148
     *
149
     * @see Symfony\Component\Console\Output\OutputInterface for available levels
150
     *
151
     * @return int
152
     *
153
     * @throws \OutOfBoundsException If the set value isn't accepted
154
     */
155 12
    protected function getVerbosityLevel()
156
    {
157
        // If `null`, is not yet set
158 12
        if (null === $this->verbosityLevel) {
159
            // Set the global verbosity level that is set as NORMAL by the TreeBuilder in Configuration
160 6
            $level = strtoupper($this->getContainer()->getParameter('liip_functional_test.command_verbosity'));
161 6
            $verbosity = '\Symfony\Component\Console\Output\StreamOutput::VERBOSITY_'.$level;
162
163 6
            $this->verbosityLevel = constant($verbosity);
164 6
        }
165
166
        // If string, it is set by the developer, so check that the value is an accepted one
167 12
        if (is_string($this->verbosityLevel)) {
168 6
            $level = strtoupper($this->verbosityLevel);
169 6
            $verbosity = '\Symfony\Component\Console\Output\StreamOutput::VERBOSITY_'.$level;
170
171 6
            if (!defined($verbosity)) {
172 1
                throw new \OutOfBoundsException(
173 1
                    sprintf('The set value "%s" for verbosityLevel is not valid. Accepted are: "quiet", "normal", "verbose", "very_verbose" and "debug".', $level)
174 1
                    );
175
            }
176
177 5
            $this->verbosityLevel = constant($verbosity);
178 5
        }
179
180 11
        return $this->verbosityLevel;
181
    }
182
183
    /**
184
     * In Symfony 2.3.1 the verbosity level has to be set through {Symfony\Component\Console\Input\ArrayInput} and not
185
     * in {Symfony\Component\Console\Output\OutputInterface}.
186
     *
187
     * This method builds $params to be passed to {Symfony\Component\Console\Input\ArrayInput}.
188
     *
189
     * @codeCoverageIgnore
190
     *
191
     * @param array $params
192
     *
193
     * @return array
194
     */
195
    private function configureVerbosityForSymfony20301(array $params)
196
    {
197
        switch ($this->getVerbosityLevel()) {
198
            case OutputInterface::VERBOSITY_QUIET:
199
                $params['-q'] = '-q';
200
                break;
201
202
            case OutputInterface::VERBOSITY_VERBOSE:
203
                $params['-v'] = '';
204
                break;
205
206
            case OutputInterface::VERBOSITY_VERY_VERBOSE:
207
                $params['-vv'] = '';
208
                break;
209
210
            case OutputInterface::VERBOSITY_DEBUG:
211
                $params['-vvv'] = '';
212
                break;
213
        }
214
215
        return $params;
216
    }
217
218 6
    public function setVerbosityLevel($level)
219
    {
220 6
        $this->verbosityLevel = $level;
221 6
    }
222
223
    /**
224
     * Retrieves the flag indicating if the output should be decorated or not.
225
     *
226
     * @return bool
227
     */
228 11
    protected function getDecorated()
229
    {
230 11
        if (null === $this->decorated) {
231
            // Set the global decoration flag that is set to `true` by the TreeBuilder in Configuration
232 5
            $this->decorated = $this->getContainer()->getParameter('liip_functional_test.command_decoration');
233 5
        }
234
235
        // Check the local decorated flag
236 11
        if (false === is_bool($this->decorated)) {
237
            throw new \OutOfBoundsException(
238
                sprintf('`WebTestCase::decorated` has to be `bool`. "%s" given.', gettype($this->decorated))
239
            );
240
        }
241
242 11
        return $this->decorated;
243
    }
244
245 6
    public function isDecorated($decorated)
246
    {
247 6
        $this->decorated = $decorated;
248 6
    }
249
250
    /**
251
     * Get an instance of the dependency injection container.
252
     * (this creates a kernel *without* parameters).
253
     *
254
     * @return ContainerInterface
255
     */
256 34
    protected function getContainer()
0 ignored issues
show
Coding Style introduced by
getContainer uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
257
    {
258 34
        if (!empty($this->kernelDir)) {
259
            $tmpKernelDir = isset($_SERVER['KERNEL_DIR']) ? $_SERVER['KERNEL_DIR'] : null;
260
            $_SERVER['KERNEL_DIR'] = getcwd().$this->kernelDir;
261
        }
262
263 34
        $cacheKey = $this->kernelDir.'|'.$this->environment;
264 34
        if (empty($this->containers[$cacheKey])) {
265
            $options = array(
266 34
                'environment' => $this->environment,
267 34
            );
268 34
            $kernel = $this->createKernel($options);
269 34
            $kernel->boot();
270
271 34
            $this->containers[$cacheKey] = $kernel->getContainer();
272 34
        }
273
274 34
        if (isset($tmpKernelDir)) {
275
            $_SERVER['KERNEL_DIR'] = $tmpKernelDir;
276
        }
277
278 34
        return $this->containers[$cacheKey];
279
    }
280
281
    /**
282
     * @param string $type
283
     *
284
     * @return string
285
     */
286 26
    private function getExecutorClass($type)
287
    {
288 26
        return 'PHPCR' === $type && class_exists('Doctrine\Bundle\PHPCRBundle\DataFixtures\PHPCRExecutor')
289 26
            ? 'Doctrine\Bundle\PHPCRBundle\DataFixtures\PHPCRExecutor'
290 26
            : 'Doctrine\\Common\\DataFixtures\\Executor\\'.$type.'Executor';
291
    }
292
293
    /**
294
     * Get file path of the SQLite database.
295
     *
296
     * @param Connection $connection
297
     *
298
     * @return string $name
299
     */
300 21
    private function getNameParameter(Connection $connection)
301
    {
302 21
        $params = $connection->getParams();
303
304 21
        if (isset($params['master'])) {
305
            $params = $params['master'];
306
        }
307
308 21
        $name = isset($params['path']) ? $params['path'] : (isset($params['dbname']) ? $params['dbname'] : false);
309
310 21
        if (!$name) {
311
            throw new \InvalidArgumentException("Connection does not contain a 'path' or 'dbname' parameter and cannot be dropped.");
312
        }
313
314 21
        return $name;
315
    }
316
317
    /**
318
     * Purge SQLite database.
319
     *
320
     * @param ObjectManager $om
321
     * @param string        $omName The name of object manager to use
322
     */
323 21
    private function getCachedMetadatas(ObjectManager $om, $omName)
324
    {
325 21
        if (!isset(self::$cachedMetadatas[$omName])) {
326 6
            self::$cachedMetadatas[$omName] = $om->getMetadataFactory()->getAllMetadata();
327
            usort(self::$cachedMetadatas[$omName], function ($a, $b) { return strcmp($a->name, $b->name); });
328 6
        }
329
330 21
        return self::$cachedMetadatas[$omName];
331
    }
332
333
    /**
334
     * This function finds the time when the data blocks of a class definition
335
     * file were being written to, that is, the time when the content of the
336
     * file was changed.
337
     *
338
     * @param string $class The fully qualified class name of the fixture class to
339
     *                      check modification date on.
340
     *
341
     * @return \DateTime|null
342
     */
343
    protected function getFixtureLastModified($class)
344
    {
345
        $lastModifiedDateTime = null;
346
347
        $reflClass = new \ReflectionClass($class);
348
        $classFileName = $reflClass->getFileName();
349
350
        if (file_exists($classFileName)) {
351
            $lastModifiedDateTime = new \DateTime();
352
            $lastModifiedDateTime->setTimestamp(filemtime($classFileName));
353
        }
354
355
        return $lastModifiedDateTime;
356
    }
357
358
    /**
359
     * Determine if the Fixtures that define a database backup have been
360
     * modified since the backup was made.
361
     *
362
     * @param array  $classNames The fixture classnames to check
363
     * @param string $backup     The fixture backup SQLite database file path
364
     *
365
     * @return bool TRUE if the backup was made since the modifications to the
366
     *              fixtures; FALSE otherwise
367
     */
368
    protected function isBackupUpToDate(array $classNames, $backup)
369
    {
370
        $backupLastModifiedDateTime = new \DateTime();
371
        $backupLastModifiedDateTime->setTimestamp(filemtime($backup));
372
373
        foreach ($classNames as &$className) {
374
            $fixtureLastModifiedDateTime = $this->getFixtureLastModified($className);
375
            if ($backupLastModifiedDateTime < $fixtureLastModifiedDateTime) {
376
                return false;
377
            }
378
        }
379
380
        return true;
381
    }
382
383
    /**
384
     * Copy SQLite backup file.
385
     *
386
     * @param ObjectManager            $om
387
     * @param string                   $executorClass
388
     * @param ProxyReferenceRepository $referenceRepository
389
     * @param string                   $backup              Path of the source file.
390
     * @param string                   $name                Path of the destination file.
391
     */
392
    private function copySqliteBackup($om, $executorClass,
393
        $referenceRepository, $backup, $name)
394
    {
395
        $om->flush();
396
        $om->clear();
397
398
        $this->preFixtureRestore($om, $referenceRepository);
399
400
        copy($backup, $name);
401
402
        $executor = new $executorClass($om);
403
        $executor->setReferenceRepository($referenceRepository);
404
        $executor->getReferenceRepository()->load($backup);
405
406
        $this->postFixtureRestore();
407
408
        return $executor;
409
    }
410
411
    /**
412
     * Prepare the SQLite database.
413
     *
414
     * @param ObjectManager            $om
415
     * @param array                    $metadatas
416
     * @param array                    $classNames
417
     * @param string                   $executorClass
418
     * @param ProxyReferenceRepository $referenceRepository
419
     * @param string                   $name
420
     */
421 21
    private function cacheSqliteDatabase($om, $metadatas,
422
                                         $classNames, $executorClass,
423
                                         ProxyReferenceRepository $referenceRepository, $name)
424
    {
425 21
        $container = $this->getContainer();
426
427 21
        if ($container->getParameter('liip_functional_test.cache_sqlite_db')) {
428 5
            $backup = $container->getParameter('kernel.cache_dir').'/test_'.md5(serialize($metadatas).serialize($classNames)).'.db';
429 5
            if (file_exists($backup) && file_exists($backup.'.ser') && $this->isBackupUpToDate($classNames, $backup)) {
430
                $executor = $this->copySqliteBackup($om,
431
                    $executorClass, $referenceRepository,
432
                    $backup, $name);
433
434
                return $executor;
435
            }
436 5
        }
437
438 21
        return null;
439
    }
440
441
    /**
442
     * Purge database.
443
     *
444
     * @param ObjectManager            $om
445
     * @param string                   $name
446
     * @param array                    $metadatas
447
     * @param string                   $executorClass
448
     * @param ProxyReferenceRepository $referenceRepository
449
     */
450 21
    private function createSqliteSchema(ObjectManager $om, $name,
451
        $metadatas, $executorClass,
452
        ProxyReferenceRepository $referenceRepository)
453
    {
454
        // TODO: handle case when using persistent connections. Fail loudly?
455 21
        $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...
456 21
        $schemaTool->dropDatabase($name);
0 ignored issues
show
Unused Code introduced by
The call to SchemaTool::dropDatabase() has too many arguments starting with $name.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
457 21
        if (!empty($metadatas)) {
458 21
            $schemaTool->createSchema($metadatas);
459 21
        }
460 21
        $this->postFixtureSetup();
461
462 21
        $executor = new $executorClass($om);
463 21
        $executor->setReferenceRepository($referenceRepository);
464
465 21
        return $executor;
466
    }
467
468
    /**
469
     * Purge database.
470
     *
471
     * @param ObjectManager            $om
472
     * @param string                   $type
473
     * @param int                      $purgeMode
474
     * @param string                   $executorClass
475
     * @param ProxyReferenceRepository $referenceRepository
476
     */
477 5
    private function purgeDatabase(ObjectManager $om, $type, $purgeMode,
478
        $executorClass,
479
        ProxyReferenceRepository $referenceRepository)
480
    {
481 5
        $container = $this->getContainer();
482
483 5
        $purgerClass = 'Doctrine\\Common\\DataFixtures\\Purger\\'.$type.'Purger';
484 5
        if ('PHPCR' === $type) {
485 1
            $purger = new $purgerClass($om);
486 1
            $initManager = $container->has('doctrine_phpcr.initializer_manager')
487 1
                ? $container->get('doctrine_phpcr.initializer_manager')
488 1
                : null;
489
490 1
            $executor = new $executorClass($om, $purger, $initManager);
491 1
        } else {
492 4
            $purger = new $purgerClass();
493 4
            if (null !== $purgeMode) {
494 1
                $purger->setPurgeMode($purgeMode);
495 1
            }
496
497 4
            $executor = new $executorClass($om, $purger);
498
        }
499
500 5
        $executor->setReferenceRepository($referenceRepository);
501 5
        $executor->purge();
502
503 5
        return $executor;
504
    }
505
506
    /**
507
     * Set the database to the provided fixtures.
508
     *
509
     * Drops the current database and then loads fixtures using the specified
510
     * classes. The parameter is a list of fully qualified class names of
511
     * classes that implement Doctrine\Common\DataFixtures\FixtureInterface
512
     * so that they can be loaded by the DataFixtures Loader::addFixture
513
     *
514
     * When using SQLite this method will automatically make a copy of the
515
     * loaded schema and fixtures which will be restored automatically in
516
     * case the same fixture classes are to be loaded again. Caveat: changes
517
     * to references and/or identities may go undetected.
518
     *
519
     * Depends on the doctrine data-fixtures library being available in the
520
     * class path.
521
     *
522
     * @param array  $classNames   List of fully qualified class names of fixtures to load
523
     * @param string $omName       The name of object manager to use
524
     * @param string $registryName The service id of manager registry to use
525
     * @param int    $purgeMode    Sets the ORM purge mode
526
     *
527
     * @return null|AbstractExecutor
528
     */
529 26
    protected function loadFixtures(array $classNames, $omName = null, $registryName = 'doctrine', $purgeMode = null)
530
    {
531 26
        $container = $this->getContainer();
532
        /** @var ManagerRegistry $registry */
533 26
        $registry = $container->get($registryName);
534 26
        $om = $registry->getManager($omName);
535 26
        $type = $registry->getName();
536
537 26
        $executorClass = $this->getExecutorClass($type);
538 26
        $referenceRepository = new ProxyReferenceRepository($om);
539
540 26
        $cacheDriver = $om->getMetadataFactory()->getCacheDriver();
541
542 26
        if ($cacheDriver) {
543 26
            $cacheDriver->deleteAll();
544 26
        }
545
546 26
        if ('ORM' === $type) {
547 25
            $connection = $om->getConnection();
548 25
            if ($connection->getDriver() instanceof SqliteDriver) {
549 21
                $name = $this->getNameParameter($connection);
550 21
                $metadatas = $this->getCachedMetadatas($om, $omName);
551
552 21
                $executor = $this->cacheSqliteDatabase($om, $omName, $classNames,
0 ignored issues
show
Documentation introduced by
$omName is of type string|null, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
553 21
                    $executorClass, $referenceRepository, $name);
554
555 21
                if ($executor !== null) {
556
                    return $executor;
557
                }
558
559 21
                $executor = $this->createSqliteSchema($om, $name, $metadatas,
560 21
                    $executorClass, $referenceRepository);
561 21
            }
562 25
        }
563
564 26
        if (empty($executor)) {
565 5
            $executor = $this->purgeDatabase($om, $type, $purgeMode,
566 5
                $executorClass, $referenceRepository);
567 5
        }
568
569 26
        $loader = $this->getFixtureLoader($container, $classNames);
570
571 26
        $executor->execute($loader->getFixtures(), true);
572
573 26
        if (isset($name) && isset($backup)) {
0 ignored issues
show
Bug introduced by
The variable $backup seems to never exist, and therefore isset should always return false. Did you maybe rename this variable?

This check looks for calls to isset(...) or empty() on variables that are yet undefined. These calls will always produce the same result and can be removed.

This is most likely caused by the renaming of a variable or the removal of a function/method parameter.

Loading history...
574
            $this->preReferenceSave($om, $executor, $backup);
575
576
            $executor->getReferenceRepository()->save($backup);
577
            copy($name, $backup);
578
579
            $this->postReferenceSave($om, $executor, $backup);
580
        }
581
582 26
        return $executor;
583
    }
584
585
    /**
586
     * @param array  $paths        Either symfony resource locators (@ BundleName/etc) or actual file paths
587
     * @param bool   $append
588
     * @param null   $omName
589
     * @param string $registryName
590
     *
591
     * @return array
592
     *
593
     * @throws \BadMethodCallException
594
     */
595 3
    public function loadFixtureFiles(array $paths = array(), $append = false, $omName = null, $registryName = 'doctrine')
596
    {
597 3
        if (!class_exists('Nelmio\Alice\Fixtures')) {
598
            throw new \BadMethodCallException('nelmio/alice should be installed to use this method.');
599
        }
600
601
        /** @var ManagerRegistry $registry */
602 3
        $registry = $this->getContainer()->get($registryName);
603 3
        $om = $registry->getManager($omName);
604
605 3
        if ($append === false) {
606
            //Clean database
607 3
            $connection = $om->getConnection();
608 3
            if ($registry->getName() === 'ORM' && $connection->getDatabasePlatform() instanceof MySqlPlatform) {
609 1
                $connection->query('SET FOREIGN_KEY_CHECKS=0');
610 1
            }
611
612 3
            $this->loadFixtures(array());
613
614 3
            if ($registry->getName() === 'ORM' && $connection->getDatabasePlatform() instanceof MySqlPlatform) {
615 1
                $connection->query('SET FOREIGN_KEY_CHECKS=1');
616 1
            }
617 3
        }
618
619 3
        $files = array();
620 3
        $kernel = $this->getContainer()->get('kernel');
621 3
        foreach ($paths as $path) {
622 3
            if ($path[0] !== '@' && file_exists($path) === true) {
623 1
                $files[] = $path;
624 1
                continue;
625
            }
626
627 2
            $files[] = $kernel->locateResource($path);
628 3
        }
629
630 3
        return Fixtures::load($files, $om);
631
    }
632
633
    /**
634
     * Callback function to be executed after Schema creation.
635
     * Use this to execute acl:init or other things necessary.
636
     */
637 21
    protected function postFixtureSetup()
638
    {
639 21
    }
640
641
    /**
642
     * Callback function to be executed after Schema restore.
643
     *
644
     * @return WebTestCase
645
     */
646
    protected function postFixtureRestore()
647
    {
648
    }
649
650
    /**
651
     * Callback function to be executed before Schema restore.
652
     *
653
     * @param ObjectManager            $manager             The object manager
654
     * @param ProxyReferenceRepository $referenceRepository The reference repository
655
     *
656
     * @return WebTestCase
657
     */
658
    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...
659
    {
660
    }
661
662
    /**
663
     * Callback function to be executed after save of references.
664
     *
665
     * @param ObjectManager    $manager        The object manager
666
     * @param AbstractExecutor $executor       Executor of the data fixtures
667
     * @param string           $backupFilePath Path of file used to backup the references of the data fixtures
668
     *
669
     * @return WebTestCase
670
     */
671
    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...
672
    {
673
    }
674
675
    /**
676
     * Callback function to be executed before save of references.
677
     *
678
     * @param ObjectManager    $manager        The object manager
679
     * @param AbstractExecutor $executor       Executor of the data fixtures
680
     * @param string           $backupFilePath Path of file used to backup the references of the data fixtures
681
     *
682
     * @return WebTestCase
683
     */
684
    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...
685
    {
686
    }
687
688
    /**
689
     * Retrieve Doctrine DataFixtures loader.
690
     *
691
     * @param ContainerInterface $container
692
     * @param array              $classNames
693
     *
694
     * @return Loader
695
     */
696 26
    protected function getFixtureLoader(ContainerInterface $container, array $classNames)
697
    {
698 26
        $loaderClass = class_exists('Symfony\Bridge\Doctrine\DataFixtures\ContainerAwareLoader')
699 26
            ? 'Symfony\Bridge\Doctrine\DataFixtures\ContainerAwareLoader'
700 26
            : (class_exists('Doctrine\Bundle\FixturesBundle\Common\DataFixtures\Loader')
701
                ? 'Doctrine\Bundle\FixturesBundle\Common\DataFixtures\Loader'
702 26
                : 'Symfony\Bundle\DoctrineFixturesBundle\Common\DataFixtures\Loader');
703
704 26
        $loader = new $loaderClass($container);
705
706 26
        foreach ($classNames as $className) {
707 8
            $this->loadFixtureClass($loader, $className);
708 26
        }
709
710 26
        return $loader;
711
    }
712
713
    /**
714
     * Load a data fixture class.
715
     *
716
     * @param Loader $loader
717
     * @param string $className
718
     */
719 8
    protected function loadFixtureClass($loader, $className)
720
    {
721 8
        $fixture = new $className();
722
723 8
        if ($loader->hasFixture($fixture)) {
724
            unset($fixture);
725
726
            return;
727
        }
728
729 8
        $loader->addFixture($fixture);
730
731 8
        if ($fixture instanceof DependentFixtureInterface) {
732
            foreach ($fixture->getDependencies() as $dependency) {
733
                $this->loadFixtureClass($loader, $dependency);
734
            }
735
        }
736 8
    }
737
738
    /**
739
     * Creates an instance of a lightweight Http client.
740
     *
741
     * If $authentication is set to 'true' it will use the content of
742
     * 'liip_functional_test.authentication' to log in.
743
     *
744
     * $params can be used to pass headers to the client, note that they have
745
     * to follow the naming format used in $_SERVER.
746
     * Example: 'HTTP_X_REQUESTED_WITH' instead of 'X-Requested-With'
747
     *
748
     * @param bool|array $authentication
749
     * @param array      $params
750
     *
751
     * @return Client
752
     */
753 46
    protected function makeClient($authentication = false, array $params = array())
754
    {
755 46
        if ($authentication) {
756 2
            if ($authentication === true) {
757
                $authentication = array(
758 1
                    'username' => $this->getContainer()
759 1
                        ->getParameter('liip_functional_test.authentication.username'),
760 1
                    'password' => $this->getContainer()
761 1
                        ->getParameter('liip_functional_test.authentication.password'),
762 1
                );
763 1
            }
764
765 2
            $params = array_merge($params, array(
766 2
                'PHP_AUTH_USER' => $authentication['username'],
767 2
                'PHP_AUTH_PW' => $authentication['password'],
768 2
            ));
769 2
        }
770
771 46
        $client = static::createClient(array('environment' => $this->environment), $params);
772
773 46
        if ($this->firewallLogins) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->firewallLogins of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
774
            // has to be set otherwise "hasPreviousSession" in Request returns false.
775 2
            $options = $client->getContainer()->getParameter('session.storage.options');
776
777 2
            if (!$options || !isset($options['name'])) {
778
                throw new \InvalidArgumentException('Missing session.storage.options#name');
779
            }
780
781 2
            $session = $client->getContainer()->get('session');
782
            // Since the namespace of the session changed in symfony 2.1, instanceof can be used to check the version.
783 2
            if ($session instanceof Session) {
784 2
                $session->setId(uniqid());
785 2
            }
786
787 2
            $client->getCookieJar()->set(new Cookie($options['name'], $session->getId()));
788
789
            /** @var $user UserInterface */
790 2
            foreach ($this->firewallLogins as $firewallName => $user) {
791 2
                $token = $this->createUserToken($user, $firewallName);
792
793
                // BC: security.token_storage is available on Symfony 2.6+
794
                // see http://symfony.com/blog/new-in-symfony-2-6-security-component-improvements
795 2
                if ($client->getContainer()->has('security.token_storage')) {
796 2
                    $tokenStorage = $client->getContainer()->get('security.token_storage');
797 2
                } else {
798
                    // This block will never be reached with Symfony 2.6+
799
                    // @codeCoverageIgnoreStart
800
                    $tokenStorage = $client->getContainer()->get('security.context');
801
                    // @codeCoverageIgnoreEnd
802
                }
803
804 2
                $tokenStorage->setToken($token);
805 2
                $session->set('_security_'.$firewallName, serialize($token));
806 2
            }
807
808 2
            $session->save();
809 2
        }
810
811 46
        return $client;
812
    }
813
814
    /**
815
     * Create User Token.
816
     *
817
     * Factory method for creating a User Token object for the firewall based on
818
     * the user object provided. By default it will be a Username/Password
819
     * Token based on the user's credentials, but may be overridden for custom
820
     * tokens in your applications.
821
     *
822
     * @param UserInterface $user         The user object to base the token off of
823
     * @param string        $firewallName name of the firewall provider to use
824
     *
825
     * @return TokenInterface The token to be used in the security context
826
     */
827 2
    protected function createUserToken(UserInterface $user, $firewallName)
828
    {
829 2
        return new UsernamePasswordToken(
830 2
            $user,
831 2
            null,
832 2
            $firewallName,
833 2
            $user->getRoles()
834 2
        );
835
    }
836
837
    /**
838
     * Extracts the location from the given route.
839
     *
840
     * @param string $route    The name of the route
841
     * @param array  $params   Set of parameters
842
     * @param bool   $absolute
843
     *
844
     * @return string
845
     */
846 1
    protected function getUrl($route, $params = array(), $absolute = UrlGeneratorInterface::ABSOLUTE_PATH)
847
    {
848 1
        return $this->getContainer()->get('router')->generate($route, $params, $absolute);
849
    }
850
851
    /**
852
     * Checks the success state of a response.
853
     *
854
     * @param Response $response Response object
855
     * @param bool     $success  to define whether the response is expected to be successful
856
     * @param string   $type
857
     */
858 5
    public function isSuccessful(Response $response, $success = true, $type = 'text/html')
859
    {
860
        try {
861 5
            $crawler = new Crawler();
862 5
            $crawler->addContent($response->getContent(), $type);
863 5
            if (!count($crawler->filter('title'))) {
864 1
                $title = '['.$response->getStatusCode().'] - '.$response->getContent();
865 1
            } else {
866 4
                $title = $crawler->filter('title')->text();
867
            }
868 5
        } catch (\Exception $e) {
869
            $title = $e->getMessage();
870
        }
871
872 5
        if ($success) {
873 4
            $this->assertTrue($response->isSuccessful(), 'The Response was not successful: '.$title);
874 4
        } else {
875 1
            $this->assertFalse($response->isSuccessful(), 'The Response was successful: '.$title);
876
        }
877 5
    }
878
879
    /**
880
     * Executes a request on the given url and returns the response contents.
881
     *
882
     * This method also asserts the request was successful.
883
     *
884
     * @param string $path           path of the requested page
885
     * @param string $method         The HTTP method to use, defaults to GET
886
     * @param bool   $authentication Whether to use authentication, defaults to false
887
     * @param bool   $success        to define whether the response is expected to be successful
888
     *
889
     * @return string
890
     */
891 1
    public function fetchContent($path, $method = 'GET', $authentication = false, $success = true)
892
    {
893 1
        $client = $this->makeClient($authentication);
894 1
        $client->request($method, $path);
895
896 1
        $content = $client->getResponse()->getContent();
897 1
        if (is_bool($success)) {
898 1
            $this->isSuccessful($client->getResponse(), $success);
899 1
        }
900
901 1
        return $content;
902
    }
903
904
    /**
905
     * Executes a request on the given url and returns a Crawler object.
906
     *
907
     * This method also asserts the request was successful.
908
     *
909
     * @param string $path           path of the requested page
910
     * @param string $method         The HTTP method to use, defaults to GET
911
     * @param bool   $authentication Whether to use authentication, defaults to false
912
     * @param bool   $success        Whether the response is expected to be successful
913
     *
914
     * @return Crawler
915
     */
916 1
    public function fetchCrawler($path, $method = 'GET', $authentication = false, $success = true)
917
    {
918 1
        $client = $this->makeClient($authentication);
919 1
        $crawler = $client->request($method, $path);
920
921 1
        $this->isSuccessful($client->getResponse(), $success);
922
923 1
        return $crawler;
924
    }
925
926
    /**
927
     * @param UserInterface $user
928
     *
929
     * @return WebTestCase
930
     */
931 2
    public function loginAs(UserInterface $user, $firewallName)
932
    {
933 2
        $this->firewallLogins[$firewallName] = $user;
934
935 2
        return $this;
936
    }
937
938
    /**
939
     * Asserts that the HTTP response code of the last request performed by
940
     * $client matches the expected code. If not, raises an error with more
941
     * information.
942
     *
943
     * @param $expectedStatusCode
944
     * @param Client $client
945
     */
946 9
    public function assertStatusCode($expectedStatusCode, Client $client)
947
    {
948 9
        $helpfulErrorMessage = null;
949
950 9
        if ($expectedStatusCode !== $client->getResponse()->getStatusCode()) {
951
            // Get a more useful error message, if available
952
            if ($exception = $client->getContainer()->get('liip_functional_test.exception_listener')->getLastException()) {
953
                $helpfulErrorMessage = $exception->getMessage();
954
            } elseif (count($validationErrors = $client->getContainer()->get('liip_functional_test.validator')->getLastErrors())) {
955
                $helpfulErrorMessage = "Unexpected validation errors:\n";
956
957
                foreach ($validationErrors as $error) {
958
                    $helpfulErrorMessage .= sprintf("+ %s: %s\n", $error->getPropertyPath(), $error->getMessage());
959
                }
960
            } else {
961
                $helpfulErrorMessage = substr($client->getResponse(), 0, 200);
962
            }
963
        }
964
965 9
        self::assertEquals($expectedStatusCode, $client->getResponse()->getStatusCode(), $helpfulErrorMessage);
966 9
    }
967
968
    /**
969
     * Assert that the last validation errors within $container match the
970
     * expected keys.
971
     *
972
     * @param array              $expected  A flat array of field names
973
     * @param ContainerInterface $container
974
     */
975 2
    public function assertValidationErrors(array $expected, ContainerInterface $container)
976
    {
977 2
        self::assertThat(
978 2
            $container->get('liip_functional_test.validator')->getLastErrors(),
979 2
            new ValidationErrorsConstraint($expected),
980
            'Validation errors should match.'
981 2
        );
982 1
    }
983
}
984