Completed
Pull Request — master (#29)
by Adrian
05:49
created

SymfonyExtension::autoconfigure()   B

Complexity

Conditions 9
Paths 20

Size

Total Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 27
rs 8.0555
c 0
b 0
f 0
cc 9
nc 20
nop 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace FriendsOfBehat\SymfonyExtension\ServiceContainer;
6
7
use Behat\MinkExtension\ServiceContainer\MinkExtension;
8
use Behat\Testwork\EventDispatcher\ServiceContainer\EventDispatcherExtension;
9
use Behat\Testwork\ServiceContainer\Extension;
10
use Behat\Testwork\ServiceContainer\ExtensionManager;
11
use FriendsOfBehat\CrossContainerExtension\CrossContainerProcessor;
12
use FriendsOfBehat\CrossContainerExtension\KernelBasedContainerAccessor;
13
use FriendsOfBehat\CrossContainerExtension\ServiceContainer\CrossContainerExtension;
14
use FriendsOfBehat\SymfonyExtension\Driver\Factory\SymfonyDriverFactory;
15
use FriendsOfBehat\SymfonyExtension\Listener\KernelRebooter;
16
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
17
use Symfony\Component\DependencyInjection\Container;
18
use Symfony\Component\DependencyInjection\ContainerBuilder;
19
use Symfony\Component\DependencyInjection\Definition;
20
use Symfony\Component\DependencyInjection\Reference;
21
use Symfony\Component\Dotenv\Dotenv;
22
use Symfony\Component\HttpKernel\KernelInterface;
23
24
final class SymfonyExtension implements Extension
25
{
26
    /**
27
     * Kernel used inside Behat contexts or to create services injected to them.
28
     * Container is built before every scenario.
29
     */
30
    const KERNEL_ID = 'sylius_symfony_extension.kernel';
31
32
    /**
33
     * The current container used in scenario contexts.
34
     * To be used as a factory for current injected application services.
35
     */
36
    const KERNEL_CONTAINER_ID = 'sylius_symfony_extension.kernel.container';
37
38
    /**
39
     * Kernel used by Symfony2 driver to isolate web container from contexts' container.
40
     * Container is built before every request.
41
     */
42
    const DRIVER_KERNEL_ID = 'sylius_symfony_extension.driver_kernel';
43
44
    /**
45
     * Kernel that should be used by extensions only.
46
     * Container is built only once at the first use.
47
     */
48
    const SHARED_KERNEL_ID = 'sylius_symfony_extension.shared_kernel';
49
50
    /**
51
     * The only container built by shared kernel.
52
     * To be used as a factory for shared injected application services.
53
     */
54
    const SHARED_KERNEL_CONTAINER_ID = 'sylius_symfony_extension.shared_kernel.container';
55
56
    /**
57
     * Default symfony environment used to run your suites.
58
     */
59
    private const DEFAULT_ENV = 'test';
60
61
    /**
62
     * Enable or disable the debug mode
63
     */
64
    private const DEFAULT_DEBUG_MODE = true;
65
66
    /**
67
     * Default Symfony configuration
68
     */
69
    private const SYMFONY_DEFAULTS = [
70
        'env_file' => null,
71
        'kernel' => [
72
            'bootstrap' => 'app/autoload.php',
73
            'path' => 'app/AppKernel.php',
74
            'class' => 'AppKernel',
75
            'env' => self::DEFAULT_ENV,
76
            'debug' => self::DEFAULT_DEBUG_MODE,
77
        ],
78
    ];
79
80
    /**
81
     * Default Symfony 4 configuration
82
     */
83
    private const SYMFONY_4_DEFAULTS = [
84
        'env_file' => '.env',
85
        'kernel' => [
86
            'bootstrap' => null,
87
            'path' => 'src/Kernel.php',
88
            'class' => 'App\Kernel',
89
            'env' => self::DEFAULT_ENV,
90
            'debug' => self::DEFAULT_DEBUG_MODE,
91
        ],
92
    ];
93
94
    /**
95
     * @var CrossContainerProcessor|null
96
     */
97
    private $crossContainerProcessor;
98
99
    /**
100
     * {@inheritdoc}
101
     */
102
    public function getConfigKey(): string
103
    {
104
        return 'fob_symfony';
105
    }
106
107
    /**
108
     * {@inheritdoc}
109
     */
110
    public function initialize(ExtensionManager $extensionManager): void
111
    {
112
        $this->registerSymfonyDriverFactory($extensionManager);
113
        $this->initializeCrossContainerProcessor($extensionManager);
114
    }
115
116
    /**
117
     * {@inheritdoc}
118
     */
119
    public function configure(ArrayNodeDefinition $builder): void
120
    {
121
        $builder
122
            ->children()
123
                ->scalarNode('env_file')->end()
124
                ->arrayNode('kernel')
125
                    ->addDefaultsIfNotSet()
126
                    ->children()
127
                        ->scalarNode('bootstrap')->defaultFalse()->end()
128
                        ->scalarNode('path')->end()
129
                        ->scalarNode('class')->end()
130
                        ->scalarNode('env')->end()
131
                        ->booleanNode('debug')->end()
132
                    ->end()
133
                ->end()
134
            ->end()
135
        ;
136
    }
137
138
    /**
139
     * {@inheritdoc}
140
     */
141
    public function load(ContainerBuilder $container, array $config): void
142
    {
143
        $config = $this->autoconfigure($container, $config);
144
145
        $this->loadKernel($container, $config['kernel']);
146
        $this->loadKernelContainer($container);
147
148
        $this->loadDriverKernel($container);
149
150
        $this->loadSharedKernel($container);
151
        $this->loadSharedKernelContainer($container);
152
153
        $this->loadKernelRebooter($container);
154
        $this->declareSymfonyContainers($container);
155
    }
156
157
    /**
158
     * {@inheritdoc}
159
     */
160
    public function process(ContainerBuilder $container): void
161
    {
162
    }
163
164
    private function autoconfigure(ContainerBuilder $container, array $userConfig): array
165
    {
166
        $defaults = self::SYMFONY_DEFAULTS;
167
168
        $symfonyFourKernelPath = sprintf('%s/%s', $container->getParameter('paths.base'), self::SYMFONY_4_DEFAULTS['kernel']['path']);
169
        if ($userConfig['kernel']['bootstrap'] === null || file_exists($symfonyFourKernelPath)) {
170
            $defaults = self::SYMFONY_4_DEFAULTS;
171
        }
172
173
        $userConfig['kernel']['bootstrap'] = $userConfig['kernel']['bootstrap'] === false ? null : $userConfig['kernel']['bootstrap'];
174
175
        $config = array_replace_recursive($defaults, $userConfig);
176
177
        if (null !== $config['env_file']) {
178
            $this->loadEnvVars($container, $config['env_file']);
179
180
            if (!isset($userConfig['kernel']['env']) && false !== getenv('APP_ENV')) {
181
                $config['kernel']['env'] = getenv('APP_ENV');
182
            }
183
184
            if (!isset($userConfig['kernel']['debug']) && false !== getenv('APP_DEBUG')) {
185
                $config['kernel']['debug'] = getenv('APP_DEBUG');
186
            }
187
        }
188
189
        return $config;
190
    }
191
192
    private function loadEnvVars(ContainerBuilder $container, string $fileName): void
193
    {
194
        $envFilePath = sprintf('%s/%s', $container->getParameter('paths.base'), $fileName);
195
        $envFilePath = file_exists($envFilePath) ? $envFilePath : $envFilePath . '.dist';
196
        (new Dotenv())->load($envFilePath);
197
    }
198
199
    private function loadKernel(ContainerBuilder $container, array $config): void
200
    {
201
        $definition = new Definition($config['class'], [
202
            $config['env'],
203
            $config['debug'],
204
        ]);
205
        $definition->addMethodCall('boot');
206
        $definition->setPublic(true);
207
208
        $file = $this->getKernelFile($container->getParameter('paths.base'), $config['path']);
209
        if (null !== $file) {
210
            $definition->setFile($file);
211
        }
212
213
        $container->setDefinition(self::KERNEL_ID, $definition);
214
215
        $this->requireKernelBootstrapFile($container->getParameter('paths.base'), $config['bootstrap']);
216
    }
217
218
    private function loadKernelContainer(ContainerBuilder $container): void
219
    {
220
        $containerDefinition = new Definition(Container::class);
221
        $containerDefinition->setFactory([
222
            new Reference(self::KERNEL_ID),
223
            'getContainer',
224
        ]);
225
226
        $container->setDefinition(self::KERNEL_CONTAINER_ID, $containerDefinition);
227
    }
228
229
    private function loadDriverKernel(ContainerBuilder $container): void
230
    {
231
        $container->setDefinition(self::DRIVER_KERNEL_ID, $container->findDefinition(self::KERNEL_ID));
232
    }
233
234
    private function loadSharedKernel(ContainerBuilder $container): void
235
    {
236
        $container->setDefinition(self::SHARED_KERNEL_ID, $container->findDefinition(self::KERNEL_ID));
237
    }
238
239
    private function loadSharedKernelContainer(ContainerBuilder $container): void
240
    {
241
        $containerDefinition = new Definition(Container::class);
242
        $containerDefinition->setFactory([
243
            new Reference(self::SHARED_KERNEL_ID),
244
            'getContainer',
245
        ]);
246
247
        $container->setDefinition(self::SHARED_KERNEL_CONTAINER_ID, $containerDefinition);
248
    }
249
250
    /**
251
     * @throws \Exception
252
     */
253
    private function loadKernelRebooter(ContainerBuilder $container): void
254
    {
255
        $definition = new Definition(KernelRebooter::class, [new Reference(self::KERNEL_ID)]);
256
        $definition->addTag(EventDispatcherExtension::SUBSCRIBER_TAG);
257
258
        $container->setDefinition(self::KERNEL_ID . '.rebooter', $definition);
259
    }
260
261
    /**
262
     * @throws \Exception
263
     */
264
    private function declareSymfonyContainers(ContainerBuilder $container): void
265
    {
266
        if (null === $this->crossContainerProcessor) {
267
            return;
268
        }
269
270
        $containerAccessors = [
271
            'symfony' => self::KERNEL_ID,
272
            'symfony_driver' => self::DRIVER_KERNEL_ID,
273
            'symfony_shared' => self::SHARED_KERNEL_ID,
274
        ];
275
276
        foreach ($containerAccessors as $containerName => $kernelIdentifier) {
277
            $kernel = $container->get($kernelIdentifier);
278
279
            if (!$kernel instanceof KernelInterface) {
280
                throw new \RuntimeException(sprintf(
281
                    'Expected service "%s" to be an instance of "%s", got "%s" instead.',
282
                    $kernelIdentifier,
283
                    KernelInterface::class,
284
                    \is_object($kernel) ? \get_class($kernel) : \gettype($kernel)
285
                ));
286
            }
287
288
            $this->crossContainerProcessor->addContainerAccessor($containerName, new KernelBasedContainerAccessor($kernel));
289
        }
290
    }
291
292
    private function initializeCrossContainerProcessor(ExtensionManager $extensionManager): void
293
    {
294
        /** @var CrossContainerExtension $extension */
295
        $extension = $extensionManager->getExtension('fob_cross_container');
296
        if (null !== $extension) {
297
            $this->crossContainerProcessor = $extension->getCrossContainerProcessor();
298
        }
299
    }
300
301
    private function registerSymfonyDriverFactory(ExtensionManager $extensionManager): void
302
    {
303
        /** @var MinkExtension|null $minkExtension */
304
        $minkExtension = $extensionManager->getExtension('mink');
305
        if (null === $minkExtension) {
306
            return;
307
        }
308
309
        $minkExtension->registerDriverFactory(new SymfonyDriverFactory(
310
            'symfony',
311
            new Reference(self::DRIVER_KERNEL_ID)
312
        ));
313
    }
314
315
    private function getKernelFile(string $basePath, string $kernelPath): ?string
316
    {
317
        $possibleFiles = [
318
            sprintf('%s/%s', $basePath, $kernelPath),
319
            $kernelPath,
320
        ];
321
322
        foreach ($possibleFiles as $possibleFile) {
323
            if (file_exists($possibleFile)) {
324
                return $possibleFile;
325
            }
326
        }
327
328
        return null;
329
    }
330
331
    /**
332
     * @throws \DomainException
333
     */
334
    private function requireKernelBootstrapFile(string $basePath, ?string $bootstrapPath): void
335
    {
336
        if (null === $bootstrapPath) {
337
            return;
338
        }
339
340
        $possiblePaths = [
341
            sprintf('%s/%s', $basePath, $bootstrapPath),
342
            $bootstrapPath,
343
        ];
344
345
        foreach ($possiblePaths as $possiblePath) {
346
            if (file_exists($possiblePath)) {
347
                require_once $possiblePath;
348
349
                return;
350
            }
351
        }
352
353
        throw new \DomainException('Could not load bootstrap file.');
354
    }
355
}
356