diff --git a/composer.json b/composer.json index b3dfccf7ee..35bb66367f 100644 --- a/composer.json +++ b/composer.json @@ -43,6 +43,8 @@ "nelmio/cors-bundle": "^2.0", "pagerfanta/pagerfanta": "^2.1", "ocramius/proxy-manager": "^2.2", + "doctrine/dbal": "^2.13.0", + "doctrine/orm": "^2.7", "doctrine/doctrine-bundle": "^2.0", "liip/imagine-bundle": "^2.3", "oneup/flysystem-bundle": "^3.4", diff --git a/eZ/Bundle/EzPublishCoreBundle/Command/ReindexCommand.php b/eZ/Bundle/EzPublishCoreBundle/Command/ReindexCommand.php index 1d8acd2155..f29740111e 100644 --- a/eZ/Bundle/EzPublishCoreBundle/Command/ReindexCommand.php +++ b/eZ/Bundle/EzPublishCoreBundle/Command/ReindexCommand.php @@ -11,10 +11,10 @@ use const DIRECTORY_SEPARATOR; use Doctrine\DBAL\Connection; use eZ\Publish\Core\Base\Exceptions\InvalidArgumentException; -use eZ\Publish\SPI\Persistence\Content\ContentInfo; use eZ\Publish\Core\Search\Common\Indexer; use eZ\Publish\Core\Search\Common\IncrementalIndexer; -use Doctrine\DBAL\Driver\Statement; +use eZ\Publish\SPI\Search\Content\IndexerGateway; +use Generator; use eZ\Publish\SPI\Persistence\Content\Location\Handler; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Command\Command; @@ -26,19 +26,12 @@ use Symfony\Component\Process\Process; use RuntimeException; use DateTime; -use PDO; class ReindexCommand extends Command implements BackwardCompatibleCommand { /** @var \eZ\Publish\Core\Search\Common\Indexer|\eZ\Publish\Core\Search\Common\IncrementalIndexer */ private $searchIndexer; - /** @var \Doctrine\DBAL\Connection */ - private $connection; - - /** @var \eZ\Publish\SPI\Persistence\Content\Location\Handler */ - private $locationHandler; - /** @var string */ private $phpPath; @@ -57,20 +50,17 @@ class ReindexCommand extends Command implements BackwardCompatibleCommand /** @var string */ private $projectDir; - /** - * @param \eZ\Publish\Core\Search\Common\IncrementalIndexer|\eZ\Publish\Core\Search\Common\Indexer $searchIndexer - * @param \Doctrine\DBAL\Connection $connection - * @param \eZ\Publish\SPI\Persistence\Content\Location\Handler $locationHandler - * @param \Psr\Log\LoggerInterface $logger - * @param string $siteaccess - * @param string $env - * @param bool $isDebug - * @param string|null $phpPath - */ + /** @var \eZ\Publish\SPI\Search\Content\IndexerGateway */ + private $gateway; + + /** @var \eZ\Publish\SPI\Persistence\Content\Location\Handler */ + private $locationHandler; + public function __construct( $searchIndexer, Connection $connection, Handler $locationHandler, + IndexerGateway $gateway, LoggerInterface $logger, string $siteaccess, string $env, @@ -78,8 +68,8 @@ public function __construct( string $projectDir, string $phpPath = null ) { + $this->gateway = $gateway; $this->searchIndexer = $searchIndexer; - $this->connection = $connection; $this->locationHandler = $locationHandler; $this->phpPath = $phpPath; $this->logger = $logger; @@ -259,16 +249,18 @@ protected function indexIncrementally( } if ($since = $input->getOption('since')) { - $stmt = $this->getStatementContentSince(new DateTime($since)); - $count = (int)$this->getStatementContentSince(new DateTime($since), true)->fetchColumn(); + $count = $this->gateway->countContentSince(new DateTime($since)); + $generator = $this->gateway->getContentSince(new DateTime($since), $iterationCount); $purge = false; } elseif ($locationId = (int) $input->getOption('subtree')) { - $stmt = $this->getStatementSubtree($locationId); - $count = (int) $this->getStatementSubtree($locationId, true)->fetchColumn(); + /** @var \eZ\Publish\SPI\Persistence\Content\Location\Handler */ + $location = $this->locationHandler->load($locationId); + $count = $this->gateway->countContentInSubtree($location->pathString); + $generator = $this->gateway->getContentInSubtree($location->pathString, $iterationCount); $purge = false; } else { - $stmt = $this->getStatementContentAll(); - $count = (int) $this->getStatementContentAll(true)->fetchColumn(); + $count = $this->gateway->countAllContent(); + $generator = $this->gateway->getAllContent($iterationCount); $purge = !$input->getOption('no-purge'); } @@ -302,10 +294,15 @@ protected function indexIncrementally( $progress->start($iterations); if ($processCount > 1) { - $this->runParallelProcess($progress, $stmt, (int) $processCount, (int) $iterationCount, $commit); + $this->runParallelProcess( + $progress, + $generator, + (int)$processCount, + $commit + ); } else { // if we only have one process, or less iterations to warrant running several, we index it all inline - foreach ($this->fetchIteration($stmt, $iterationCount) as $contentIds) { + foreach ($generator as $contentIds) { $this->searchIndexer->updateSearchIndex($contentIds, $commit); $progress->advance(1); } @@ -326,14 +323,12 @@ protected function indexIncrementally( */ private function runParallelProcess( ProgressBar $progress, - Statement $stmt, + Generator $generator, int $processCount, - int $iterationCount, bool $commit ): void { /** @var \Symfony\Component\Process\Process[]|null[] */ $processes = array_fill(0, $processCount, null); - $generator = $this->fetchIteration($stmt, $iterationCount); do { /** @var \Symfony\Component\Process\Process $process */ foreach ($processes as $key => $process) { @@ -372,88 +367,6 @@ private function runParallelProcess( } while (!empty($processes)); } - /** - * @param DateTime $since - * @param bool $count - * - * @return \Doctrine\DBAL\Driver\Statement - */ - private function getStatementContentSince(DateTime $since, $count = false) - { - $q = $this->connection->createQueryBuilder() - ->select($count ? 'count(c.id)' : 'c.id') - ->from('ezcontentobject', 'c') - ->where('c.status = :status')->andWhere('c.modified >= :since') - ->orderBy('c.modified') - ->setParameter('status', ContentInfo::STATUS_PUBLISHED, PDO::PARAM_INT) - ->setParameter('since', $since->getTimestamp(), PDO::PARAM_INT); - - return $q->execute(); - } - - /** - * @param mixed $locationId - * @param bool $count - * - * @return \Doctrine\DBAL\Driver\Statement - * - * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - */ - private function getStatementSubtree($locationId, $count = false) - { - $location = $this->locationHandler->load($locationId); - $q = $this->connection->createQueryBuilder() - ->select($count ? 'count(DISTINCT c.id)' : 'DISTINCT c.id') - ->from('ezcontentobject', 'c') - ->innerJoin('c', 'ezcontentobject_tree', 't', 't.contentobject_id = c.id') - ->where('c.status = :status') - ->andWhere('t.path_string LIKE :path') - ->setParameter('status', ContentInfo::STATUS_PUBLISHED, PDO::PARAM_INT) - ->setParameter('path', $location->pathString . '%', PDO::PARAM_STR); - - return $q->execute(); - } - - /** - * @param bool $count - * - * @return \Doctrine\DBAL\Driver\Statement - */ - private function getStatementContentAll($count = false) - { - $q = $this->connection->createQueryBuilder() - ->select($count ? 'count(c.id)' : 'c.id') - ->from('ezcontentobject', 'c') - ->where('c.status = :status') - ->setParameter('status', ContentInfo::STATUS_PUBLISHED, PDO::PARAM_INT); - - return $q->execute(); - } - - /** - * @param \Doctrine\DBAL\Driver\Statement $stmt - * @param int $iterationCount - * - * @return \Generator Return an array of arrays, each array contains content id's of $iterationCount. - */ - private function fetchIteration(Statement $stmt, $iterationCount) - { - do { - $contentIds = []; - for ($i = 0; $i < $iterationCount; ++$i) { - if ($contentId = $stmt->fetch(PDO::FETCH_COLUMN)) { - $contentIds[] = $contentId; - } elseif (empty($contentIds)) { - return; - } else { - break; - } - } - - yield $contentIds; - } while (!empty($contentId)); - } - /** * @param array $contentIds * diff --git a/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/Compiler/EntityManagerFactoryServiceLocatorPass.php b/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/Compiler/EntityManagerFactoryServiceLocatorPass.php new file mode 100644 index 0000000000..6a47d162f4 --- /dev/null +++ b/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/Compiler/EntityManagerFactoryServiceLocatorPass.php @@ -0,0 +1,42 @@ +getDefinition('ibexa.doctrine.orm.entity_manager_factory'); + + $ibexaEntityManagers = $this->getIbexaEntityManagers($container); + $entityManagerFactory->setArgument( + '$serviceLocator', + ServiceLocatorTagPass::register($container, $ibexaEntityManagers) + ); + } + + private function getIbexaEntityManagers(ContainerBuilder $container): array + { + $entityManagers = []; + foreach ($container->getParameter('doctrine.entity_managers') as $name => $serviceId) { + if (false === strpos($name, 'ibexa_')) { + continue; + } + + $entityManagers[$serviceId] = new Reference($serviceId); + } + + return $entityManagers; + } +} diff --git a/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/Compiler/InjectEntityManagerMappingsPass.php b/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/Compiler/InjectEntityManagerMappingsPass.php new file mode 100644 index 0000000000..5f21d22d25 --- /dev/null +++ b/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/Compiler/InjectEntityManagerMappingsPass.php @@ -0,0 +1,156 @@ +getParameter('doctrine.entity_managers'); + $entityMappings = $container->getParameter('ibexa.orm.entity_mappings'); + + $mappingDriverConfig = $this->prepareMappingDriverConfig($entityMappings, $container); + + foreach ($entityManagers as $entityManagerName => $serviceName) { + if (strpos($entityManagerName, 'ibexa_') !== 0) { + continue; + } + + $chainMetadataDriverDefinition = $container->getDefinition( + sprintf('doctrine.orm.%s_metadata_driver', $entityManagerName) + ); + + $ormConfigDefinition = $container->getDefinition( + sprintf('doctrine.orm.%s_configuration', $entityManagerName) + ); + + $entityMap = $this->getEntityMapForConfigurationService($entityMappings); + $ormConfigDefinition->addMethodCall('setEntityNamespaces', [$entityMap]); + + foreach ($mappingDriverConfig as $driverType => $driverPaths) { + $metadataDriverServiceName = "doctrine.orm.{$entityManagerName}_{$driverType}_metadata_driver"; + $metadataDriverDefinition = $this->createMetadataDriverDefinition($driverType, $driverPaths); + + if ( + false !== strpos($metadataDriverDefinition->getClass(), 'yml') + || false !== strpos($metadataDriverDefinition->getClass(), 'xml') + ) { + $metadataDriverDefinition->setArguments([array_flip($driverPaths)]); + $metadataDriverDefinition->addMethodCall('setGlobalBasename', ['mapping']); + } + + $container->setDefinition($metadataDriverServiceName, $metadataDriverDefinition); + + foreach ($driverPaths as $prefix => $driverPath) { + $chainMetadataDriverDefinition->addMethodCall( + 'addDriver', + [new Reference($metadataDriverServiceName), $prefix] + ); + } + } + } + } + + private function createMetadataDriverDefinition($driverType, $driverPaths): Definition + { + $metadataDriver = new Definition("%doctrine.orm.metadata.{$driverType}.class%"); + $arguments = []; + + if ('annotation' === $driverType) { + $arguments[] = new Reference('doctrine.orm.metadata.annotation_reader'); + } + + $arguments[] = array_values($driverPaths); + + $metadataDriver->setArguments($arguments); + $metadataDriver->setPublic(false); + + return $metadataDriver; + } + + private function prepareMappingDriverConfig(array $entityManagerConfig, ContainerBuilder $container): array + { + $bundles = $container->getParameter('kernel.bundles'); + $driverConfig = []; + foreach ($entityManagerConfig as $mappingName => $config) { + $config = array_replace([ + 'dir' => false, + 'type' => false, + 'prefix' => false, + ], (array) $config); + + $config['dir'] = $container->getParameterBag()->resolveValue($config['dir']); + + if ($config['is_bundle']) { + $bundle = null; + foreach ($bundles as $bundleName => $class) { + if ($mappingName === $bundleName) { + $bundle = new \ReflectionClass($class); + + break; + } + } + + if (null === $bundle) { + throw new \InvalidArgumentException(sprintf( + 'Bundle "%s" does not exist or it is not enabled.', + $mappingName) + ); + } + + $config = $this->getMappingDriverBundleConfigDefaults($config, $bundle); + } + + if (!is_dir($config['dir'])) { + throw new \InvalidArgumentException(sprintf( + 'Invalid Doctrine mapping path given. Cannot load Doctrine mapping/bundle named "%s".', + $mappingName + )); + } + + $driverConfig[$config['type']][$config['prefix']] = realpath($config['dir']) ?: $config['dir']; + } + + return $driverConfig; + } + + private function getMappingDriverBundleConfigDefaults( + array $bundleConfig, + \ReflectionClass $bundle + ): array { + $bundleDir = \dirname($bundle->getFileName()); + + if (!$bundleConfig['type'] || !$bundleConfig['dir'] || !$bundleConfig['prefix']) { + throw new \InvalidArgumentException( + "Entity Mapping has invalid configuration. Please provide 'type', 'dir' and 'prefix' parameters." + ); + } + + $bundleConfig['dir'] = $bundleDir . '/' . $bundleConfig['dir']; + + return $bundleConfig; + } + + private function getEntityMapForConfigurationService(array $entityMappings): array + { + return array_combine( + array_keys($entityMappings), + array_column($entityMappings, 'prefix') + ); + } +} diff --git a/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/Compiler/LazyDoctrineRepositoriesPass.php b/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/Compiler/LazyDoctrineRepositoriesPass.php new file mode 100644 index 0000000000..7c6adbe81b --- /dev/null +++ b/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/Compiler/LazyDoctrineRepositoriesPass.php @@ -0,0 +1,57 @@ +getDefinitions() as $serviceId => $definition) { + if (!is_array($definition->getFactory())) { + continue; + } + + $factory = $definition->getFactory(); + if (!is_string($factory[0]) && !$factory[0] instanceof Reference) { + continue; + } + + $factoryServiceId = (string) $factory[0]; + + if ($factoryServiceId !== 'ibexa.doctrine.orm.entity_manager') { + continue; + } + + if ($definition->isLazy()) { + continue; + } + + $nonLazyServices[] = $serviceId; + } + + if (empty($nonLazyServices)) { + return; + } + + throw new RuntimeException( + sprintf( + 'Services: "%s" have a dependency on repository aware Entity Manager. ' + . 'To prevent premature Entity Manager initialization before siteaccess is resolved ' + . "you need to mark these services as 'lazy'.", + implode('", "', $nonLazyServices) + ) + ); + } +} diff --git a/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/Configuration.php b/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/Configuration.php index 5c53cb9dfd..923bebd88d 100644 --- a/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/Configuration.php +++ b/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/Configuration.php @@ -55,6 +55,7 @@ public function getConfigTreeBuilder() $this->addUrlAliasSection($rootNode); $this->addImagePlaceholderSection($rootNode); $this->addUrlWildcardsSection($rootNode); + $this->addOrmSection($rootNode); // Delegate SiteAccess config to configuration parsers $this->mainConfigParser->addSemanticConfig($this->generateScopeBaseNode($rootNode)); @@ -519,4 +520,55 @@ private function addUrlWildcardsSection($rootNode): ArrayNodeDefinition ->end() ->end(); } + + /** + * Defines configuration for Doctrine ORM. + * + * The configuration is available at: + * + * ezpublish: + * orm: + * entity_mappings: + * EzPublishCoreBundle: + * is_bundle: true + * type: annotation + * dir: Entity + * prefix: eZ\Bundle\EzPublishCoreBundle\Entity + * + * + * + * @param \Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition $rootNode + * + * @return \Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition + */ + private function addOrmSection($rootNode): ArrayNodeDefinition + { + return $rootNode + ->children() + ->arrayNode('orm') + ->children() + ->arrayNode('entity_mappings') + ->info('Entity Mapping configuration compatible with Doctrine ORM bundle') + ->useAttributeAsKey('name') + ->defaultValue([]) + ->arrayPrototype() + ->children() + ->booleanNode('is_bundle')->defaultTrue()->end() + ->booleanNode('mapping')->defaultTrue()->end() + ->scalarNode('type') + ->isRequired() + ->end() + ->scalarNode('dir') + ->isRequired() + ->end() + ->scalarNode('prefix') + ->isRequired() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end(); + } } diff --git a/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/EzPublishCoreExtension.php b/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/EzPublishCoreExtension.php index a5e8a6f531..8371566320 100644 --- a/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/EzPublishCoreExtension.php +++ b/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/EzPublishCoreExtension.php @@ -33,6 +33,11 @@ class EzPublishCoreExtension extends Extension implements PrependExtensionInterface { + private const ENTITY_MANAGER_TEMPLATE = [ + 'connection' => null, + 'mappings' => [], + ]; + /** @var \eZ\Bundle\EzPublishCoreBundle\DependencyInjection\Configuration\Suggestion\Collector\SuggestionCollector */ private $suggestionCollector; @@ -113,6 +118,7 @@ public function load(array $configs, ContainerBuilder $container) $this->registerImageMagickConfiguration($config, $container); $this->registerUrlAliasConfiguration($config, $container); $this->registerUrlWildcardsConfiguration($config, $container); + $this->registerOrmConfiguration($config, $container); // Routing $this->handleRouting($config, $container, $loader); @@ -166,6 +172,7 @@ public function getConfiguration(array $config, ContainerBuilder $container) public function prepend(ContainerBuilder $container) { $this->prependTranslatorConfiguration($container); + $this->prependDoctrineConfiguration($container); } /** @@ -268,6 +275,16 @@ private function registerImageMagickConfiguration(array $config, ContainerBuilde $container->setParameter('ezpublish.image.imagemagick.filters', $filters); } + private function registerOrmConfiguration(array $config, ContainerBuilder $container): void + { + if (!isset($config['orm']['entity_mappings'])) { + return; + } + + $entityMappings = $config['orm']['entity_mappings']; + $container->setParameter('ibexa.orm.entity_mappings', $entityMappings); + } + /** * Handle routing parameters. * @@ -550,6 +567,61 @@ private function prependTranslatorConfiguration(ContainerBuilder $container) } } + private function prependDoctrineConfiguration(ContainerBuilder $container): void + { + if (!$container->hasExtension('doctrine')) { + return; + } + + $kernelConfigs = array_merge( + $container->getExtensionConfig('ezpublish'), + $container->getExtensionConfig('ezplatform') + ); + $entityMappings = []; + + $repositoryConnections = []; + foreach ($kernelConfigs as $config) { + if (isset($config['orm']['entity_mappings'])) { + $entityMappings[] = $config['orm']['entity_mappings']; + } + + if (isset($config['repositories'])) { + $repositoryConnections[] = array_map( + static function (array $repository): ?string { + return $repository['storage']['connection'] + ?? 'default'; + }, + $config['repositories'] + ); + } + } + + // compose clean array with all connection identifiers + $connections = array_values( + array_filter( + array_unique( + array_merge(...$repositoryConnections) ?? []) + ) + ); + + $doctrineConfig = [ + 'orm' => [ + 'entity_managers' => [], + ], + ]; + + $entityMappingConfig = !empty($entityMappings) ? array_merge_recursive(...$entityMappings) : []; + + foreach ($connections as $connection) { + $doctrineConfig['orm']['entity_managers'][sprintf('ibexa_%s', $connection)] = array_merge( + self::ENTITY_MANAGER_TEMPLATE, + ['connection' => $connection, 'mappings' => $entityMappingConfig] + ); + } + + $container->prependExtensionConfig('doctrine', $doctrineConfig); + } + /** * @param array $config * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container diff --git a/eZ/Bundle/EzPublishCoreBundle/Entity/EntityManagerFactory.php b/eZ/Bundle/EzPublishCoreBundle/Entity/EntityManagerFactory.php new file mode 100644 index 0000000000..362fd546c9 --- /dev/null +++ b/eZ/Bundle/EzPublishCoreBundle/Entity/EntityManagerFactory.php @@ -0,0 +1,71 @@ + */ + private $entityManagers; + + public function __construct( + RepositoryConfigurationProvider $repositoryConfigurationProvider, + ServiceLocator $serviceLocator, + string $defaultConnection, + array $entityManagers + ) { + $this->repositoryConfigurationProvider = $repositoryConfigurationProvider; + $this->serviceLocator = $serviceLocator; + $this->defaultConnection = $defaultConnection; + $this->entityManagers = $entityManagers; + } + + public function getEntityManager(): EntityManagerInterface + { + $repositoryConfig = $this->repositoryConfigurationProvider->getRepositoryConfig(); + + if (isset($repositoryConfig['storage']['connection'])) { + $entityManagerId = $this->getEntityManagerServiceId($repositoryConfig['storage']['connection']); + } else { + $defaultEntityManagerId = $this->getEntityManagerServiceId($this->defaultConnection); + $entityManagerId = $this->serviceLocator->has($defaultEntityManagerId) + ? $defaultEntityManagerId + : 'doctrine.orm.entity_manager'; + } + + if (!$this->serviceLocator->has($entityManagerId)) { + throw new \InvalidArgumentException( + "Invalid Doctrine Entity Manager '{$entityManagerId}' for Repository '{$repositoryConfig['alias']}'. " . + 'Valid Entity Managers are: ' . implode(', ', array_keys($this->entityManagers)) + ); + } + + return $this->serviceLocator->get($entityManagerId); + } + + protected function getEntityManagerServiceId(string $connection): string + { + return sprintf('doctrine.orm.ibexa_%s_entity_manager', $connection); + } +} diff --git a/eZ/Bundle/EzPublishCoreBundle/EzPublishCoreBundle.php b/eZ/Bundle/EzPublishCoreBundle/EzPublishCoreBundle.php index d149c018d0..31e84a8735 100644 --- a/eZ/Bundle/EzPublishCoreBundle/EzPublishCoreBundle.php +++ b/eZ/Bundle/EzPublishCoreBundle/EzPublishCoreBundle.php @@ -9,9 +9,12 @@ use eZ\Bundle\EzPublishCoreBundle\DependencyInjection\Compiler\BinaryContentDownloadPass; use eZ\Bundle\EzPublishCoreBundle\DependencyInjection\Compiler\ConsoleCacheWarmupPass; use eZ\Bundle\EzPublishCoreBundle\DependencyInjection\Compiler\ConsoleCommandPass; +use eZ\Bundle\EzPublishCoreBundle\DependencyInjection\Compiler\EntityManagerFactoryServiceLocatorPass; use eZ\Bundle\EzPublishCoreBundle\DependencyInjection\Compiler\FieldTypeParameterProviderRegistryPass; use eZ\Bundle\EzPublishCoreBundle\DependencyInjection\Compiler\FragmentPass; use eZ\Bundle\EzPublishCoreBundle\DependencyInjection\Compiler\ViewMatcherRegistryPass; +use eZ\Bundle\EzPublishCoreBundle\DependencyInjection\Compiler\InjectEntityManagerMappingsPass; +use eZ\Bundle\EzPublishCoreBundle\DependencyInjection\Compiler\LazyDoctrineRepositoriesPass; use eZ\Bundle\EzPublishCoreBundle\DependencyInjection\Compiler\NotificationRendererPass; use eZ\Bundle\EzPublishCoreBundle\DependencyInjection\Compiler\PlaceholderProviderPass; use eZ\Bundle\EzPublishCoreBundle\DependencyInjection\Compiler\ImaginePass; @@ -76,6 +79,9 @@ public function build(ContainerBuilder $container) $container->addCompilerPass(new ViewMatcherRegistryPass()); $container->addCompilerPass(new SiteAccessMatcherRegistryPass()); $container->addCompilerPass(new ConsoleCommandPass()); + $container->addCompilerPass(new LazyDoctrineRepositoriesPass(), PassConfig::TYPE_BEFORE_REMOVING); + $container->addCompilerPass(new EntityManagerFactoryServiceLocatorPass()); + $container->addCompilerPass(new InjectEntityManagerMappingsPass()); // Storage passes $container->addCompilerPass(new ExternalStorageRegistryPass()); diff --git a/eZ/Bundle/EzPublishCoreBundle/Resources/config/commands.yml b/eZ/Bundle/EzPublishCoreBundle/Resources/config/commands.yml index 9888bdde92..298ffb5e3e 100644 --- a/eZ/Bundle/EzPublishCoreBundle/Resources/config/commands.yml +++ b/eZ/Bundle/EzPublishCoreBundle/Resources/config/commands.yml @@ -31,3 +31,16 @@ services: - '@ezpublish.api.search_engine.legacy.connection' tags: - { name: console.command } + + eZ\Bundle\EzPublishCoreBundle\Command\ReindexCommand: + autowire: true + autoconfigure: true + arguments: + $searchIndexer: '@ezpublish.spi.search.indexer' + $locationHandler: '@ezpublish.spi.persistence.location_handler' + $siteaccess: '@ezpublish.siteaccess' + $env: '%kernel.environment%' + $projectDir: '%kernel.project_dir%' + $isDebug: '%kernel.debug%' + tags: + - { name: console.command, command: ezplatform:reindex } diff --git a/eZ/Bundle/EzPublishCoreBundle/Resources/config/default_settings.yml b/eZ/Bundle/EzPublishCoreBundle/Resources/config/default_settings.yml index 433d6513b5..eb9d925118 100644 --- a/eZ/Bundle/EzPublishCoreBundle/Resources/config/default_settings.yml +++ b/eZ/Bundle/EzPublishCoreBundle/Resources/config/default_settings.yml @@ -248,3 +248,7 @@ parameters: # SiteAccesses, indexed by language. ezpublish.siteaccesses_by_language: {} + ## + # Siteaccess aware Entity Manager + ## + ibexa.orm.entity_mappings: [] diff --git a/eZ/Bundle/EzPublishCoreBundle/Resources/config/papi.yml b/eZ/Bundle/EzPublishCoreBundle/Resources/config/papi.yml index 6dc24ac8f7..8f1e248e3d 100644 --- a/eZ/Bundle/EzPublishCoreBundle/Resources/config/papi.yml +++ b/eZ/Bundle/EzPublishCoreBundle/Resources/config/papi.yml @@ -70,3 +70,6 @@ services: - '@ezpublish.spi.search' tags: - { name: kernel.event_subscriber } + + eZ\Publish\SPI\Search\Content\IndexerGateway: + alias: eZ\Publish\Core\Search\Legacy\Content\IndexerGateway diff --git a/eZ/Bundle/EzPublishCoreBundle/Resources/config/services.yml b/eZ/Bundle/EzPublishCoreBundle/Resources/config/services.yml index a8490e4dd7..bbd47f3adf 100644 --- a/eZ/Bundle/EzPublishCoreBundle/Resources/config/services.yml +++ b/eZ/Bundle/EzPublishCoreBundle/Resources/config/services.yml @@ -356,3 +356,15 @@ services: $isDebug: '%kernel.debug%' tags: - { name: console.command } + + ibexa.doctrine.orm.entity_manager: + class: Doctrine\ORM\EntityManager + lazy: true + factory: ['@ibexa.doctrine.orm.entity_manager_factory', 'getEntityManager'] + + ibexa.doctrine.orm.entity_manager_factory: + class: eZ\Bundle\EzPublishCoreBundle\Entity\EntityManagerFactory + arguments: + $repositoryConfigurationProvider: '@ezpublish.api.repository_configuration_provider' + $defaultConnection: '%doctrine.default_connection%' + $entityManagers: '%doctrine.entity_managers%' diff --git a/eZ/Bundle/EzPublishCoreBundle/Tests/DependencyInjection/Compiler/EntityManagerFactoryServiceLocatorPassTest.php b/eZ/Bundle/EzPublishCoreBundle/Tests/DependencyInjection/Compiler/EntityManagerFactoryServiceLocatorPassTest.php new file mode 100644 index 0000000000..8db32a0194 --- /dev/null +++ b/eZ/Bundle/EzPublishCoreBundle/Tests/DependencyInjection/Compiler/EntityManagerFactoryServiceLocatorPassTest.php @@ -0,0 +1,67 @@ +setDefinition( + 'ibexa.doctrine.orm.entity_manager_factory', + new Definition(null, [ + '$repositoryConfigurationProvider' => new Reference('ezpublish.api.repository_configuration_provider'), + '$defaultConnection' => '%doctrine.default_connection%', + '$entityManagers' => '%doctrine.entity_managers%', + ]) + ); + $this->setParameter('doctrine.entity_managers', [ + 'default' => 'doctrine.orm.default_entity_manager', + 'ibexa_second_connection' => 'doctrine.orm.ibexa_second_connection_entity_manager', + ]); + $this->setParameter('doctrine.default_connection', 'default'); + } + + protected function registerCompilerPass(ContainerBuilder $container): void + { + $container->addCompilerPass(new EntityManagerFactoryServiceLocatorPass()); + } + + public function testAddServiceLocatorArgument(): void + { + $this->compile(); + + $definition = $this->container->getDefinition('ibexa.doctrine.orm.entity_manager_factory'); + $arguments = $definition->getArguments(); + + self::assertArrayHasKey('$serviceLocator', $arguments); + + $serviceLocatorServiceId = (string) $arguments['$serviceLocator']; + + $expectedEntityManagers = [ + 'doctrine.orm.ibexa_second_connection_entity_manager' => new ServiceClosureArgument( + new Reference('doctrine.orm.ibexa_second_connection_entity_manager') + ), + ]; + + $this->assertContainerBuilderHasServiceDefinitionWithArgument( + $serviceLocatorServiceId, + 0, + $expectedEntityManagers + ); + } +} diff --git a/eZ/Bundle/EzPublishCoreBundle/Tests/DependencyInjection/Compiler/InjectEntityManagerMappingPassTest.php b/eZ/Bundle/EzPublishCoreBundle/Tests/DependencyInjection/Compiler/InjectEntityManagerMappingPassTest.php new file mode 100644 index 0000000000..b6edde6c5b --- /dev/null +++ b/eZ/Bundle/EzPublishCoreBundle/Tests/DependencyInjection/Compiler/InjectEntityManagerMappingPassTest.php @@ -0,0 +1,102 @@ + AnnotationEntityBundle::class, + 'XmlEntityBundle' => XmlEntityBundle::class, + ]; + private const ENTITY_MANAGERS = ['ibexa_connection' => 'doctrine.orm.ibexa_connection_entity_manager']; + private const ENTITY_MAPPINGS = [ + 'AnnotationEntityBundle' => [ + 'is_bundle' => true, + 'type' => 'annotation', + 'dir' => 'Entity', + 'prefix' => '\eZ\Bundle\EzPublishCoreBundle\Tests\DependencyInjection\Stub\AnnotationEntityBundle\Entity', + ], + 'XmlEntityBundle' => [ + 'is_bundle' => true, + 'type' => 'xml', + 'dir' => 'config', + 'prefix' => '\eZ\Bundle\EzPublishCoreBundle\Tests\DependencyInjection\Stub\XmlEntityBundle\XmlEntityBundle\Entity', + ], + ]; + + protected function setUp(): void + { + parent::setUp(); + + $this->setDefinition('doctrine.orm.ibexa_connection_metadata_driver', new Definition()); + $this->setDefinition('doctrine.orm.ibexa_connection_configuration', new Definition()); + $this->setParameter('doctrine.orm.metadata.annotation.class', 'Vendor/Doctrine/Metadata/Driver/AnnotationDriver'); + $this->setParameter('doctrine.orm.metadata.yml.class', 'Vendor/Doctrine/Metadata/Driver/YmlDriver'); + $this->setParameter('doctrine.orm.metadata.xml.class', 'Vendor/Doctrine/Metadata/Driver/XmlDriver'); + $this->setParameter('kernel.bundles', self::BUNDLES); + + $this->setParameter('doctrine.entity_managers', self::ENTITY_MANAGERS); + $this->setParameter('ibexa.orm.entity_mappings', self::ENTITY_MAPPINGS); + } + + protected function registerCompilerPass(ContainerBuilder $container): void + { + $container->addCompilerPass(new InjectEntityManagerMappingsPass()); + } + + public function testInjectEntityMapping(): void + { + $this->compile(); + + $expectedDriverPaths = [ + 'AnnotationEntityBundle' => [ + realpath(__DIR__ . '/../Stub/AnnotationEntityBundle/' . self::ENTITY_MAPPINGS['AnnotationEntityBundle']['dir']), + ], + 'XmlEntityBundle' => [ + realpath(__DIR__ . '/../Stub/XmlEntityBundle/' . self::ENTITY_MAPPINGS['XmlEntityBundle']['dir']) => sprintf('\\%s\Entity', XmlEntityBundle::class), + ], + ]; + + $expectedEntityNamespaces = [ + 'AnnotationEntityBundle' => self::ENTITY_MAPPINGS['AnnotationEntityBundle']['prefix'], + 'XmlEntityBundle' => self::ENTITY_MAPPINGS['XmlEntityBundle']['prefix'], + ]; + + foreach (self::ENTITY_MANAGERS as $name => $serviceId) { + $this->assertContainerBuilderHasService("doctrine.orm.{$name}_metadata_driver"); + $this->assertContainerBuilderHasServiceDefinitionWithMethodCall( + "doctrine.orm.{$name}_configuration", + 'setEntityNamespaces', + [$expectedEntityNamespaces] + ); + + foreach (self::ENTITY_MAPPINGS as $mappingName => $config) { + $metadataDriver = "doctrine.orm.{$name}_{$config['type']}_metadata_driver"; + $this->assertContainerBuilderHasServiceDefinitionWithArgument( + $metadataDriver, + 'annotation' === $config['type'] ? 1 : 0, + $expectedDriverPaths[$mappingName] + ); + $this->assertContainerBuilderHasServiceDefinitionWithMethodCall( + "doctrine.orm.{$name}_metadata_driver", + 'addDriver', + [new Reference($metadataDriver), $config['prefix']] + ); + } + } + } +} diff --git a/eZ/Bundle/EzPublishCoreBundle/Tests/DependencyInjection/Compiler/LazyDoctrineRepositoriesPassTest.php b/eZ/Bundle/EzPublishCoreBundle/Tests/DependencyInjection/Compiler/LazyDoctrineRepositoriesPassTest.php new file mode 100644 index 0000000000..cd74483d5b --- /dev/null +++ b/eZ/Bundle/EzPublishCoreBundle/Tests/DependencyInjection/Compiler/LazyDoctrineRepositoriesPassTest.php @@ -0,0 +1,50 @@ +addCompilerPass(new LazyDoctrineRepositoriesPass()); + } + + public function testNonLazyServices(): void + { + $myServiceWithEntityManagerFactory = new Definition(); + $myServiceWithEntityManagerFactory->setFactory( + [new Reference('ibexa.doctrine.orm.entity_manager'), 'getEntityManager'] + ); + + $myLazyServiceWithEntityManagerFactory = new Definition(); + $myLazyServiceWithEntityManagerFactory->setLazy(true); + $myLazyServiceWithEntityManagerFactory->setFactory( + [new Reference('ibexa.doctrine.orm.entity_manager'), 'getEntityManager'] + ); + + $myServiceWithFactory = new Definition(); + $myServiceWithFactory->setFactory([new Reference('my_factory'), 'getService']); + $myServiceWithFactory->setLazy(true); + + $this->setDefinition('my_service', $myServiceWithFactory); + $this->setDefinition('my_entity_manager', $myServiceWithEntityManagerFactory); + $this->setDefinition('my_lazy_entity_manager', $myLazyServiceWithEntityManagerFactory); + + $this->expectException(RuntimeException::class); + + $this->compile(); + } +} diff --git a/eZ/Bundle/EzPublishCoreBundle/Tests/DependencyInjection/Stub/AnnotationEntityBundle/AnnotationEntityBundle.php b/eZ/Bundle/EzPublishCoreBundle/Tests/DependencyInjection/Stub/AnnotationEntityBundle/AnnotationEntityBundle.php new file mode 100644 index 0000000000..9434f2fb41 --- /dev/null +++ b/eZ/Bundle/EzPublishCoreBundle/Tests/DependencyInjection/Stub/AnnotationEntityBundle/AnnotationEntityBundle.php @@ -0,0 +1,15 @@ + self::DEFAULT_ENTITY_MANAGER, + 'ibexa_invalid' => self::INVALID_ENTITY_MANAGER, + ]; + + /** @var \eZ\Bundle\EzPublishCoreBundle\ApiLoader\RepositoryConfigurationProvider */ + private $repositoryConfigurationProvider; + + /** @var \Doctrine\ORM\EntityManagerInterface */ + private $entityManager; + + /** @var \Symfony\Component\DependencyInjection\ServiceLocator */ + private $serviceLocator; + + public function setUp(): void + { + $this->repositoryConfigurationProvider = $this->getRepositoryConfigurationProvider(); + $this->entityManager = $this->getEntityManager(); + $this->serviceLocator = $this->getServiceLocator(); + } + + public function testGetEntityManager(): void + { + $serviceLocator = $this->getServiceLocator(); + $serviceLocator + ->method('has') + ->with(self::DEFAULT_ENTITY_MANAGER) + ->willReturn(true); + $serviceLocator + ->method('get') + ->with(self::DEFAULT_ENTITY_MANAGER) + ->willReturn($this->getEntityManager()); + + $this->repositoryConfigurationProvider + ->method('getRepositoryConfig') + ->willReturn([ + 'alias' => 'my_repository', + 'storage' => [ + 'connection' => 'default', + ], + ]); + + $entityManagerFactory = new EntityManagerFactory( + $this->repositoryConfigurationProvider, + $serviceLocator, + self::DEFAULT_CONNECTION, + self::ENTITY_MANAGERS + ); + + self::assertEquals($this->getEntityManager(), $entityManagerFactory->getEntityManager()); + } + + public function testGetEntityManagerWillUseDefaultConnection(): void + { + $serviceLocator = $this->getServiceLocator(); + $serviceLocator + ->method('has') + ->with(self::DEFAULT_ENTITY_MANAGER) + ->willReturn(true); + $serviceLocator + ->method('get') + ->with(self::DEFAULT_ENTITY_MANAGER) + ->willReturn($this->entityManager); + + $this->repositoryConfigurationProvider + ->method('getRepositoryConfig') + ->willReturn([ + 'storage' => [], + ]); + + $entityManagerFactory = new EntityManagerFactory( + $this->repositoryConfigurationProvider, + $serviceLocator, + self::DEFAULT_CONNECTION, + self::ENTITY_MANAGERS + ); + + self::assertEquals($this->entityManager, $entityManagerFactory->getEntityManager()); + } + + public function testGetEntityManagerInvalid(): void + { + $serviceLocator = $this->getServiceLocator(); + + $serviceLocator + ->method('has') + ->with(self::INVALID_ENTITY_MANAGER) + ->willReturn(false); + + $this->repositoryConfigurationProvider + ->method('getRepositoryConfig') + ->willReturn([ + 'alias' => 'invalid', + 'storage' => [ + 'connection' => 'invalid', + ], + ]); + + $entityManagerFactory = new EntityManagerFactory( + $this->repositoryConfigurationProvider, + $serviceLocator, + 'default', + [ + 'default' => 'doctrine.orm.default_entity_manager', + ] + ); + + $this->expectException(InvalidArgumentException::class); + + $entityManagerFactory->getEntityManager(); + } + + /** + * @return \eZ\Bundle\EzPublishCoreBundle\ApiLoader\RepositoryConfigurationProvider|\PHPUnit\Framework\MockObject\MockObject + */ + protected function getRepositoryConfigurationProvider(): RepositoryConfigurationProvider + { + return $this->createMock(RepositoryConfigurationProvider::class); + } + + /** + * @return \Symfony\Component\DependencyInjection\ServiceLocator|\PHPUnit\Framework\MockObject\MockObject + */ + protected function getServiceLocator(): ServiceLocator + { + return $this->createMock(ServiceLocator::class); + } + + /** + * @return \Doctrine\ORM\EntityManagerInterface|\PHPUnit\Framework\MockObject\MockObject + */ + protected function getEntityManager(): EntityManagerInterface + { + return $this->createMock(EntityManagerInterface::class); + } +} diff --git a/eZ/Publish/API/Repository/Tests/Limitation/PermissionResolver/LocationLimitationIntegrationTest.php b/eZ/Publish/API/Repository/Tests/Limitation/PermissionResolver/LocationLimitationIntegrationTest.php index c1118f68ca..3eb04c317e 100644 --- a/eZ/Publish/API/Repository/Tests/Limitation/PermissionResolver/LocationLimitationIntegrationTest.php +++ b/eZ/Publish/API/Repository/Tests/Limitation/PermissionResolver/LocationLimitationIntegrationTest.php @@ -8,6 +8,7 @@ namespace eZ\Publish\API\Repository\Tests\Limitation\PermissionResolver; +use eZ\Publish\API\Repository\Repository; use eZ\Publish\API\Repository\Values\User\Limitation\LocationLimitation; use eZ\Publish\SPI\Limitation\Target\Version; @@ -62,4 +63,35 @@ public function testCanUserEditContent(array $limitations, bool $expectedResult) [$location, new Version(['allLanguageCodesList' => 'eng-GB'])] ); } + + /** + * @dataProvider providerForCanUserEditOrPublishContent + * + * @throws \eZ\Publish\API\Repository\Exceptions\ForbiddenException + * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException + * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException + */ + public function testCanUserReadTrashedContent(array $limitations, bool $expectedResult): void + { + $repository = $this->getRepository(); + $locationService = $repository->getLocationService(); + + $location = $locationService->loadLocation(2); + + $this->loginAsEditorUserWithLimitations('content', 'read', $limitations); + + $trashItem = $repository->sudo( + function (Repository $repository) use ($location) { + return $repository->getTrashService()->trash($location); + } + ); + + $this->assertCanUser( + $expectedResult, + 'content', + 'read', + $limitations, + $trashItem->contentInfo + ); + } } diff --git a/eZ/Publish/Core/Limitation/LocationLimitationType.php b/eZ/Publish/Core/Limitation/LocationLimitationType.php index cf7881de08..ef26bb82eb 100644 --- a/eZ/Publish/Core/Limitation/LocationLimitationType.php +++ b/eZ/Publish/Core/Limitation/LocationLimitationType.php @@ -133,6 +133,8 @@ public function evaluate(APILimitationValue $value, APIUserReference $currentUse if ($targets === null) { if ($object->published) { $targets = $this->persistence->locationHandler()->loadLocationsByContent($object->id); + } elseif ($object->isTrashed()) { + $targets = $this->persistence->locationHandler()->loadLocationsByTrashContent($object->id); } else { // @todo Need support for draft locations to to work correctly $targets = $this->persistence->locationHandler()->loadParentLocationsForDraftContent($object->id); diff --git a/eZ/Publish/Core/Search/Legacy/Content/IndexerGateway.php b/eZ/Publish/Core/Search/Legacy/Content/IndexerGateway.php new file mode 100644 index 0000000000..5e44cf975b --- /dev/null +++ b/eZ/Publish/Core/Search/Legacy/Content/IndexerGateway.php @@ -0,0 +1,147 @@ +connection = $connection; + } + + public function getContentSince(DateTimeInterface $since, int $iterationCount): Generator + { + $query = $this->buildQueryForContentSince($since); + $query->orderBy('c.modified'); + + yield from $this->fetchIteration($query->execute(), $iterationCount); + } + + public function countContentSince(DateTimeInterface $since): int + { + $query = $this->buildCountingQuery( + $this->buildQueryForContentSince($since) + ); + + return (int)$query->execute()->fetchOne(); + } + + public function getContentInSubtree(string $locationPath, int $iterationCount): Generator + { + $query = $this->buildQueryForContentInSubtree($locationPath); + + yield from $this->fetchIteration($query->execute(), $iterationCount); + } + + public function countContentInSubtree(string $locationPath): int + { + $query = $this->buildCountingQuery( + $this->buildQueryForContentInSubtree($locationPath) + ); + + return (int)$query->execute()->fetchOne(); + } + + public function getAllContent(int $iterationCount): Generator + { + $query = $this->buildQueryForAllContent(); + + yield from $this->fetchIteration($query->execute(), $iterationCount); + } + + public function countAllContent(): int + { + $query = $this->buildCountingQuery( + $this->buildQueryForAllContent() + ); + + return (int)$query->execute()->fetchOne(); + } + + private function buildQueryForContentSince(DateTimeInterface $since): QueryBuilder + { + return $this->connection->createQueryBuilder() + ->select('c.id') + ->from('ezcontentobject', 'c') + ->where('c.status = :status')->andWhere('c.modified >= :since') + ->setParameter('status', ContentInfo::STATUS_PUBLISHED, ParameterType::INTEGER) + ->setParameter('since', $since->getTimestamp(), ParameterType::INTEGER); + } + + private function buildQueryForContentInSubtree(string $locationPath): QueryBuilder + { + return $this->connection->createQueryBuilder() + ->select('DISTINCT c.id') + ->from('ezcontentobject', 'c') + ->innerJoin('c', 'ezcontentobject_tree', 't', 't.contentobject_id = c.id') + ->where('c.status = :status') + ->andWhere('t.path_string LIKE :path') + ->setParameter('status', ContentInfo::STATUS_PUBLISHED, ParameterType::INTEGER) + ->setParameter('path', $locationPath . '%', ParameterType::STRING); + } + + private function buildQueryForAllContent(): QueryBuilder + { + return $this->connection->createQueryBuilder() + ->select('c.id') + ->from('ezcontentobject', 'c') + ->where('c.status = :status') + ->setParameter('status', ContentInfo::STATUS_PUBLISHED, ParameterType::INTEGER); + } + + /** + * @throws \Doctrine\DBAL\Exception + */ + private function buildCountingQuery(QueryBuilder $query): QueryBuilder + { + $databasePlatform = $this->connection->getDatabasePlatform(); + + // wrap existing select part in count expression + $query->select( + $databasePlatform->getCountExpression( + $query->getQueryPart('select')[0] + ) + ); + + return $query; + } + + private function fetchIteration(ResultStatement $statement, int $iterationCount): Generator + { + do { + $contentIds = []; + for ($i = 0; $i < $iterationCount; ++$i) { + if ($contentId = $statement->fetchOne()) { + $contentIds[] = $contentId; + } elseif (empty($contentIds)) { + return; + } else { + break; + } + } + + yield $contentIds; + } while (!empty($contentId)); + } +} diff --git a/eZ/Publish/Core/settings/search_engines/legacy.yml b/eZ/Publish/Core/settings/search_engines/legacy.yml index d985b7b0e9..18f74ab62a 100644 --- a/eZ/Publish/Core/settings/search_engines/legacy.yml +++ b/eZ/Publish/Core/settings/search_engines/legacy.yml @@ -81,3 +81,7 @@ services: tags: - {name: ezplatform.search_engine.indexer, alias: legacy} lazy: true + + eZ\Publish\Core\Search\Legacy\Content\IndexerGateway: + arguments: + $connection: '@ezpublish.persistence.connection' diff --git a/eZ/Publish/SPI/Search/Content/IndexerGateway.php b/eZ/Publish/SPI/Search/Content/IndexerGateway.php new file mode 100644 index 0000000000..8f14ee3259 --- /dev/null +++ b/eZ/Publish/SPI/Search/Content/IndexerGateway.php @@ -0,0 +1,54 @@ +repository = (new Legacy())->getRepository(true); + } +} diff --git a/eZ/Publish/SPI/Tests/Search/Content/IndexerGatewayTest.php b/eZ/Publish/SPI/Tests/Search/Content/IndexerGatewayTest.php new file mode 100644 index 0000000000..b7a48e40cf --- /dev/null +++ b/eZ/Publish/SPI/Tests/Search/Content/IndexerGatewayTest.php @@ -0,0 +1,135 @@ +gateway = new IndexerGateway($this->getRawDatabaseConnection()); + } + + public function getDataForContentSince(): iterable + { + yield '1999-01-01' => [ + new DateTimeImmutable('1999-01-01'), + 9, + 2, + ]; + + yield 'now' => [ + new DateTimeImmutable('now'), + 0, + 2, + ]; + } + + public function getDataForContentInSubtree(): iterable + { + yield '/1/5/' => [ + '/1/5/', + 8, + 1, + ]; + + yield '/999/888/' => [ + '/999/888/', + 0, + 1, + ]; + } + + /** + * @dataProvider getDataForContentSince + * + * @throws \Doctrine\DBAL\Exception + */ + public function testGetContentSince( + DateTimeImmutable $since, + int $expectedCount, + int $iterationCount + ): void { + self::assertCount($expectedCount, iterator_to_array($this->gateway->getContentSince($since, $iterationCount))); + } + + /** + * @dataProvider getDataForContentSince + * + * @throws \Doctrine\DBAL\Exception + */ + public function testCountContentSince( + DateTimeImmutable $since, + int $expectedCount, + int $iterationCount + ): void { + self::assertSame( + $expectedCount * $iterationCount, + $this->gateway->countContentSince($since) + ); + } + + /** + * @dataProvider getDataForContentInSubtree + * + * @throws \Doctrine\DBAL\Exception + */ + public function testGetContentInSubtree( + string $subtreePath, + int $expectedCount, + int $iterationCount + ): void { + self::assertCount( + $expectedCount, + iterator_to_array($this->gateway->getContentInSubtree($subtreePath, $iterationCount)) + ); + } + + /** + * @dataProvider getDataForContentInSubtree + * + * @throws \Doctrine\DBAL\Exception + */ + public function testCountContentInSubtree( + string $subtreePath, + int $expectedCount, + int $iterationCount + ): void { + self::assertSame( + $expectedCount * $iterationCount, + $this->gateway->countContentInSubtree($subtreePath) + ); + } + + public function testCountAllContent(): void + { + self::assertCount(9, iterator_to_array($this->gateway->getAllContent(2))); + } + + public function testGetAllContent(): void + { + self::assertSame(18, $this->gateway->countAllContent()); + } +} diff --git a/phpunit-integration-legacy.xml b/phpunit-integration-legacy.xml index 3a454027b8..024789db46 100644 --- a/phpunit-integration-legacy.xml +++ b/phpunit-integration-legacy.xml @@ -73,6 +73,9 @@ eZ/Publish/Core/FieldType/Tests/Integration + + eZ/Publish/SPI/Tests/Search/Content/IndexerGatewayTest.php +