From 97009168d4886e57cf17b5ebaf0d4e32dac33a8d Mon Sep 17 00:00:00 2001 From: Maciej Kobus Date: Fri, 26 Mar 2021 13:47:26 +0100 Subject: [PATCH 1/7] EZEE-3491: Implemented siteaccess aware Entity Manager (#3092) * EZEE-3491: Implemented siteaccess aware Entity Manager * EZEE-3491: Implemented code review suggestions * EZEE-3491: Made registering ORM config only when it's configured * EZEE-3491: Added unit tests * EZEE-3491: Removed parsing Doctrine configuration to avoid issues with unresolved parameters * fixup! EZEE-3491: Removed parsing Doctrine configuration to avoid issues with unresolved parameters * EZEE-3491: Fixed invalid class reference in EntityManagerFactoryTest * EZEE-3491: Added entity map injection to EM Configuration service * EZEE-3491: Refactored EntityManagerFactory to use ServiceLocator * EZEE-3491: Added default value for `ibexa.orm.entity_mappings` * EZEE-3491: Fixed EntityManagerFactoryTest * EZEE-3491: Fixed typo in EntityManagerFactoryServiceLocatorPass name * EZEE-3491: Fixed code style in default_settings.yml * EZEE-3491: Fixed typos --- composer.json | 1 + ...EntityManagerFactoryServiceLocatorPass.php | 42 +++++ .../InjectEntityManagerMappingsPass.php | 156 +++++++++++++++++ .../Compiler/LazyDoctrineRepositoriesPass.php | 52 ++++++ .../DependencyInjection/Configuration.php | 52 ++++++ .../EzPublishCoreExtension.php | 69 ++++++++ .../Entity/EntityManagerFactory.php | 71 ++++++++ .../EzPublishCoreBundle.php | 6 + .../Resources/config/default_settings.yml | 4 + .../Resources/config/services.yml | 12 ++ ...tyManagerFactoryServiceLocatorPassTest.php | 67 ++++++++ .../InjectEntityManagerMappingPassTest.php | 102 +++++++++++ .../LazyDoctrineRepositoriesPassTest.php | 50 ++++++ .../AnnotationEntityBundle.php | 15 ++ .../AnnotationEntityBundle/Entity/.gitkeep | 0 .../Stub/XmlEntityBundle/XmlEntityBundle.php | 15 ++ .../Stub/XmlEntityBundle/config/.gitkeep | 0 .../Tests/Entity/EntityManagerFactoryTest.php | 158 ++++++++++++++++++ 18 files changed, 872 insertions(+) create mode 100644 eZ/Bundle/EzPublishCoreBundle/DependencyInjection/Compiler/EntityManagerFactoryServiceLocatorPass.php create mode 100644 eZ/Bundle/EzPublishCoreBundle/DependencyInjection/Compiler/InjectEntityManagerMappingsPass.php create mode 100644 eZ/Bundle/EzPublishCoreBundle/DependencyInjection/Compiler/LazyDoctrineRepositoriesPass.php create mode 100644 eZ/Bundle/EzPublishCoreBundle/Entity/EntityManagerFactory.php create mode 100644 eZ/Bundle/EzPublishCoreBundle/Tests/DependencyInjection/Compiler/EntityManagerFactoryServiceLocatorPassTest.php create mode 100644 eZ/Bundle/EzPublishCoreBundle/Tests/DependencyInjection/Compiler/InjectEntityManagerMappingPassTest.php create mode 100644 eZ/Bundle/EzPublishCoreBundle/Tests/DependencyInjection/Compiler/LazyDoctrineRepositoriesPassTest.php create mode 100644 eZ/Bundle/EzPublishCoreBundle/Tests/DependencyInjection/Stub/AnnotationEntityBundle/AnnotationEntityBundle.php create mode 100644 eZ/Bundle/EzPublishCoreBundle/Tests/DependencyInjection/Stub/AnnotationEntityBundle/Entity/.gitkeep create mode 100644 eZ/Bundle/EzPublishCoreBundle/Tests/DependencyInjection/Stub/XmlEntityBundle/XmlEntityBundle.php create mode 100644 eZ/Bundle/EzPublishCoreBundle/Tests/DependencyInjection/Stub/XmlEntityBundle/config/.gitkeep create mode 100644 eZ/Bundle/EzPublishCoreBundle/Tests/Entity/EntityManagerFactoryTest.php diff --git a/composer.json b/composer.json index d38d17a3cb..42e76f1aaa 100644 --- a/composer.json +++ b/composer.json @@ -27,6 +27,7 @@ "hautelook/templated-uri-bundle": "^2.1", "pagerfanta/pagerfanta": "^2.0", "ocramius/proxy-manager": "^2.1", + "doctrine/orm": "^2.7", "doctrine/doctrine-bundle": "~1.6", "liip/imagine-bundle": "^2.1", "oneup/flysystem-bundle": "^3.0", diff --git a/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/Compiler/EntityManagerFactoryServiceLocatorPass.php b/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/Compiler/EntityManagerFactoryServiceLocatorPass.php new file mode 100644 index 0000000000..5b0d74924d --- /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..e0de2b6600 --- /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..d322e5867f --- /dev/null +++ b/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/Compiler/LazyDoctrineRepositoriesPass.php @@ -0,0 +1,52 @@ +getDefinitions() as $serviceId => $definition) { + if (!is_array($definition->getFactory())) { + continue; + } + + $factory = $definition->getFactory(); + $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 d276ca3809..81137389d2 100644 --- a/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/Configuration.php +++ b/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/Configuration.php @@ -57,6 +57,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)); @@ -703,4 +704,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 ea2678cc66..36a97f372c 100644 --- a/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/EzPublishCoreExtension.php +++ b/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/EzPublishCoreExtension.php @@ -30,6 +30,11 @@ class EzPublishCoreExtension extends Extension implements PrependExtensionInterf const RICHTEXT_CUSTOM_STYLES_PARAMETER = 'ezplatform.ezrichtext.custom_styles'; const RICHTEXT_CUSTOM_TAGS_PARAMETER = 'ezplatform.ezrichtext.custom_tags'; + private const ENTITY_MANAGER_TEMPLATE = [ + 'connection' => null, + 'mappings' => [], + ]; + /** @var \eZ\Bundle\EzPublishCoreBundle\DependencyInjection\Configuration\Suggestion\Collector\SuggestionCollector */ private $suggestionCollector; @@ -107,6 +112,7 @@ public function load(array $configs, ContainerBuilder $container) $this->registerRichTextConfiguration($config, $container); $this->registerUrlAliasConfiguration($config, $container); $this->registerUrlWildcardsConfiguration($config, $container); + $this->registerOrmConfiguration($config, $container); // Routing $this->handleRouting($config, $container, $loader); @@ -159,6 +165,7 @@ public function getConfiguration(array $config, ContainerBuilder $container) public function prepend(ContainerBuilder $container) { $this->prependTranslatorConfiguration($container); + $this->prependDoctrineConfiguration($container); } /** @@ -324,6 +331,16 @@ private function registerRichTextConfiguration(array $config, ContainerBuilder $ } } + 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. * @@ -655,6 +672,58 @@ private function prependTranslatorConfiguration(ContainerBuilder $container) } } + private function prependDoctrineConfiguration(ContainerBuilder $container): void + { + if (!$container->hasExtension('doctrine')) { + return; + } + + $kernelConfigs = $container->getExtensionConfig('ezpublish'); + $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..cc4d50dff1 --- /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 85f3c729bb..d3ff42b1cc 100644 --- a/eZ/Bundle/EzPublishCoreBundle/EzPublishCoreBundle.php +++ b/eZ/Bundle/EzPublishCoreBundle/EzPublishCoreBundle.php @@ -11,8 +11,11 @@ use eZ\Bundle\EzPublishCoreBundle\DependencyInjection\Compiler\ComplexSettingsPass; use eZ\Bundle\EzPublishCoreBundle\DependencyInjection\Compiler\ConfigResolverParameterPass; use eZ\Bundle\EzPublishCoreBundle\DependencyInjection\Compiler\ConsoleCacheWarmupPass; +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\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; @@ -84,6 +87,9 @@ public function build(ContainerBuilder $container) $container->addCompilerPass(new PlaceholderProviderPass()); $container->addCompilerPass(new NotificationRendererPass()); $container->addCompilerPass(new ConsoleCacheWarmupPass()); + $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/default_settings.yml b/eZ/Bundle/EzPublishCoreBundle/Resources/config/default_settings.yml index f154d88496..f84cf92679 100644 --- a/eZ/Bundle/EzPublishCoreBundle/Resources/config/default_settings.yml +++ b/eZ/Bundle/EzPublishCoreBundle/Resources/config/default_settings.yml @@ -320,3 +320,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/services.yml b/eZ/Bundle/EzPublishCoreBundle/Resources/config/services.yml index 560ce66264..f7aae98a01 100644 --- a/eZ/Bundle/EzPublishCoreBundle/Resources/config/services.yml +++ b/eZ/Bundle/EzPublishCoreBundle/Resources/config/services.yml @@ -281,3 +281,15 @@ services: $imagine: '@liip_imagine' tags: - { name: console.command, command: ezplatform:images:resize-original } + + 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..671041f873 --- /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..07f5ea23a2 --- /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..510d34b461 --- /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..a94a62f600 --- /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() + { + $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); + } +} From a18ac4c547cce7b19ed948b3c01e02bab9995f2b Mon Sep 17 00:00:00 2001 From: Bartek Date: Mon, 29 Mar 2021 15:32:13 +0200 Subject: [PATCH 2/7] EZP-32401: Fixed handling trashed Content by LocationLimitation (#3093) --- .../LocationLimitationIntegrationTest.php | 32 +++++++++++++++++++ .../Limitation/LocationLimitationType.php | 2 ++ 2 files changed, 34 insertions(+) diff --git a/eZ/Publish/API/Repository/Tests/Limitation/PermissionResolver/LocationLimitationIntegrationTest.php b/eZ/Publish/API/Repository/Tests/Limitation/PermissionResolver/LocationLimitationIntegrationTest.php index 2ce01c4399..013f36565c 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 520bfdff50..661977cf2d 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); From 90bc47c9ca7fbc48aac69bfdc0eb2a12918eeb16 Mon Sep 17 00:00:00 2001 From: Maciej Kobus Date: Mon, 29 Mar 2021 12:19:37 +0200 Subject: [PATCH 3/7] EZP-32284: Fixed method signature --- .../Tests/Entity/EntityManagerFactoryTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eZ/Bundle/EzPublishCoreBundle/Tests/Entity/EntityManagerFactoryTest.php b/eZ/Bundle/EzPublishCoreBundle/Tests/Entity/EntityManagerFactoryTest.php index 37b6b9533a..6596bdea98 100644 --- a/eZ/Bundle/EzPublishCoreBundle/Tests/Entity/EntityManagerFactoryTest.php +++ b/eZ/Bundle/EzPublishCoreBundle/Tests/Entity/EntityManagerFactoryTest.php @@ -34,7 +34,7 @@ class EntityManagerFactoryTest extends TestCase /** @var \Symfony\Component\DependencyInjection\ServiceLocator */ private $serviceLocator; - public function setUp() + public function setUp(): void { $this->repositoryConfigurationProvider = $this->getRepositoryConfigurationProvider(); $this->entityManager = $this->getEntityManager(); From d93bfed7eaf43892243ad6f9904a9983923631c4 Mon Sep 17 00:00:00 2001 From: Maciej Kobus Date: Mon, 29 Mar 2021 12:21:42 +0200 Subject: [PATCH 4/7] EZP-32284: Fixed code style --- .../Compiler/EntityManagerFactoryServiceLocatorPass.php | 2 +- .../Compiler/InjectEntityManagerMappingsPass.php | 2 +- .../Compiler/LazyDoctrineRepositoriesPass.php | 2 +- eZ/Bundle/EzPublishCoreBundle/Entity/EntityManagerFactory.php | 2 +- .../Compiler/EntityManagerFactoryServiceLocatorPassTest.php | 2 +- .../Compiler/InjectEntityManagerMappingPassTest.php | 2 +- .../Compiler/LazyDoctrineRepositoriesPassTest.php | 2 +- .../Stub/AnnotationEntityBundle/AnnotationEntityBundle.php | 2 +- .../Stub/XmlEntityBundle/XmlEntityBundle.php | 2 +- .../Tests/Entity/EntityManagerFactoryTest.php | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/Compiler/EntityManagerFactoryServiceLocatorPass.php b/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/Compiler/EntityManagerFactoryServiceLocatorPass.php index 5b0d74924d..6a47d162f4 100644 --- a/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/Compiler/EntityManagerFactoryServiceLocatorPass.php +++ b/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/Compiler/EntityManagerFactoryServiceLocatorPass.php @@ -1,7 +1,7 @@ Date: Mon, 29 Mar 2021 15:30:04 +0200 Subject: [PATCH 5/7] EZP-32284: Added fetching repository config from `ezplatform` extension --- .../DependencyInjection/EzPublishCoreExtension.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/EzPublishCoreExtension.php b/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/EzPublishCoreExtension.php index 3ba9f3fc96..8de738e3ad 100644 --- a/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/EzPublishCoreExtension.php +++ b/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/EzPublishCoreExtension.php @@ -571,7 +571,10 @@ private function prependDoctrineConfiguration(ContainerBuilder $container): void return; } - $kernelConfigs = $container->getExtensionConfig('ezpublish'); + $kernelConfigs = array_merge( + $container->getExtensionConfig('ezpublish'), + $container->getExtensionConfig('ezplatform') + ); $entityMappings = []; $repositoryConnections = []; From 6f9afa918d58212d43f0c43a1d86f676598e95bf Mon Sep 17 00:00:00 2001 From: Maciej Kobus Date: Thu, 8 Apr 2021 15:05:18 +0200 Subject: [PATCH 6/7] EZP-32284: Added additional check for extracting Factory service --- .../Compiler/LazyDoctrineRepositoriesPass.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/Compiler/LazyDoctrineRepositoriesPass.php b/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/Compiler/LazyDoctrineRepositoriesPass.php index 2a3e3316ac..7c6adbe81b 100644 --- a/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/Compiler/LazyDoctrineRepositoriesPass.php +++ b/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/Compiler/LazyDoctrineRepositoriesPass.php @@ -11,6 +11,7 @@ use RuntimeException; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; final class LazyDoctrineRepositoriesPass implements CompilerPassInterface { @@ -23,6 +24,10 @@ public function process(ContainerBuilder $container): void } $factory = $definition->getFactory(); + if (!is_string($factory[0]) && !$factory[0] instanceof Reference) { + continue; + } + $factoryServiceId = (string) $factory[0]; if ($factoryServiceId !== 'ibexa.doctrine.orm.entity_manager') { From 9f1c87e9ecc1d56e9042914f1c0476b001e37bd3 Mon Sep 17 00:00:00 2001 From: Andrew Longosz Date: Fri, 9 Apr 2021 13:41:21 +0200 Subject: [PATCH 7/7] IBX-143: Refactored reindexing command to be DBAL 2.13-compatible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For more details see: * ezsystems/ezpublish-kernel#3094 * https://issues.ibexa.co/browse/IBX-143 Changes: * Bumped doctrine/dbal to ^2.13.0 * Extracted Indexer Doctrine Gateway from ReindexCommand * [Tests] Added integration test coverage for IndexerGateway Co-Authored-By: Adam Wójs Co-Authored-By: Łukasz Serwatka Co-Authored-By: Tomasz Kryszan --- composer.json | 1 + .../Command/ReindexCommand.php | 135 ++++------------ .../Resources/config/commands.yml | 6 + .../Resources/config/papi.yml | 3 + eZ/Publish/API/Repository/Tests/BaseTest.php | 2 +- .../Search/Legacy/Content/IndexerGateway.php | 147 ++++++++++++++++++ .../Core/settings/search_engines/legacy.yml | 4 + .../SPI/Search/Content/IndexerGateway.php | 54 +++++++ eZ/Publish/SPI/Tests/BaseGatewayTest.php | 23 +++ .../Search/Content/IndexerGatewayTest.php | 135 ++++++++++++++++ phpunit-integration-legacy.xml | 3 + 11 files changed, 410 insertions(+), 103 deletions(-) create mode 100644 eZ/Publish/Core/Search/Legacy/Content/IndexerGateway.php create mode 100644 eZ/Publish/SPI/Search/Content/IndexerGateway.php create mode 100644 eZ/Publish/SPI/Tests/BaseGatewayTest.php create mode 100644 eZ/Publish/SPI/Tests/Search/Content/IndexerGatewayTest.php diff --git a/composer.json b/composer.json index 42e76f1aaa..6e5d3c5b4b 100644 --- a/composer.json +++ b/composer.json @@ -27,6 +27,7 @@ "hautelook/templated-uri-bundle": "^2.1", "pagerfanta/pagerfanta": "^2.0", "ocramius/proxy-manager": "^2.1", + "doctrine/dbal": "^2.13.0", "doctrine/orm": "^2.7", "doctrine/doctrine-bundle": "~1.6", "liip/imagine-bundle": "^2.1", diff --git a/eZ/Bundle/EzPublishCoreBundle/Command/ReindexCommand.php b/eZ/Bundle/EzPublishCoreBundle/Command/ReindexCommand.php index 796466d3c1..edc65999ed 100644 --- a/eZ/Bundle/EzPublishCoreBundle/Command/ReindexCommand.php +++ b/eZ/Bundle/EzPublishCoreBundle/Command/ReindexCommand.php @@ -7,10 +7,11 @@ namespace eZ\Bundle\EzPublishCoreBundle\Command; 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\Persistence\Content\Location\Handler as LocationHandler; +use eZ\Publish\SPI\Search\Content\IndexerGateway; +use Generator; use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Input\InputInterface; @@ -21,16 +22,12 @@ use Symfony\Component\Process\ProcessBuilder; use RuntimeException; use DateTime; -use PDO; class ReindexCommand extends ContainerAwareCommand { /** @var \eZ\Publish\Core\Search\Common\Indexer|\eZ\Publish\Core\Search\Common\IncrementalIndexer */ private $searchIndexer; - /** @var \Doctrine\DBAL\Connection */ - private $connection; - /** @var string */ private $phpPath; @@ -49,6 +46,20 @@ class ReindexCommand extends ContainerAwareCommand /** @var string */ private $projectDir; + /** @var \eZ\Publish\SPI\Search\Content\IndexerGateway */ + private $gateway; + + /** @var \eZ\Publish\SPI\Persistence\Content\Location\Handler */ + private $locationHandler; + + public function __construct(IndexerGateway $gateway, LocationHandler $locationHandler) + { + $this->gateway = $gateway; + $this->locationHandler = $locationHandler; + + parent::__construct(); + } + /** * Initialize objects required by {@see execute()}. * @@ -59,7 +70,6 @@ public function initialize(InputInterface $input, OutputInterface $output) { parent::initialize($input, $output); $this->searchIndexer = $this->getContainer()->get('ezpublish.spi.search.indexer'); - $this->connection = $this->getContainer()->get('ezpublish.api.storage_engine.legacy.connection'); $this->logger = $this->getContainer()->get('logger'); $this->env = $this->getContainer()->getParameter('kernel.environment'); $this->isDebug = $this->getContainer()->getParameter('kernel.debug'); @@ -200,16 +210,18 @@ protected function indexIncrementally(InputInterface $input, OutputInterface $ou } 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'); } @@ -242,10 +254,15 @@ protected function indexIncrementally(InputInterface $input, OutputInterface $ou $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); } @@ -266,14 +283,12 @@ protected function indexIncrementally(InputInterface $input, OutputInterface $ou */ 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) { @@ -312,90 +327,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) - { - /** @var \eZ\Publish\SPI\Persistence\Content\Location\Handler */ - $locationHandler = $this->getContainer()->get('ezpublish.spi.persistence.location_handler'); - $location = $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/Resources/config/commands.yml b/eZ/Bundle/EzPublishCoreBundle/Resources/config/commands.yml index 4c97723df5..ac6fdfef1d 100644 --- a/eZ/Bundle/EzPublishCoreBundle/Resources/config/commands.yml +++ b/eZ/Bundle/EzPublishCoreBundle/Resources/config/commands.yml @@ -15,3 +15,9 @@ services: - "@ezpublish.siteaccess" tags: - { name: console.command } + + eZ\Bundle\EzPublishCoreBundle\Command\ReindexCommand: + autowire: true + autoconfigure: true + arguments: + $locationHandler: '@ezpublish.spi.persistence.location_handler' diff --git a/eZ/Bundle/EzPublishCoreBundle/Resources/config/papi.yml b/eZ/Bundle/EzPublishCoreBundle/Resources/config/papi.yml index 8d6164af99..4c3dfbb7f7 100644 --- a/eZ/Bundle/EzPublishCoreBundle/Resources/config/papi.yml +++ b/eZ/Bundle/EzPublishCoreBundle/Resources/config/papi.yml @@ -98,3 +98,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/Publish/API/Repository/Tests/BaseTest.php b/eZ/Publish/API/Repository/Tests/BaseTest.php index dd1d2f0168..1c3da3598a 100644 --- a/eZ/Publish/API/Repository/Tests/BaseTest.php +++ b/eZ/Publish/API/Repository/Tests/BaseTest.php @@ -622,7 +622,7 @@ public function createUserWithPolicies($login, array $policiesData, RoleLimitati * * @throws \ErrorException */ - protected function getRawDatabaseConnection() + protected function getRawDatabaseConnection(): Connection { $connection = $this ->getSetupFactory() 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..23d858279c --- /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 ff13a80487..a0450c4741 100644 --- a/eZ/Publish/Core/settings/search_engines/legacy.yml +++ b/eZ/Publish/Core/settings/search_engines/legacy.yml @@ -104,3 +104,7 @@ services: tags: - {name: ezpublish.searchEngineIndexer, 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..72f94ab03b --- /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..09a0f416b3 --- /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 50b07b9858..37d624bea9 100644 --- a/phpunit-integration-legacy.xml +++ b/phpunit-integration-legacy.xml @@ -69,6 +69,9 @@ eZ/Publish/Core/FieldType/Tests/Integration + + eZ/Publish/SPI/Tests/Search/Content/IndexerGatewayTest.php +