Completed
Pull Request — master (#4852)
by
unknown
15:00
created

Symfony::getKernelClass()   C

Complexity

Conditions 7
Paths 10

Size

Total Lines 49
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 31
nc 10
nop 0
dl 0
loc 49
rs 6.7272
c 0
b 0
f 0
1
<?php
2
namespace Codeception\Module;
3
4
use Codeception\Configuration;
5
use Codeception\Lib\Framework;
6
use Codeception\Exception\ModuleRequireException;
7
use Codeception\Lib\Connector\Symfony as SymfonyConnector;
8
use Codeception\Lib\Interfaces\DoctrineProvider;
9
use Codeception\Lib\Interfaces\PartedModule;
10
use Symfony\Component\Finder\Finder;
11
use Symfony\Component\DependencyInjection\ContainerInterface;
12
use Symfony\Component\Finder\SplFileInfo;
13
use Symfony\Component\VarDumper\Cloner\Data;
14
15
/**
16
 * This module uses Symfony Crawler and HttpKernel to emulate requests and test response.
17
 *
18
 * ## Demo Project
19
 *
20
 * <https://github.com/Codeception/symfony-demo>
21
 *
22
 * ## Config
23
 *
24
 * ### Symfony 4.x
25
 *
26
 * * app_path: 'src' - in Symfony 4 Kernel is located inside `src`
27
 * * environment: 'local' - environment used for load kernel
28
 * * em_service: 'doctrine.orm.entity_manager' - use the stated EntityManager to pair with Doctrine Module.
29
 * * debug: true - turn on/off debug mode
30
 * * cache_router: 'false' - enable router caching between tests in order to [increase performance](http://lakion.com/blog/how-did-we-speed-up-sylius-behat-suite-with-blackfire)
31
 * * rebootable_client: 'true' - reboot client's kernel before each request
32
 *
33
 * #### Example (`functional.suite.yml`) - Symfony 4 Directory Structure
34
 *
35
 *     modules:
36
 *        enabled:
37
 *           - Symfony:
38
 *               app_path: 'src'
39
 *               environment: 'test'
40
 *
41
 *
42
 * ### Symfony 3.x
43
 *
44
 * * app_path: 'app' - specify custom path to your app dir, where the kernel interface is located.
45
 * * var_path: 'var' - specify custom path to your var dir, where bootstrap cache is located.
46
 * * environment: 'local' - environment used for load kernel
47
 * * em_service: 'doctrine.orm.entity_manager' - use the stated EntityManager to pair with Doctrine Module.
48
 * * debug: true - turn on/off debug mode
49
 * * cache_router: 'false' - enable router caching between tests in order to [increase performance](http://lakion.com/blog/how-did-we-speed-up-sylius-behat-suite-with-blackfire)
50
 * * rebootable_client: 'true' - reboot client's kernel before each request
51
 *
52
 * #### Example (`functional.suite.yml`) - Symfony 3 Directory Structure
53
 *
54
 *     modules:
55
 *        enabled:
56
 *           - Symfony:
57
 *               app_path: 'app/front'
58
 *               var_path: 'var'
59
 *               environment: 'local_test'
60
 *
61
 *
62
 * ### Symfony 2.x
63
 *
64
 * * app_path: 'app' - specify custom path to your app dir, where bootstrap cache and kernel interface is located.
65
 * * environment: 'local' - environment used for load kernel
66
 * * debug: true - turn on/off debug mode
67
 * * em_service: 'doctrine.orm.entity_manager' - use the stated EntityManager to pair with Doctrine Module.
68
 * * cache_router: 'false' - enable router caching between tests in order to [increase performance](http://lakion.com/blog/how-did-we-speed-up-sylius-behat-suite-with-blackfire)
69
 * * rebootable_client: 'true' - reboot client's kernel before each request
70
 *
71
 * ### Example (`functional.suite.yml`) - Symfony 2.x Directory Structure
72
 *
73
 * ```
74
 *    modules:
75
 *        - Symfony:
76
 *            app_path: 'app/front'
77
 *            environment: 'local_test'
78
 * ```
79
 *
80
 * ## Public Properties
81
 *
82
 * * kernel - HttpKernel instance
83
 * * client - current Crawler instance
84
 *
85
 * ## Parts
86
 *
87
 * * services - allows to use Symfony DIC only with WebDriver or PhpBrowser modules.
88
 *
89
 * Usage example:
90
 *
91
 * ```yaml
92
 * actor: AcceptanceTester
93
 * modules:
94
 *     enabled:
95
 *         - Symfony:
96
 *             part: SERVICES
97
 *         - Doctrine2:
98
 *             depends: Symfony
99
 *         - WebDriver:
100
 *             url: http://your-url.com
101
 *             browser: phantomjs
102
 * ```
103
 *
104
 */
105
class Symfony extends Framework implements DoctrineProvider, PartedModule
106
{
107
    /**
108
     * @var \Symfony\Component\HttpKernel\Kernel
109
     */
110
    public $kernel;
111
112
    public $config = [
113
        'app_path' => 'app',
114
        'var_path' => 'app',
115
        'environment' => 'test',
116
        'debug' => true,
117
        'cache_router' => false,
118
        'em_service' => 'doctrine.orm.entity_manager',
119
        'rebootable_client' => true,
120
    ];
121
122
    /**
123
     * @return array
124
     */
125
    public function _parts()
126
    {
127
        return ['services'];
128
    }
129
130
    /**
131
     * @var
132
     */
133
    protected $kernelClass;
134
135
    /**
136
     * Services that should be persistent permanently for all tests
137
     *
138
     * @var array
139
     */
140
    protected $permanentServices = [];
141
142
    /**
143
     * Services that should be persistent during test execution between kernel reboots
144
     *
145
     * @var array
146
     */
147
    protected $persistentServices = [];
148
149
    public function _initialize()
150
    {
151
152
        $this->initializeSymfonyCache();
153
        $this->kernelClass = $this->getKernelClass();
154
        $maxNestingLevel = 200; // Symfony may have very long nesting level
155
        $xdebugMaxLevelKey = 'xdebug.max_nesting_level';
156
        if (ini_get($xdebugMaxLevelKey) < $maxNestingLevel) {
157
            ini_set($xdebugMaxLevelKey, $maxNestingLevel);
158
        }
159
160
        $this->kernel = new $this->kernelClass($this->config['environment'], $this->config['debug']);
161
        $this->kernel->boot();
162
163
        if ($this->config['cache_router'] === true) {
164
            $this->persistService('router', true);
165
        }
166
    }
167
168
    /**
169
     * Require Symfonys bootstrap.php.cache only for PHP Version < 7
170
     *
171
     * @throws ModuleRequireException
172
     */
173
    private function initializeSymfonyCache()
174
    {
175
        $cache = Configuration::projectDir() . $this->config['var_path'] . DIRECTORY_SEPARATOR . 'bootstrap.php.cache';
176
        if (PHP_VERSION_ID < 70000 && !file_exists($cache)) {
177
            throw new ModuleRequireException(
178
                __CLASS__,
179
                "Symfony bootstrap file not found in $cache\n \n" .
180
                "Please specify path to bootstrap file using `var_path` config option\n \n" .
181
                "If you are trying to load bootstrap from a Bundle provide path like:\n \n" .
182
                "modules:\n    enabled:\n" .
183
                "    - Symfony:\n" .
184
                "        var_path: '../../app'\n" .
185
                "        app_path: '../../app'"
186
            );
187
        }
188
        if (file_exists($cache)) {
189
            require_once $cache;
190
        }
191
    }
192
193
    /**
194
     * Initialize new client instance before each test
195
     */
196
    public function _before(\Codeception\TestInterface $test)
197
    {
198
        $this->persistentServices = array_merge($this->persistentServices, $this->permanentServices);
199
        $this->client = new SymfonyConnector($this->kernel, $this->persistentServices, $this->config['rebootable_client']);
200
    }
201
202
    /**
203
     * Update permanent services after each test
204
     */
205
    public function _after(\Codeception\TestInterface $test)
206
    {
207
        foreach ($this->permanentServices as $serviceName => $service) {
208
            $this->permanentServices[$serviceName] = $this->grabService($serviceName);
209
        }
210
        parent::_after($test);
211
    }
212
213
    /**
214
     * Retrieve Entity Manager.
215
     *
216
     * EM service is retrieved once and then that instance returned on each call
217
     */
218
    public function _getEntityManager()
219
    {
220
        if ($this->kernel === null) {
221
            $this->fail('Symfony2 platform module is not loaded');
222
        }
223
        if (!isset($this->permanentServices[$this->config['em_service']])) {
224
            // try to persist configured EM
225
            $this->persistService($this->config['em_service'], true);
226
227
            if ($this->_getContainer()->has('doctrine')) {
228
                $this->persistService('doctrine', true);
229
            }
230
            if ($this->_getContainer()->has('doctrine.orm.default_entity_manager')) {
231
                $this->persistService('doctrine.orm.default_entity_manager', true);
232
            }
233
            if ($this->_getContainer()->has('doctrine.dbal.backend_connection')) {
234
                $this->persistService('doctrine.dbal.backend_connection', true);
235
            }
236
        }
237
        return $this->permanentServices[$this->config['em_service']];
238
    }
239
240
    /**
241
     * Return container.
242
     *
243
     * @return ContainerInterface
244
     */
245
    public function _getContainer()
246
    {
247
        return $this->kernel->getContainer();
248
    }
249
250
    /**
251
     * Attempts to guess the kernel location.
252
     *
253
     * When the Kernel is located, the file is required.
254
     *
255
     * @return string The Kernel class name
256
     */
257
    protected function getKernelClass()
258
    {
259
        $path = codecept_root_dir() . $this->config['app_path'];
260
        if (!file_exists(codecept_root_dir() . $this->config['app_path'])) {
261
            throw new ModuleRequireException(
262
                __CLASS__,
263
                "Can't load Kernel from $path.\n"
264
                . "Directory does not exists. Use `app_path` parameter to provide valid application path"
265
            );
266
        }
267
268
        $finder = new Finder();
269
        $finder->name('*Kernel.php')->depth('0')->in($path);
270
        $results = iterator_to_array($finder);
271
        if (!count($results)) {
272
            throw new ModuleRequireException(
273
                __CLASS__,
274
                "File with Kernel class was not found at $path. "
275
                . "Specify directory where file with Kernel class for your application is located with `app_path` parameter."
276
            );
277
        }
278
        $file = current($results);
279
280
        if (file_exists(codecept_root_dir() . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php')) {
281
            // ensure autoloader from this dir is loaded
282
            require_once codecept_root_dir() . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php';
283
        }
284
285
        require_once $file;
286
287
        $possibleKernelClasses = [
288
            'AppKernel', // Symfony Standard
289
            'App\Kernel', // Symfony Flex
290
        ];
291
        foreach ($possibleKernelClasses as $class) {
292
            if (class_exists($class)) {
293
                $refClass = new \ReflectionClass($class);
294
                if ($refClass->getFileName() === $file->getRealpath()) {
295
                    return $class;
296
                }
297
            }
298
        }
299
300
        throw new ModuleRequireException(
301
            __CLASS__,
302
            "Kernel class was not found in $file. "
303
            . "Specify directory where file with Kernel class for your application is located with `app_path` parameter."
304
        );
305
    }
306
307
    /**
308
     * Get service $serviceName and add it to the lists of persistent services.
309
     * If $isPermanent then service becomes persistent between tests
310
     *
311
     * @param string  $serviceName
312
     * @param boolean $isPermanent
313
     */
314
    public function persistService($serviceName, $isPermanent = false)
315
    {
316
        $service = $this->grabService($serviceName);
317
        $this->persistentServices[$serviceName] = $service;
318
        if ($isPermanent) {
319
            $this->permanentServices[$serviceName] = $service;
320
        }
321
        if ($this->client) {
322
            $this->client->persistentServices[$serviceName] = $service;
0 ignored issues
show
Bug introduced by
The property persistentServices does not seem to exist in Symfony\Component\BrowserKit\Client.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
323
        }
324
    }
325
326
    /**
327
     * Remove service $serviceName from the lists of persistent services.
328
     *
329
     * @param string $serviceName
330
     */
331
    public function unpersistService($serviceName)
332
    {
333
        if (isset($this->persistentServices[$serviceName])) {
334
            unset($this->persistentServices[$serviceName]);
335
        }
336
        if (isset($this->permanentServices[$serviceName])) {
337
            unset($this->permanentServices[$serviceName]);
338
        }
339
        if ($this->client && isset($this->client->persistentServices[$serviceName])) {
0 ignored issues
show
Bug introduced by
The property persistentServices does not seem to exist in Symfony\Component\BrowserKit\Client.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
340
            unset($this->client->persistentServices[$serviceName]);
341
        }
342
    }
343
344
    /**
345
     * Invalidate previously cached routes.
346
     */
347
    public function invalidateCachedRouter()
348
    {
349
        $this->unpersistService('router');
350
    }
351
352
    /**
353
     * Opens web page using route name and parameters.
354
     *
355
     * ``` php
356
     * <?php
357
     * $I->amOnRoute('posts.create');
358
     * $I->amOnRoute('posts.show', array('id' => 34));
359
     * ?>
360
     * ```
361
     *
362
     * @param $routeName
363
     * @param array $params
364
     */
365
    public function amOnRoute($routeName, array $params = [])
366
    {
367
        $router = $this->grabService('router');
368
        if (!$router->getRouteCollection()->get($routeName)) {
369
            $this->fail(sprintf('Route with name "%s" does not exists.', $routeName));
370
        }
371
        $url = $router->generate($routeName, $params);
372
        $this->amOnPage($url);
373
    }
374
375
    /**
376
     * Checks that current url matches route.
377
     *
378
     * ``` php
379
     * <?php
380
     * $I->seeCurrentRouteIs('posts.index');
381
     * $I->seeCurrentRouteIs('posts.show', array('id' => 8));
382
     * ?>
383
     * ```
384
     *
385
     * @param $routeName
386
     * @param array $params
387
     */
388
    public function seeCurrentRouteIs($routeName, array $params = [])
389
    {
390
        $router = $this->grabService('router');
391
        if (!$router->getRouteCollection()->get($routeName)) {
392
            $this->fail(sprintf('Route with name "%s" does not exists.', $routeName));
393
        }
394
395
        $uri = explode('?', $this->grabFromCurrentUrl())[0];
396
        try {
397
            $match = $router->match($uri);
398
        } catch (\Symfony\Component\Routing\Exception\ResourceNotFoundException $e) {
399
            $this->fail(sprintf('The "%s" url does not match with any route', $uri));
400
        }
401
        $expected = array_merge(['_route' => $routeName], $params);
402
        $intersection = array_intersect_assoc($expected, $match);
403
404
        $this->assertEquals($expected, $intersection);
405
    }
406
407
    /**
408
     * Checks that current url matches route.
409
     * Unlike seeCurrentRouteIs, this can matches without exact route parameters
410
     *
411
     * ``` php
412
     * <?php
413
     * $I->seeCurrentRouteMatches('my_blog_pages');
414
     * ?>
415
     * ```
416
     *
417
     * @param $routeName
418
     */
419
    public function seeInCurrentRoute($routeName)
420
    {
421
        $router = $this->grabService('router');
422
        if (!$router->getRouteCollection()->get($routeName)) {
423
            $this->fail(sprintf('Route with name "%s" does not exists.', $routeName));
424
        }
425
426
        $uri = explode('?', $this->grabFromCurrentUrl())[0];
427
        try {
428
            $matchedRouteName = $router->match($uri)['_route'];
429
        } catch (\Symfony\Component\Routing\Exception\ResourceNotFoundException $e) {
430
            $this->fail(sprintf('The "%s" url does not match with any route', $uri));
431
        }
432
433
        $this->assertEquals($matchedRouteName, $routeName);
434
    }
435
436
    /**
437
     * Checks if any email were sent by last request
438
     *
439
     * @throws \LogicException
440
     */
441
    public function seeEmailIsSent()
442
    {
443
        $profile = $this->getProfile();
444
        if (!$profile) {
445
            $this->fail('Emails can\'t be tested without Profiler');
446
        }
447
        if (!$profile->hasCollector('swiftmailer')) {
448
            $this->fail('Emails can\'t be tested without SwiftMailer connector');
449
        }
450
451
        $this->assertGreaterThan(0, $profile->getCollector('swiftmailer')->getMessageCount());
452
    }
453
454
    /**
455
     * Grabs a service from Symfony DIC container.
456
     * Recommended to use for unit testing.
457
     *
458
     * ``` php
459
     * <?php
460
     * $em = $I->grabServiceFromContainer('doctrine');
461
     * ?>
462
     * ```
463
     *
464
     * @param $service
465
     * @return mixed
466
     * @part services
467
     * @deprecated Use grabService instead
468
     */
469
    public function grabServiceFromContainer($service)
470
    {
471
        return $this->grabService($service);
472
    }
473
474
    /**
475
     * Grabs a service from Symfony DIC container.
476
     * Recommended to use for unit testing.
477
     *
478
     * ``` php
479
     * <?php
480
     * $em = $I->grabService('doctrine');
481
     * ?>
482
     * ```
483
     *
484
     * @param $service
485
     * @return mixed
486
     * @part services
487
     */
488
    public function grabService($service)
489
    {
490
        $container = $this->_getContainer();
491
        if (!$container->has($service)) {
492
            $this->fail("Service $service is not available in container");
493
        }
494
        return $container->get($service);
495
    }
496
497
    /**
498
     * @return \Symfony\Component\HttpKernel\Profiler\Profile
499
     */
500
    protected function getProfile()
501
    {
502
        $container = $this->_getContainer();
503
        if (!$container->has('profiler')) {
504
            return null;
505
        }
506
507
        $profiler = $this->grabService('profiler');
508
        $response = $this->client->getResponse();
509
        if (null === $response) {
510
            $this->fail("You must perform a request before using this method.");
511
        }
512
        return $profiler->loadProfileFromResponse($response);
513
    }
514
515
    /**
516
     * @param $url
517
     */
518
    protected function debugResponse($url)
519
    {
520
        parent::debugResponse($url);
521
522
        if ($profile = $this->getProfile()) {
523
            if ($profile->hasCollector('security')) {
524
                if ($profile->getCollector('security')->isAuthenticated()) {
525
                    $roles = $profile->getCollector('security')->getRoles();
526
527
                    if ($roles instanceof Data) {
528
                        $roles = $this->extractRawRoles($roles);
529
                    }
530
531
                    $this->debugSection(
532
                        'User',
533
                        $profile->getCollector('security')->getUser()
534
                        . ' [' . implode(',', $roles) . ']'
535
                    );
536
                } else {
537
                    $this->debugSection('User', 'Anonymous');
538
                }
539
            }
540
            if ($profile->hasCollector('swiftmailer')) {
541
                $messages = $profile->getCollector('swiftmailer')->getMessageCount();
542
                if ($messages) {
543
                    $this->debugSection('Emails', $messages . ' sent');
544
                }
545
            }
546
            if ($profile->hasCollector('timer')) {
547
                $this->debugSection('Time', $profile->getCollector('timer')->getTime());
548
            }
549
        }
550
    }
551
552
    /**
553
     * @param Data $data
554
     * @return array
555
     */
556
    private function extractRawRoles(Data $data)
557
    {
558
        if ($this->dataRevealsValue($data)) {
559
            $roles = $data->getValue();
560
        } else {
561
            $raw = $data->getRawData();
562
            $roles = isset($raw[1]) ? $raw[1] : [];
563
        }
564
565
        return $roles;
566
    }
567
568
    /**
569
     * Returns a list of recognized domain names.
570
     *
571
     * @return array
572
     */
573
    protected function getInternalDomains()
574
    {
575
        $internalDomains = [];
576
577
        $routes = $this->grabService('router')->getRouteCollection();
578
        /* @var \Symfony\Component\Routing\Route $route */
579
        foreach ($routes as $route) {
580
            if (!is_null($route->getHost())) {
581
                $compiled = $route->compile();
582
                if (!is_null($compiled->getHostRegex())) {
583
                    $internalDomains[] = $compiled->getHostRegex();
584
                }
585
            }
586
        }
587
588
        return array_unique($internalDomains);
589
    }
590
591
    /**
592
     * Reboot client's kernel.
593
     * Can be used to manually reboot kernel when 'rebootable_client' => false
594
     *
595
     * ``` php
596
     * <?php
597
     * ...
598
     * perform some requests
599
     * ...
600
     * $I->rebootClientKernel();
601
     * ...
602
     * perform other requests
603
     * ...
604
     *
605
     * ?>
606
     * ```
607
     *
608
     */
609
    public function rebootClientKernel()
610
    {
611
        if ($this->client) {
612
            $this->client->rebootKernel();
613
        }
614
    }
615
616
    /**
617
     * Public API from Data changed from Symfony 3.2 to 3.3.
618
     *
619
     * @param \Symfony\Component\VarDumper\Cloner\Data $data
620
     *
621
     * @return bool
622
     */
623
    private function dataRevealsValue(Data $data)
624
    {
625
        return method_exists($data, 'getValue');
626
    }
627
}
628