diff --git a/.travis.yml b/.travis.yml index 1a9df4b..eb6680c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,6 @@ before_install: - phpenv config-rm xdebug.ini || true install: - - composer require symfony/dotenv:${SYMFONY_VERSION} --no-update --no-scripts --prefer-dist - composer require symfony/http-kernel:${SYMFONY_VERSION} --no-update --no-scripts --prefer-dist - composer require symfony/proxy-manager-bridge:${SYMFONY_VERSION} --no-update --no-scripts --prefer-dist - composer require --dev symfony/framework-bundle:${SYMFONY_VERSION} --no-update --no-scripts --prefer-dist diff --git a/composer.json b/composer.json index 998fa7e..dc29dc6 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,6 @@ "require": { "php": "^7.1", "behat/behat": "^3.4", - "symfony/dotenv": "^3.4|^4.1", "symfony/http-kernel": "^3.4|^4.1", "symfony/proxy-manager-bridge": "^3.4|^4.1" }, diff --git a/features/configuration/autodiscovering_application_kernel.feature b/features/configuration/autodiscovering_application_kernel.feature new file mode 100644 index 0000000..04b8dad --- /dev/null +++ b/features/configuration/autodiscovering_application_kernel.feature @@ -0,0 +1,214 @@ +Feature: Autodiscovering the application kernel + + Background: + Given a standard Symfony autoloader configured + And a feature file containing: + """ + Feature: + Scenario: + Then the passed service should be an instance of "\Psr\Container\ContainerInterface" + """ + And a Behat configuration containing: + """ + default: + extensions: + FriendsOfBehat\SymfonyExtension: ~ + + suites: + default: + contexts: + - App\Tests\SomeContext + """ + And a context file "tests/SomeContext.php" containing: + """ + service = $service; } + + /** @Then the passed service should be an instance of :expected */ + public function serviceShouldBe(string $expected): void + { + assert(is_object($this->service)); + assert($this->service instanceof $expected); + } + } + """ + And a services file "config/services.yaml" containing: + """ + services: + App\Tests\SomeContext: + public: true + arguments: + - "@service_container" + """ + + Scenario: Autodiscovering kernel in Symfony 4 directory structure application + Given a kernel file "src/Kernel.php" containing: + """ + loadFromExtension('framework', [ + 'test' => true, + 'secret' => 'Pigeon', + ]); + + $loader->load(__DIR__ . '/../config/services.yaml'); + } + + protected function configureRoutes(RouteCollectionBuilder $routes): void {} + } + """ + When I run Behat + Then it should pass + + Scenario: Autodiscovering kernel in Symfony 3 directory structure application + Given a kernel file "app/AppKernel.php" containing: + """ + loadFromExtension('framework', [ + 'test' => true, + 'secret' => 'Pigeon', + ]); + + $loader->load(__DIR__ . '/../config/services.yaml'); + } + + protected function configureRoutes(RouteCollectionBuilder $routes): void {} + } + """ + When I run Behat + Then it should pass + + Scenario: Failing to autodiscover the kernel + When I run Behat + Then it should fail with "Could not autodiscover the application kernel" + + Scenario: Failing to autodiscover the kernel + Given a kernel file "src/Kernel.php" containing: + """ + loadFromExtension('framework', [ + 'test' => true, + 'secret' => 'Pigeon', + ]); + + $loader->load(__DIR__ . '/../config/services.yaml'); + } + + protected function configureRoutes(RouteCollectionBuilder $routes): void {} + } + """ + And a kernel file "app/AppKernel.php" containing: + """ + loadFromExtension('framework', [ + 'test' => true, + 'secret' => 'Pigeon', + ]); + + $loader->load(__DIR__ . '/../config/services.yaml'); + } + + protected function configureRoutes(RouteCollectionBuilder $routes): void {} + } + """ + When I run Behat + Then it should fail with "Could not autodiscover the application kernel" diff --git a/features/configuration/autodiscovering_bootstrap_file.feature b/features/configuration/autodiscovering_bootstrap_file.feature new file mode 100644 index 0000000..a33cda8 --- /dev/null +++ b/features/configuration/autodiscovering_bootstrap_file.feature @@ -0,0 +1,101 @@ +Feature: Autodiscovering bootstrap file + + Background: + Given a working Symfony application with SymfonyExtension configured + And a Behat configuration containing: + """ + default: + suites: + default: + contexts: + - App\Tests\SomeContext + """ + And a context file "tests/SomeContext.php" containing: + """ + parameter = $parameter; } + + /** @Then the passed parameter should be :expected */ + public function parameterShouldBe(string $expected): void { assert($this->parameter === $expected); } + } + """ + And a YAML services file containing: + """ + services: + App\Tests\SomeContext: + public: true + arguments: + - "%env(CUSTOM_VARIABLE)%" + """ + And a feature file containing: + """ + Feature: + Scenario: + Then the passed parameter should be "lol2" + """ + + Scenario: Autodiscovering bootstrap file in Symfony 4 directory structure application + Given a boostrap file "config/bootstrap.php" containing: + """ + kernel = $kernel; } + + /** @Then the application kernel should have environment :environment */ + public function kernelEnvironmentShouldBe(string $environment): void { assert($this->kernel->getEnvironment() === $environment); } + + /** @Then the application kernel should have debug :state*/ + public function kernelDebugShouldBe(string $state): void + { + $map = ['enabled' => true, 'disabled' => false]; + + if (!array_key_exists($state, $map)) { throw new \Exception('Invalid state passed!'); } + + assert($this->kernel->isDebug() === $map[$state]); + } + } + """ + And a YAML services file containing: + """ + services: + App\Tests\SomeContext: + public: true + arguments: + - "@kernel" + """ + + Scenario: Using test environment with debug enabled by default + Given a feature file containing: + """ + Feature: + Scenario: + Then the application kernel should have environment "test" + And the application kernel should have debug enabled + """ + When I run Behat + Then it should pass + + Scenario: Using configured environment + Given a Behat configuration containing: + """ + default: + extensions: + FriendsOfBehat\SymfonyExtension: + kernel: + environment: custom + """ + And a feature file containing: + """ + Feature: + Scenario: + Then the application kernel should have environment "custom" + """ + When I run Behat + Then it should pass + + Scenario: Using configured debug setting + Given a Behat configuration containing: + """ + default: + extensions: + FriendsOfBehat\SymfonyExtension: + kernel: + debug: false + """ + And a feature file containing: + """ + Feature: + Scenario: + And the application kernel should have debug disabled + """ + When I run Behat + Then it should pass diff --git a/features/configuration/loading_configured_application_kernel.feature b/features/configuration/loading_configured_application_kernel.feature new file mode 100644 index 0000000..67e6908 --- /dev/null +++ b/features/configuration/loading_configured_application_kernel.feature @@ -0,0 +1,146 @@ +Feature: Loading configured application kernel + + Background: + Given a standard Symfony autoloader configured + And a feature file containing: + """ + Feature: + Scenario: + Then the passed service should be an instance of "\Psr\Container\ContainerInterface" + """ + And a Behat configuration containing: + """ + default: + suites: + default: + contexts: + - App\Tests\SomeContext + """ + And a context file "tests/SomeContext.php" containing: + """ + service = $service; } + + /** @Then the passed service should be an instance of :expected */ + public function serviceShouldBe(string $expected): void + { + assert(is_object($this->service)); + assert($this->service instanceof $expected); + } + } + """ + And a services file "config/services.yaml" containing: + """ + services: + App\Tests\SomeContext: + public: true + arguments: + - "@service_container" + """ + + Scenario: Loading kernel by its classname + Given a Behat configuration containing: + """ + default: + extensions: + FriendsOfBehat\SymfonyExtension: + kernel: + class: App\Custom\Kernel + """ + And a kernel file "src/Custom/Kernel.php" containing: + """ + loadFromExtension('framework', [ + 'test' => true, + 'secret' => 'Pigeon', + ]); + + $loader->load(__DIR__ . '/../../config/services.yaml'); + } + + protected function configureRoutes(RouteCollectionBuilder $routes): void {} + } + """ + When I run Behat + Then it should pass + + Scenario: Loading kernel from custom path + Given a Behat configuration containing: + """ + default: + extensions: + FriendsOfBehat\SymfonyExtension: + kernel: + path: app/Nested/Kernel.php + class: AppKernel + """ + And a kernel file "app/Nested/Kernel.php" containing: + """ + loadFromExtension('framework', [ + 'test' => true, + 'secret' => 'Pigeon', + ]); + + $loader->load(__DIR__ . '/../../config/services.yaml'); + } + + protected function configureRoutes(RouteCollectionBuilder $routes): void {} + } + """ + When I run Behat + Then it should pass diff --git a/features/configuration/loading_configured_bootstrap_file.feature b/features/configuration/loading_configured_bootstrap_file.feature new file mode 100644 index 0000000..505e827 --- /dev/null +++ b/features/configuration/loading_configured_bootstrap_file.feature @@ -0,0 +1,56 @@ +Feature: Loading configured bootstrap file + + Scenario: Loading configured bootstrap file + Given a working Symfony application with SymfonyExtension configured + And a Behat configuration containing: + """ + default: + extensions: + FriendsOfBehat\SymfonyExtension: + bootstrap: custom/bootstrap.php + + suites: + default: + contexts: + - App\Tests\SomeContext + """ + And a boostrap file "custom/bootstrap.php" containing: + """ + parameter = $parameter; } + + /** @Then the passed parameter should be :expected */ + public function parameterShouldBe(string $expected): void { assert($this->parameter === $expected); } + } + """ + And a YAML services file containing: + """ + services: + App\Tests\SomeContext: + public: true + arguments: + - "%env(CUSTOM_VARIABLE)%" + """ + And a feature file containing: + """ + Feature: + Scenario: + Then the passed parameter should be "lol2" + """ + When I run Behat + Then it should pass diff --git a/phpstan.neon b/phpstan.neon index d944ec3..3da1825 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -4,5 +4,6 @@ parameters: ignoreErrors: - '/Cannot access offset 0 on callable\./' - '/Cannot access offset 1 on callable\./' - - '/Method FriendsOfBehat\\SymfonyExtension\\Context\\Environment\\InitialisedContextServiceEnvironment::bindCallee\(\) should return callable/' - '/Cannot call method [a-zA-Z0-9]+\(\) on Symfony\\Component\\Config\\Definition\\Builder\\NodeParentInterface|null\./' + - '/Method FriendsOfBehat\\SymfonyExtension\\Context\\Environment\\InitialisedContextServiceEnvironment::bindCallee\(\) should return callable/' + - '/Strict comparison using === between 0\|1 and 2 will always evaluate to false\./' diff --git a/src/ServiceContainer/SymfonyExtension.php b/src/ServiceContainer/SymfonyExtension.php index 60ab56b..8c5512a 100644 --- a/src/ServiceContainer/SymfonyExtension.php +++ b/src/ServiceContainer/SymfonyExtension.php @@ -51,11 +51,16 @@ final class SymfonyExtension implements Extension public function configure(ArrayNodeDefinition $builder): void { $builder + ->addDefaultsIfNotSet() ->children() + ->scalarNode('bootstrap')->defaultNull()->end() ->arrayNode('kernel') ->addDefaultsIfNotSet() ->children() - ->scalarNode('class')->end() + ->scalarNode('path')->defaultNull()->end() + ->scalarNode('class')->defaultNull()->end() + ->scalarNode('environment')->defaultValue('test')->end() + ->booleanNode('debug')->defaultTrue()->end() ->end() ->end() ->end() @@ -64,7 +69,9 @@ final class SymfonyExtension implements Extension public function load(ContainerBuilder $container, array $config): void { - $this->loadKernel($container, $config['kernel']); + $container->setParameter('fob_symfony.bootstrap', $config['bootstrap']); + + $this->loadKernel($container, $this->autodiscoverKernelConfiguration($config['kernel'])); $this->loadDriverKernel($container); $this->loadKernelRebooter($container); @@ -79,6 +86,7 @@ final class SymfonyExtension implements Extension public function process(ContainerBuilder $container): void { + $this->processBootstrap($this->autodiscoverBootstrap($container->getParameter('fob_symfony.bootstrap'))); } private function registerMinkDriver(ExtensionManager $extensionManager): void @@ -97,12 +105,16 @@ final class SymfonyExtension implements Extension private function loadKernel(ContainerBuilder $container, array $config): void { $definition = new Definition($config['class'], [ - 'test', - true, + $config['environment'], + $config['debug'], ]); $definition->addMethodCall('boot'); $definition->setPublic(true); + if ($config['path'] !== null) { + $definition->setFile($config['path']); + } + $container->setDefinition(self::KERNEL_ID, $definition); } @@ -149,4 +161,82 @@ final class SymfonyExtension implements Extension $container->setDefinition('fob_symfony.mink.parameters', $minkParametersDefinition); } + + private function autodiscoverKernelConfiguration(array $config): array + { + if ($config['class'] !== null) { + return $config; + } + + $autodiscovered = 0; + + if (class_exists('\App\Kernel')) { + $config['class'] = '\App\Kernel'; + + ++$autodiscovered; + } + + if (file_exists('app/AppKernel.php')) { + $config['class'] = '\AppKernel'; + $config['path'] = 'app/AppKernel.php'; + + ++$autodiscovered; + } + + if ($autodiscovered !== 1) { + throw new \RuntimeException( + 'Could not autodiscover the application kernel. ' . + 'Please define it manually with "FriendsOfBehat\SymfonyExtension.kernel" configuration option.' + ); + } + + return $config; + } + + /** + * @param string|bool|null $bootstrap + */ + private function autodiscoverBootstrap($bootstrap): ?string + { + if (is_string($bootstrap)) { + return $bootstrap; + } + + if ($bootstrap === false) { + return null; + } + + $autodiscovered = 0; + + if (file_exists('config/bootstrap.php')) { + $bootstrap = 'config/bootstrap.php'; + + ++$autodiscovered; + } + + if (file_exists('app/autoload.php')) { + $bootstrap = 'app/autoload.php'; + + ++$autodiscovered; + } + + if ($autodiscovered === 2) { + throw new \RuntimeException( + 'Could not autodiscover the bootstrap file. ' . + 'Please define it manually with "FriendsOfBehat\SymfonyExtension.bootstrap" configuration option. ' . + 'Setting that option to "false" disables autodiscovering.' + ); + } + + return is_string($bootstrap) ? $bootstrap : null; + } + + private function processBootstrap(?string $bootstrap): void + { + if ($bootstrap === null) { + return; + } + + require_once $bootstrap; + } } diff --git a/tests/Behat/Context/TestContext.php b/tests/Behat/Context/TestContext.php index 1398c1f..0857bdf 100644 --- a/tests/Behat/Context/TestContext.php +++ b/tests/Behat/Context/TestContext.php @@ -51,6 +51,25 @@ final class TestContext implements Context self::$filesystem->remove(self::$workingDir); } + /** + * @Given a standard Symfony autoloader configured + */ + public function standardSymfonyAutoloaderConfigured(): void + { + $this->thereIsFile('vendor/autoload.php', sprintf(<<<'CON' +addPsr4('App\\', __DIR__ . '/../src/'); +$loader->addPsr4('App\\Tests\\', __DIR__ . '/../tests/'); + +return $loader; +CON + , __DIR__ . '/../../../vendor/autoload.php')); + } + /** * @Given a working Symfony application with SymfonyExtension configured */ @@ -65,18 +84,7 @@ default: CON ); - $this->thereIsFile('vendor/autoload.php', sprintf(<<<'CON' -addPsr4('App\\', __DIR__ . '/../src/'); -$loader->addPsr4('App\\Tests\\', __DIR__ . '/../tests/'); - -return $loader; -CON - , __DIR__ . '/../../../vendor/autoload.php')); + $this->standardSymfonyAutoloaderConfigured(); $this->thereIsFile('src/Kernel.php', <<<'CON'