diff --git a/src/bundle/DependencyInjection/CompilerPass/RemoveUnsatisfiableHooksPass.php b/src/bundle/DependencyInjection/CompilerPass/RemoveUnsatisfiableHooksPass.php new file mode 100644 index 0000000..1c4f0d8 --- /dev/null +++ b/src/bundle/DependencyInjection/CompilerPass/RemoveUnsatisfiableHooksPass.php @@ -0,0 +1,58 @@ +> hook service id => service ids it cannot work without + */ + private const HOOK_REQUIREMENTS = [ + DatabaseSchemaHook::class => [ + SchemaBuilderInterface::class, + // core's own schema contribution; without it the event yields every other package's + // tables but none of core's + BuildSchemaSubscriber::class, + ], + FixtureHook::class => [FixtureImporter::class], + PurgeSearchIndexHook::class => ['ibexa.spi.search'], + PurgeIndexAfterFixturesHook::class => ['ibexa.spi.search'], + ]; + + public function process(ContainerBuilder $container): void + { + foreach (self::HOOK_REQUIREMENTS as $hookId => $requiredServiceIds) { + if (!$container->hasDefinition($hookId)) { + continue; + } + + foreach ($requiredServiceIds as $serviceId) { + if (!$container->has($serviceId)) { + $container->removeDefinition($hookId); + + break; + } + } + } + } +} diff --git a/src/bundle/IbexaTestCoreBundle.php b/src/bundle/IbexaTestCoreBundle.php index a0bbb49..435b6d4 100644 --- a/src/bundle/IbexaTestCoreBundle.php +++ b/src/bundle/IbexaTestCoreBundle.php @@ -9,6 +9,7 @@ namespace Ibexa\Bundle\Test\Core; use Ibexa\Bundle\Test\Core\DependencyInjection\CompilerPass\PersistenceCheckCompilerPass; +use Ibexa\Bundle\Test\Core\DependencyInjection\CompilerPass\RemoveUnsatisfiableHooksPass; use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; @@ -18,5 +19,6 @@ final class IbexaTestCoreBundle extends Bundle public function build(ContainerBuilder $container): void { $container->addCompilerPass(new PersistenceCheckCompilerPass(), PassConfig::TYPE_AFTER_REMOVING); + $container->addCompilerPass(new RemoveUnsatisfiableHooksPass()); } } diff --git a/src/bundle/Resources/config/services.php b/src/bundle/Resources/config/services.php index 654a94a..26607ed 100644 --- a/src/bundle/Resources/config/services.php +++ b/src/bundle/Resources/config/services.php @@ -17,21 +17,16 @@ use Ibexa\Contracts\Test\Core\Bootstrapper\HooksExecutorInterface; use Ibexa\Contracts\Test\Core\Bootstrapper\PurgeIndexAfterFixturesHook; use Ibexa\Contracts\Test\Core\Bootstrapper\PurgeSearchIndexHook; -use Ibexa\Contracts\Test\Core\Bootstrapper\SchemaFilesProviderInterface; use Ibexa\Test\Core\Bootstrapper\FixtureKernelMethodProvider; use Ibexa\Test\Core\Bootstrapper\FixtureParameterProvider; use Ibexa\Test\Core\Bootstrapper\FixtureProviderChain; use Ibexa\Test\Core\Bootstrapper\HooksExecutor; -use Ibexa\Test\Core\Bootstrapper\SchemaFilesKernelMethodProvider; -use Ibexa\Test\Core\Bootstrapper\SchemaFilesParameterProvider; -use Ibexa\Test\Core\Bootstrapper\SchemaFilesProviderChain; return static function (ContainerConfigurator $containerConfigurator): void { $containerConfigurator->parameters() // null (not []): an unset/unconfigured parameter must be distinguishable from a consumer // deliberately configuring an empty list, since only the latter should stop the fallback - // chain in *ProviderChain — see SchemaFilesProviderInterface's docblock. - ->set('ibexa.test.schema_files', null) + // chain in FixtureProviderChain — see FixtureProviderInterface's docblock. ->set('ibexa.test.fixture_files', null); $services = $containerConfigurator->services(); @@ -56,17 +51,6 @@ $services->set(DefaultFixtureProvider::class); - $services->set(SchemaFilesKernelMethodProvider::class) - ->arg('$kernel', service('kernel')) - ->tag(SchemaFilesProviderInterface::TAG, ['priority' => 0]); - - $services->set(SchemaFilesParameterProvider::class) - ->arg('$schemaFiles', '%ibexa.test.schema_files%') - ->tag(SchemaFilesProviderInterface::TAG, ['priority' => 100]); - - $services->set(SchemaFilesProviderChain::class) - ->arg('$providers', tagged_iterator(SchemaFilesProviderInterface::TAG)); - $services->set(FixtureKernelMethodProvider::class) ->arg('$kernel', service('kernel')) ->tag(FixtureProviderInterface::TAG, ['priority' => 0]); @@ -79,7 +63,6 @@ ->arg('$providers', tagged_iterator(FixtureProviderInterface::TAG)); $services->set(DatabaseSchemaHook::class) - ->arg('$provider', service(SchemaFilesProviderChain::class)) ->tag(HookInterface::TAG, ['priority' => DatabaseSchemaHook::PRIORITY]); $services->set(FixtureHook::class) diff --git a/src/contracts/Bootstrapper/Bootstrapper.php b/src/contracts/Bootstrapper/Bootstrapper.php index c6e1ccf..552d19c 100644 --- a/src/contracts/Bootstrapper/Bootstrapper.php +++ b/src/contracts/Bootstrapper/Bootstrapper.php @@ -8,10 +8,10 @@ namespace Ibexa\Contracts\Test\Core\Bootstrapper; -use Ibexa\Contracts\Test\Core\IbexaTestKernel; use LogicException; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -74,7 +74,7 @@ public function __construct( public function bootstrap( ?string $kernelClass = null, array $options = [] - ): IbexaTestKernel { + ): KernelInterface { $kernel = $this->kernelProvider->getKernel($kernelClass); $testContainer = self::getService($kernel->getContainer(), 'test.service_container', ContainerInterface::class); diff --git a/src/contracts/Bootstrapper/DatabasePreparer.php b/src/contracts/Bootstrapper/DatabasePreparer.php index dc29c85..aabaad1 100644 --- a/src/contracts/Bootstrapper/DatabasePreparer.php +++ b/src/contracts/Bootstrapper/DatabasePreparer.php @@ -8,8 +8,8 @@ namespace Ibexa\Contracts\Test\Core\Bootstrapper; -use Ibexa\Contracts\Test\Core\IbexaTestKernel; use Symfony\Bundle\FrameworkBundle\Console\Application; +use Symfony\Component\HttpKernel\KernelInterface; /** * @internal @@ -24,7 +24,7 @@ public function __construct() } public function prepareDatabase( - IbexaTestKernel $kernel, + KernelInterface $kernel, bool $runSchemaUpdate ): void { $application = new Application($kernel); diff --git a/src/contracts/Bootstrapper/DatabasePreparerInterface.php b/src/contracts/Bootstrapper/DatabasePreparerInterface.php index f2f1a7e..e1d935b 100644 --- a/src/contracts/Bootstrapper/DatabasePreparerInterface.php +++ b/src/contracts/Bootstrapper/DatabasePreparerInterface.php @@ -8,7 +8,7 @@ namespace Ibexa\Contracts\Test\Core\Bootstrapper; -use Ibexa\Contracts\Test\Core\IbexaTestKernel; +use Symfony\Component\HttpKernel\KernelInterface; /** * @experimental @@ -23,7 +23,7 @@ interface DatabasePreparerInterface * @throws \Exception command failures propagate as-is */ public function prepareDatabase( - IbexaTestKernel $kernel, + KernelInterface $kernel, bool $runSchemaUpdate ): void; } diff --git a/src/contracts/Bootstrapper/DatabaseSchemaHook.php b/src/contracts/Bootstrapper/DatabaseSchemaHook.php index f2d728a..129d047 100644 --- a/src/contracts/Bootstrapper/DatabaseSchemaHook.php +++ b/src/contracts/Bootstrapper/DatabaseSchemaHook.php @@ -8,14 +8,19 @@ namespace Ibexa\Contracts\Test\Core\Bootstrapper; -use Ibexa\Tests\Core\Repository\LegacySchemaImporter; +use Doctrine\DBAL\Connection; +use Ibexa\Bundle\Test\Core\DependencyInjection\CompilerPass\RemoveUnsatisfiableHooksPass; +use Ibexa\Contracts\DoctrineSchema\Builder\SchemaBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; /** - * Imports the legacy raw-SQL schema files exposed by {@see SchemaFilesProviderInterface}. + * Installs the database schema built by {@see SchemaBuilderInterface::buildSchema()}, i.e. from + * every registered bundle's own SchemaBuilderEvent subscriber. Requires `DoctrineSchemaBundle` and + * `IbexaRepositoryInstallerBundle`; removed from the container when either is missing, by + * {@see RemoveUnsatisfiableHooksPass}. * - * Enabled by default; pass `[self::OPTION_LOAD_SCHEMA => false]` as this hook's own options (keyed - * by its own service id in the bootstrap options array) to skip it. + * Enabled by default; pass `[self::OPTION_LOAD_SCHEMA => false]` as this hook's own options to + * skip it. */ final class DatabaseSchemaHook implements HookInterface { @@ -27,16 +32,16 @@ final class DatabaseSchemaHook implements HookInterface public const OPTION_LOAD_SCHEMA = 'load_schema'; - private SchemaFilesProviderInterface $provider; + private SchemaBuilderInterface $schemaBuilder; - private LegacySchemaImporter $schemaImporter; + private Connection $connection; public function __construct( - SchemaFilesProviderInterface $provider, - LegacySchemaImporter $schemaImporter + SchemaBuilderInterface $schemaBuilder, + Connection $connection ) { - $this->provider = $provider; - $this->schemaImporter = $schemaImporter; + $this->schemaBuilder = $schemaBuilder; + $this->connection = $connection; } public function configureOptions(OptionsResolver $resolver): void @@ -52,8 +57,11 @@ public function __invoke(array $options): void return; } - foreach ($this->provider->getSchemaFiles() ?? [] as $file) { - $this->schemaImporter->importSchema($file); + $schema = $this->schemaBuilder->buildSchema(); + $platform = $this->connection->getDatabasePlatform(); + + foreach ($schema->toSql($platform) as $sql) { + $this->connection->executeStatement($sql); } } } diff --git a/src/contracts/Bootstrapper/KernelProvider.php b/src/contracts/Bootstrapper/KernelProvider.php index 78c6d7e..a32338a 100644 --- a/src/contracts/Bootstrapper/KernelProvider.php +++ b/src/contracts/Bootstrapper/KernelProvider.php @@ -8,22 +8,22 @@ namespace Ibexa\Contracts\Test\Core\Bootstrapper; -use Ibexa\Contracts\Test\Core\IbexaTestKernel; use LogicException; +use Symfony\Component\HttpKernel\KernelInterface; /** * @internal */ final class KernelProvider implements KernelProviderInterface { - public function getKernel(?string $kernelClass): IbexaTestKernel + public function getKernel(?string $kernelClass): KernelInterface { $kernelClass ??= $_ENV['KERNEL_CLASS'] ?? $_SERVER['KERNEL_CLASS'] ?? null; - if ($kernelClass === null || !is_a($kernelClass, IbexaTestKernel::class, true)) { + if ($kernelClass === null || !is_a($kernelClass, KernelInterface::class, true)) { throw new LogicException(sprintf( - 'The kernel class "%s" must be a subclass of "%s". Ensure that the KERNEL_CLASS environment variable is set to a valid test kernel class.', + 'The kernel class "%s" must implement "%s". Ensure that the KERNEL_CLASS environment variable is set to a valid test kernel class.', $kernelClass ?? 'null', - IbexaTestKernel::class, + KernelInterface::class, )); } diff --git a/src/contracts/Bootstrapper/KernelProviderInterface.php b/src/contracts/Bootstrapper/KernelProviderInterface.php index abf20a5..d692fe1 100644 --- a/src/contracts/Bootstrapper/KernelProviderInterface.php +++ b/src/contracts/Bootstrapper/KernelProviderInterface.php @@ -8,7 +8,7 @@ namespace Ibexa\Contracts\Test\Core\Bootstrapper; -use Ibexa\Contracts\Test\Core\IbexaTestKernel; +use Symfony\Component\HttpKernel\KernelInterface; /** * @experimental @@ -20,8 +20,8 @@ interface KernelProviderInterface { /** - * @throws \LogicException if $kernelClass (or its env/server fallback) isn't a valid - * IbexaTestKernel subclass + * @throws \LogicException if $kernelClass (or its env/server fallback) doesn't name a + * bootable {@see KernelInterface} implementation */ - public function getKernel(?string $kernelClass): IbexaTestKernel; + public function getKernel(?string $kernelClass): KernelInterface; } diff --git a/src/contracts/Bootstrapper/SchemaFilesProviderInterface.php b/src/contracts/Bootstrapper/SchemaFilesProviderInterface.php deleted file mode 100644 index 7f7a6f7..0000000 --- a/src/contracts/Bootstrapper/SchemaFilesProviderInterface.php +++ /dev/null @@ -1,29 +0,0 @@ -|null null if this provider has nothing to contribute — the caller - * should fall back to another provider, not treat this the same - * as a deliberately empty list - */ - public function getSchemaFiles(): ?iterable; -} diff --git a/src/contracts/IbexaTestKernel.php b/src/contracts/IbexaTestKernel.php index 356d524..c5eeccb 100644 --- a/src/contracts/IbexaTestKernel.php +++ b/src/contracts/IbexaTestKernel.php @@ -12,11 +12,14 @@ use Doctrine\DBAL\Connection; use FOS\JsRoutingBundle\FOSJsRoutingBundle; use Ibexa\Bundle\Core\IbexaCoreBundle; +use Ibexa\Bundle\DoctrineSchema\DoctrineSchemaBundle; use Ibexa\Bundle\LegacySearchEngine\IbexaLegacySearchEngineBundle; +use Ibexa\Bundle\RepositoryInstaller\IbexaRepositoryInstallerBundle; use Ibexa\Bundle\Test\Core\IbexaTestCoreBundle; use Ibexa\Contracts\Core\Persistence\TransactionHandler; use Ibexa\Contracts\Core\Repository; use Ibexa\Contracts\Core\Test\IbexaTestKernelInterface; +use Ibexa\Contracts\DoctrineSchema\Builder\SchemaBuilderInterface; use Ibexa\Contracts\Test\Core\Bootstrapper\Bootstrapper; use Ibexa\Contracts\Test\Core\Bootstrapper\DefaultFixtureProvider; use Ibexa\Contracts\Test\Core\Bootstrapper\DefaultSchemaFilesProvider; @@ -64,6 +67,15 @@ * {@see HooksExecutorInterface} and the built-in hooks it * registers) must `yield new IbexaTestCoreBundle();` from its own registerBundles() override. * + * It does register {@see DoctrineSchemaBundle} and + * {@see IbexaRepositoryInstallerBundle}, which together make + * SchemaBuilderEvent usable: the former provides + * {@see SchemaBuilderInterface}, the latter carries core's + * own BuildSchemaSubscriber (it lives there rather than in IbexaCoreBundle), without which the + * event yields every other package's tables but none of core's. They are a pair — + * IbexaRepositoryInstallerBundle::build() throws if DoctrineSchemaBundle is absent. A subclass must + * therefore NOT yield either of them again; Symfony rejects two bundles with the same name. + * * ## Exposing your services * * To add services to the test Kernel and make them available in tests via IbexaKernelTestCase::getServiceByClassName(), @@ -144,6 +156,8 @@ public function registerBundles(): iterable { yield new SecurityBundle(); yield new IbexaCoreBundle(); + yield new DoctrineSchemaBundle(); + yield new IbexaRepositoryInstallerBundle(); yield new IbexaLegacySearchEngineBundle(); yield new JMSTranslationBundle(); yield new FOSJsRoutingBundle(); diff --git a/src/lib/Bootstrapper/SchemaFilesKernelMethodProvider.php b/src/lib/Bootstrapper/SchemaFilesKernelMethodProvider.php deleted file mode 100644 index 252b25c..0000000 --- a/src/lib/Bootstrapper/SchemaFilesKernelMethodProvider.php +++ /dev/null @@ -1,35 +0,0 @@ -kernel = $kernel; - } - - public function getSchemaFiles(): ?iterable - { - if (!$this->kernel instanceof IbexaTestKernelInterface) { - return null; - } - - return $this->kernel->getSchemaFiles(); - } -} diff --git a/src/lib/Bootstrapper/SchemaFilesParameterProvider.php b/src/lib/Bootstrapper/SchemaFilesParameterProvider.php deleted file mode 100644 index 172dcef..0000000 --- a/src/lib/Bootstrapper/SchemaFilesParameterProvider.php +++ /dev/null @@ -1,36 +0,0 @@ -|null - */ - private ?iterable $schemaFiles; - - /** - * @param iterable|null $schemaFiles - */ - public function __construct(?iterable $schemaFiles) - { - $this->schemaFiles = $schemaFiles; - } - - public function getSchemaFiles(): ?iterable - { - return $this->schemaFiles; - } -} diff --git a/src/lib/Bootstrapper/SchemaFilesProviderChain.php b/src/lib/Bootstrapper/SchemaFilesProviderChain.php deleted file mode 100644 index e187cbd..0000000 --- a/src/lib/Bootstrapper/SchemaFilesProviderChain.php +++ /dev/null @@ -1,45 +0,0 @@ - - */ - private iterable $providers; - - /** - * @param iterable $providers priority-sorted - */ - public function __construct(iterable $providers) - { - $this->providers = $providers; - } - - public function getSchemaFiles(): ?iterable - { - foreach ($this->providers as $provider) { - $schemaFiles = $provider->getSchemaFiles(); - if ($schemaFiles !== null) { - return $schemaFiles; - } - } - - return null; - } -} diff --git a/tests/contracts/Bootstrapper/KernelProviderTest.php b/tests/contracts/Bootstrapper/KernelProviderTest.php index b7b84d1..88a8e86 100644 --- a/tests/contracts/Bootstrapper/KernelProviderTest.php +++ b/tests/contracts/Bootstrapper/KernelProviderTest.php @@ -13,6 +13,8 @@ use LogicException; use PHPUnit\Framework\TestCase; use stdClass; +use Symfony\Component\Config\Loader\LoaderInterface; +use Symfony\Component\HttpKernel\Kernel; /** * @covers \Ibexa\Contracts\Test\Core\Bootstrapper\KernelProvider @@ -52,8 +54,8 @@ protected function tearDown(): void } /** - * @testWith [null, "The kernel class \"null\" must be a subclass of \"Ibexa\\Contracts\\Test\\Core\\IbexaTestKernel\". Ensure that the KERNEL_CLASS environment variable is set to a valid test kernel class."] - * ["stdClass", "The kernel class \"stdClass\" must be a subclass of \"Ibexa\\Contracts\\Test\\Core\\IbexaTestKernel\". Ensure that the KERNEL_CLASS environment variable is set to a valid test kernel class."] + * @testWith [null, "The kernel class \"null\" must implement \"Symfony\\Component\\HttpKernel\\KernelInterface\". Ensure that the KERNEL_CLASS environment variable is set to a valid test kernel class."] + * ["stdClass", "The kernel class \"stdClass\" must implement \"Symfony\\Component\\HttpKernel\\KernelInterface\". Ensure that the KERNEL_CLASS environment variable is set to a valid test kernel class."] */ public function testThrowsWhenKernelClassIsInvalid( ?string $kernelClass, @@ -93,6 +95,14 @@ public function testArgumentTakesPrecedenceOverEnvAndServerFallbacks(): void self::assertInstanceOf(NoopBootTestKernel::class, $kernel); } + public function testAcceptsAnyKernelInterfaceImplementation(): void + { + $kernel = (new KernelProvider())->getKernel(NoopBootPlainKernel::class); + + self::assertInstanceOf(NoopBootPlainKernel::class, $kernel); + self::assertTrue($kernel->didBoot); + } + public function testBootsTheReturnedKernel(): void { $kernel = (new KernelProvider())->getKernel(NoopBootTestKernel::class); @@ -102,6 +112,28 @@ public function testBootsTheReturnedKernel(): void } } +final class NoopBootPlainKernel extends Kernel +{ + public bool $didBoot = false; + + public function __construct() + { + parent::__construct('test', true); + } + + public function boot(): void + { + $this->didBoot = true; + } + + public function registerBundles(): iterable + { + return []; + } + + public function registerContainerConfiguration(LoaderInterface $loader): void {} +} + final class NoopBootTestKernel extends IbexaTestKernel { public bool $didBoot = false; diff --git a/tests/lib/Bootstrapper/SchemaFilesProviderChainTest.php b/tests/lib/Bootstrapper/SchemaFilesProviderChainTest.php deleted file mode 100644 index f94fb3f..0000000 --- a/tests/lib/Bootstrapper/SchemaFilesProviderChainTest.php +++ /dev/null @@ -1,85 +0,0 @@ -providerReturning(null), - $this->providerReturning(['a.sql']), - ]); - - self::assertSame(['a.sql'], $chain->getSchemaFiles()); - } - - public function testStopsAtFirstNonNullResultEvenIfEmpty(): void - { - $chain = new SchemaFilesProviderChain([ - $this->providerReturning([]), - $this->providerReturning(['never-reached.sql']), - ]); - - self::assertSame([], $chain->getSchemaFiles()); - } - - public function testTriesProvidersInTheGivenOrder(): void - { - $chain = new SchemaFilesProviderChain([ - $this->providerReturning(['first.sql']), - $this->providerReturning(['second.sql']), - ]); - - self::assertSame(['first.sql'], $chain->getSchemaFiles()); - } - - public function testReturnsNullWhenNoProviderHasAnAnswer(): void - { - $chain = new SchemaFilesProviderChain([ - $this->providerReturning(null), - $this->providerReturning(null), - ]); - - self::assertNull($chain->getSchemaFiles()); - } - - /** - * @param iterable|null $schemaFiles - */ - private function providerReturning(?iterable $schemaFiles): SchemaFilesProviderInterface - { - return new class($schemaFiles) implements SchemaFilesProviderInterface { - /** - * @var iterable|null - */ - private ?iterable $schemaFiles; - - /** - * @param iterable|null $schemaFiles - */ - public function __construct(?iterable $schemaFiles) - { - $this->schemaFiles = $schemaFiles; - } - - public function getSchemaFiles(): ?iterable - { - return $this->schemaFiles; - } - }; - } -}