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
+