diff --git a/src/bundle/Core/ApiLoader/RepositoryFactory.php b/src/bundle/Core/ApiLoader/RepositoryFactory.php index 45df9b0283..2d2ac159fd 100644 --- a/src/bundle/Core/ApiLoader/RepositoryFactory.php +++ b/src/bundle/Core/ApiLoader/RepositoryFactory.php @@ -10,6 +10,7 @@ use Ibexa\Contracts\Core\Persistence\Filter\Location\Handler as LocationFilteringHandler; use Ibexa\Contracts\Core\Persistence\Handler as PersistenceHandler; use Ibexa\Contracts\Core\Repository\LanguageResolver; +use Ibexa\Contracts\Core\Repository\NameSchema\NameSchemaServiceInterface; use Ibexa\Contracts\Core\Repository\PasswordHashService; use Ibexa\Contracts\Core\Repository\PermissionService; use Ibexa\Contracts\Core\Repository\Repository; @@ -63,7 +64,7 @@ public function __construct( $this->repositoryClass = $repositoryClass; $this->policyMap = $policyMap; $this->languageResolver = $languageResolver; - $this->logger = null !== $logger ? $logger : new NullLogger(); + $this->logger = $logger ?? new NullLogger(); } /** @@ -91,9 +92,10 @@ public function buildRepository( ContentFilteringHandler $contentFilteringHandler, LocationFilteringHandler $locationFilteringHandler, PasswordValidatorInterface $passwordValidator, - ConfigResolverInterface $configResolver + ConfigResolverInterface $configResolver, + NameSchemaServiceInterface $nameSchemaService ): Repository { - $config = $this->container->get(\Ibexa\Bundle\Core\ApiLoader\RepositoryConfigurationProvider::class)->getRepositoryConfig(); + $config = $this->container->get(RepositoryConfigurationProvider::class)->getRepositoryConfig(); return new $this->repositoryClass( $persistenceHandler, @@ -116,6 +118,7 @@ public function buildRepository( $locationFilteringHandler, $passwordValidator, $configResolver, + $nameSchemaService, [ 'role' => [ 'policyMap' => $this->policyMap, diff --git a/src/bundle/Core/Resources/config/events.yml b/src/bundle/Core/Resources/config/events.yml deleted file mode 100644 index 185f72926e..0000000000 --- a/src/bundle/Core/Resources/config/events.yml +++ /dev/null @@ -1,7 +0,0 @@ -services: - _defaults: - autowire: true - autoconfigure: true - public: false - - Ibexa\Core\Repository\EventSubscriber\DeleteUserSubscriber: ~ diff --git a/src/bundle/Core/Resources/config/papi.yml b/src/bundle/Core/Resources/config/papi.yml index 475b7471a8..2669a3f1bd 100644 --- a/src/bundle/Core/Resources/config/papi.yml +++ b/src/bundle/Core/Resources/config/papi.yml @@ -11,7 +11,6 @@ parameters: services: # API Ibexa\Bundle\Core\ApiLoader\RepositoryFactory: - class: Ibexa\Bundle\Core\ApiLoader\RepositoryFactory arguments: - '@ibexa.config.resolver' - Ibexa\Core\Repository\Repository diff --git a/src/contracts/Event/ResolveUrlAliasSchemaEvent.php b/src/contracts/Event/ResolveUrlAliasSchemaEvent.php new file mode 100644 index 0000000000..3f75eb3463 --- /dev/null +++ b/src/contracts/Event/ResolveUrlAliasSchemaEvent.php @@ -0,0 +1,53 @@ + */ + private array $schemaIdentifiers; + + private Content $content; + + /** + * @var array> + */ + private array $names = []; + + public function __construct( + array $schemaIdentifiers, + Content $content + ) { + $this->schemaIdentifiers = $schemaIdentifiers; + $this->content = $content; + } + + public function getSchemaIdentifiers(): array + { + return $this->schemaIdentifiers; + } + + public function getContent(): Content + { + return $this->content; + } + + public function getTokenValues(): array + { + return $this->names; + } + + public function setTokenValues(array $names): void + { + $this->names = $names; + } +} diff --git a/src/contracts/Repository/NameSchema/NameSchemaServiceInterface.php b/src/contracts/Repository/NameSchema/NameSchemaServiceInterface.php new file mode 100644 index 0000000000..1d27bb149e --- /dev/null +++ b/src/contracts/Repository/NameSchema/NameSchemaServiceInterface.php @@ -0,0 +1,50 @@ + key value map of names for a language code + */ + public function resolveUrlAliasSchema(Content $content, ContentType $contentType = null): array; + + /** + * @param array> $fieldMap + * @param array $languageCodes + * + * @return array + */ + public function resolveNameSchema( + Content $content, + array $fieldMap = [], + array $languageCodes = [], + ContentType $contentType = null + ): array; + + /** + * Returns the real name for a content name pattern. + * + * @param array> $fieldMap + * @param array $languageCodes + * + * @return array + * + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException + */ + public function resolve(string $nameSchema, ContentType $contentType, array $fieldMap, array $languageCodes): array; +} diff --git a/src/contracts/Repository/NameSchema/SchemaIdentifierExtractorInterface.php b/src/contracts/Repository/NameSchema/SchemaIdentifierExtractorInterface.php new file mode 100644 index 0000000000..e86980e817 --- /dev/null +++ b/src/contracts/Repository/NameSchema/SchemaIdentifierExtractorInterface.php @@ -0,0 +1,17 @@ +> + */ + public function extract(string $schemaString): array; +} diff --git a/src/contracts/Test/IbexaKernelTestTrait.php b/src/contracts/Test/IbexaKernelTestTrait.php index 9a2eafa096..6b75669022 100644 --- a/src/contracts/Test/IbexaKernelTestTrait.php +++ b/src/contracts/Test/IbexaKernelTestTrait.php @@ -19,6 +19,7 @@ use Ibexa\Contracts\Core\Repository\RoleService; use Ibexa\Contracts\Core\Repository\SearchService; use Ibexa\Contracts\Core\Repository\SectionService; +use Ibexa\Contracts\Core\Repository\URLAliasService; use Ibexa\Contracts\Core\Repository\UserService; use Ibexa\Contracts\Core\Test\Persistence\Fixture\FixtureImporter; use Ibexa\Core\Repository\Values\User\UserReference; @@ -163,6 +164,11 @@ protected static function getSectionService(): SectionService return self::getServiceByClassName(SectionService::class); } + protected static function getUrlAliasService(): URLAliasService + { + return self::getServiceByClassName(URLAliasService::class); + } + protected static function setAnonymousUser(): void { $anonymousUserId = 10; diff --git a/src/contracts/Test/IbexaTestKernel.php b/src/contracts/Test/IbexaTestKernel.php index b76f4348c1..0e547c3ff1 100644 --- a/src/contracts/Test/IbexaTestKernel.php +++ b/src/contracts/Test/IbexaTestKernel.php @@ -89,6 +89,7 @@ class IbexaTestKernel extends Kernel implements IbexaTestKernelInterface Repository\SectionService::class, Repository\UserService::class, Repository\TokenService::class, + Repository\URLAliasService::class, ]; /** @@ -150,6 +151,7 @@ public function registerContainerConfiguration(LoaderInterface $loader): void { $loader->load(static function (ContainerBuilder $container): void { $container->setParameter('ibexa.core.test.resource_dir', self::getResourcesPath()); + $container->setParameter('ibexa.kernel.root_dir', dirname(__DIR__, 3)); }); $this->loadConfiguration($loader); diff --git a/src/lib/Base/Container/ApiLoader/RepositoryFactory.php b/src/lib/Base/Container/ApiLoader/RepositoryFactory.php index c3703ee72b..91de80f53a 100644 --- a/src/lib/Base/Container/ApiLoader/RepositoryFactory.php +++ b/src/lib/Base/Container/ApiLoader/RepositoryFactory.php @@ -10,6 +10,7 @@ use Ibexa\Contracts\Core\Persistence\Filter\Location\Handler as LocationFilteringHandler; use Ibexa\Contracts\Core\Persistence\Handler as PersistenceHandler; use Ibexa\Contracts\Core\Repository\LanguageResolver; +use Ibexa\Contracts\Core\Repository\NameSchema\NameSchemaServiceInterface; use Ibexa\Contracts\Core\Repository\PasswordHashService; use Ibexa\Contracts\Core\Repository\PermissionService; use Ibexa\Contracts\Core\Repository\Repository; @@ -83,6 +84,7 @@ public function buildRepository( LocationFilteringHandler $locationFilteringHandler, PasswordValidatorInterface $passwordValidator, ConfigResolverInterface $configResolver, + NameSchemaServiceInterface $nameSchemaService, array $languages ): Repository { return new $this->repositoryClass( @@ -106,6 +108,7 @@ public function buildRepository( $locationFilteringHandler, $passwordValidator, $configResolver, + $nameSchemaService, [ 'role' => [ 'policyMap' => $this->policyMap, diff --git a/src/lib/Repository/ContentService.php b/src/lib/Repository/ContentService.php index a8ba108e3b..c3009df5d0 100644 --- a/src/lib/Repository/ContentService.php +++ b/src/lib/Repository/ContentService.php @@ -25,6 +25,7 @@ use Ibexa\Contracts\Core\Persistence\Handler; use Ibexa\Contracts\Core\Repository\ContentService as ContentServiceInterface; use Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException as APINotFoundException; +use Ibexa\Contracts\Core\Repository\NameSchema\NameSchemaServiceInterface; use Ibexa\Contracts\Core\Repository\PermissionService; use Ibexa\Contracts\Core\Repository\Repository as RepositoryInterface; use Ibexa\Contracts\Core\Repository\Validator\ContentValidator; @@ -87,8 +88,7 @@ class ContentService implements ContentServiceInterface /** @var \Ibexa\Core\Repository\Helper\RelationProcessor */ protected $relationProcessor; - /** @var \Ibexa\Core\Repository\Helper\NameSchemaService */ - protected $nameSchemaService; + protected NameSchemaServiceInterface $nameSchemaService; /** @var \Ibexa\Core\FieldType\FieldTypeRegistry */ protected $fieldTypeRegistry; @@ -110,7 +110,7 @@ public function __construct( Handler $handler, ContentDomainMapper $contentDomainMapper, Helper\RelationProcessor $relationProcessor, - Helper\NameSchemaService $nameSchemaService, + NameSchemaServiceInterface $nameSchemaService, FieldTypeRegistry $fieldTypeRegistry, PermissionService $permissionService, ContentMapper $contentMapper, diff --git a/src/lib/Repository/EventSubscriber/NameSchemaSubscriber.php b/src/lib/Repository/EventSubscriber/NameSchemaSubscriber.php new file mode 100644 index 0000000000..89bbfc8615 --- /dev/null +++ b/src/lib/Repository/EventSubscriber/NameSchemaSubscriber.php @@ -0,0 +1,75 @@ +fieldTypeRegistry = $fieldTypeRegistry; + } + + public static function getSubscribedEvents(): array + { + return [ + ResolveUrlAliasSchemaEvent::class => [ + ['onResolveUrlAliasSchema', -100], + ], + ]; + } + + /** + * Resolves the URL alias schema by setting token values for specified field identifiers and languages. + * + * @param \Ibexa\Contracts\Core\Event\ResolveUrlAliasSchemaEvent $event + */ + public function onResolveUrlAliasSchema(ResolveUrlAliasSchemaEvent $event): void + { + if (!array_key_exists('field', $event->getSchemaIdentifiers())) { + return; + } + + $content = $event->getContent(); + $identifiers = $event->getSchemaIdentifiers()['field']; + $languages = $event->getContent()->getVersionInfo()->getLanguages(); + $tokenValues = $event->getTokenValues(); + + $contentType = $content->getContentType(); + foreach ($languages as $language) { + $languageCode = $language->getLanguageCode(); + foreach ($identifiers as $identifier) { + $fieldDefinition = $contentType->getFieldDefinition($identifier); + if (null === $fieldDefinition) { + continue; + } + $persistenceFieldType = $this->fieldTypeRegistry->getFieldType($fieldDefinition->fieldTypeIdentifier); + + $fieldValue = $content->getFieldValue($identifier, $languageCode); + $fieldValue = $persistenceFieldType->getName( + $fieldValue, + $fieldDefinition, + $languageCode + ); + + $tokenValues[$languageCode][$identifier] = $fieldValue; + } + } + + $event->setTokenValues($tokenValues); + } +} diff --git a/src/lib/Repository/Helper/NameSchemaService.php b/src/lib/Repository/Helper/NameSchemaService.php index 11f5a42b73..47682ef83c 100644 --- a/src/lib/Repository/Helper/NameSchemaService.php +++ b/src/lib/Repository/Helper/NameSchemaService.php @@ -6,441 +6,27 @@ */ namespace Ibexa\Core\Repository\Helper; -use Ibexa\Contracts\Core\Persistence\Content\Type as SPIContentType; -use Ibexa\Contracts\Core\Persistence\Content\Type\Handler as ContentTypeHandler; use Ibexa\Contracts\Core\Repository\Values\Content\Content; use Ibexa\Contracts\Core\Repository\Values\ContentType\ContentType; -use Ibexa\Core\Base\Exceptions\InvalidArgumentType; -use Ibexa\Core\FieldType\FieldTypeRegistry; -use Ibexa\Core\Repository\Mapper\ContentTypeDomainMapper; +use Ibexa\Core\Repository\NameSchema\NameSchemaService as NativeNameSchemaService; /** - * NameSchemaService is internal service for resolving content name and url alias patterns. - * This code supports content name pattern groups. - * - * Syntax: - * - * <attribute_identifier> - * <attribute_identifier> <2nd-identifier> - * User text <attribute_identifier>|(<2nd-identifier><3rd-identifier>) - * - * - * Example: - * - * <nickname|(<firstname> <lastname>)> - * - * - * Tokens are looked up from left to right. If a match is found for the - * leftmost token, the 2nd token will not be used. Tokens are representations - * of fields. So a match means that that the current field has data. - * - * Tokens are the field definition identifiers which are used in the class edit-interface. - * - * @internal Meant for internal use by Repository. + * @deprecated inject \Ibexa\Contracts\Core\Repository\NameSchema\NameSchemaServiceInterface instead. + * @see \Ibexa\Contracts\Core\Repository\NameSchema\NameSchemaServiceInterface */ -class NameSchemaService +class NameSchemaService extends NativeNameSchemaService { - /** - * The string to use to signify group tokens. - * - * @var string - */ - public const META_STRING = 'EZMETAGROUP_'; - - /** @var \Ibexa\Contracts\Core\Persistence\Content\Type\Handler */ - protected $contentTypeHandler; - - /** @var \Ibexa\Core\Repository\Mapper\ContentTypeDomainMapper */ - protected $contentTypeDomainMapper; - - /** @var \Ibexa\Core\FieldType\FieldTypeRegistry */ - protected $fieldTypeRegistry; - - /** @var array */ - protected $settings; - - /** - * Constructs a object to resolve $nameSchema with $contentVersion fields values. - * - * @param \Ibexa\Contracts\Core\Persistence\Content\Type\Handler $contentTypeHandler - * @param \Ibexa\Core\Repository\Mapper\ContentTypeDomainMapper $contentTypeDomainMapper - * @param \Ibexa\Core\FieldType\FieldTypeRegistry $fieldTypeRegistry - * @param array $settings - */ - public function __construct( - ContentTypeHandler $contentTypeHandler, - ContentTypeDomainMapper $contentTypeDomainMapper, - FieldTypeRegistry $fieldTypeRegistry, - array $settings = [] - ) { - $this->contentTypeHandler = $contentTypeHandler; - $this->contentTypeDomainMapper = $contentTypeDomainMapper; - $this->fieldTypeRegistry = $fieldTypeRegistry; - // Union makes sure default settings are ignored if provided in argument - $this->settings = $settings + [ - 'limit' => 150, - 'sequence' => '...', - ]; - } - - /** - * Convenience method for resolving URL alias schema. - * - * @param \Ibexa\Contracts\Core\Repository\Values\Content\Content $content - * @param \Ibexa\Contracts\Core\Repository\Values\ContentType\ContentType|null $contentType - * - * @return array - */ - public function resolveUrlAliasSchema(Content $content, ContentType $contentType = null) + public function resolveUrlAliasSchema(Content $content, ContentType $contentType = null): array { - if ($contentType === null) { - $contentType = $this->contentTypeHandler->load($content->contentInfo->contentTypeId); - } + $contentType = $contentType ?? $content->getContentType(); return $this->resolve( - strlen($contentType->urlAliasSchema) === 0 ? $contentType->nameSchema : $contentType->urlAliasSchema, + empty($contentType->urlAliasSchema) ? $contentType->nameSchema : $contentType->urlAliasSchema, $contentType, $content->fields, $content->versionInfo->languageCodes ); } - - /** - * Convenience method for resolving name schema. - * - * @param \Ibexa\Contracts\Core\Repository\Values\Content\Content $content - * @param array $fieldMap - * @param array $languageCodes - * @param \Ibexa\Contracts\Core\Repository\Values\ContentType\ContentType|null $contentType - * - * @return array - */ - public function resolveNameSchema(Content $content, array $fieldMap = [], array $languageCodes = [], ContentType $contentType = null) - { - if ($contentType === null) { - $contentType = $this->contentTypeHandler->load($content->contentInfo->contentTypeId); - } - - $languageCodes = $languageCodes ?: $content->versionInfo->languageCodes; - - return $this->resolve( - $contentType->nameSchema, - $contentType, - $this->mergeFieldMap( - $content, - $fieldMap, - $languageCodes - ), - $languageCodes - ); - } - - /** - * Convenience method for resolving name schema. - * - * @param \Ibexa\Contracts\Core\Repository\Values\Content\Content $content - * @param array $fieldMap - * @param array $languageCodes - * - * @return array - */ - protected function mergeFieldMap(Content $content, array $fieldMap, array $languageCodes) - { - if (empty($fieldMap)) { - return $content->fields; - } - - $mergedFieldMap = []; - - foreach ($content->fields as $fieldIdentifier => $fieldLanguageMap) { - foreach ($languageCodes as $languageCode) { - $mergedFieldMap[$fieldIdentifier][$languageCode] = isset($fieldMap[$fieldIdentifier][$languageCode]) - ? $fieldMap[$fieldIdentifier][$languageCode] - : $fieldLanguageMap[$languageCode]; - } - } - - return $mergedFieldMap; - } - - /** - * Returns the real name for a content name pattern. - * - * @param string $nameSchema - * @param \Ibexa\Contracts\Core\Persistence\Content\Type|\Ibexa\Contracts\Core\Repository\Values\ContentType\ContentType $contentType - * @param array $fieldMap - * @param array $languageCodes - * - * @return string[] - */ - public function resolve($nameSchema, $contentType, array $fieldMap, array $languageCodes) - { - list($filteredNameSchema, $groupLookupTable) = $this->filterNameSchema($nameSchema); - $tokens = $this->extractTokens($filteredNameSchema); - $schemaIdentifiers = $this->getIdentifiers($nameSchema); - - $names = []; - - foreach ($languageCodes as $languageCode) { - // Fetch titles for language code - $titles = $this->getFieldTitles($schemaIdentifiers, $contentType, $fieldMap, $languageCode); - $name = $filteredNameSchema; - - // Replace tokens with real values - foreach ($tokens as $token) { - $string = $this->resolveToken($token, $titles, $groupLookupTable); - $name = str_replace($token, $string, $name); - } - - // Make sure length is not longer then $limit unless it's 0 - if ($this->settings['limit'] && mb_strlen($name) > $this->settings['limit']) { - $name = rtrim(mb_substr($name, 0, $this->settings['limit'] - strlen($this->settings['sequence']))) . $this->settings['sequence']; - } - - $names[$languageCode] = $name; - } - - return $names; - } - - /** - * Fetches the list of available Field identifiers in the token and returns - * an array of their current title value. - * - * @see \Ibexa\Core\Repository\Values\ContentType\FieldType::getName() - * - * @param string[] $schemaIdentifiers - * @param \Ibexa\Contracts\Core\Persistence\Content\Type|\Ibexa\Contracts\Core\Repository\Values\ContentType\ContentType $contentType - * @param array $fieldMap - * @param string $languageCode - * - * @throws \Ibexa\Core\Base\Exceptions\InvalidArgumentType - * - * @return string[] Key is the field identifier, value is the title value - */ - protected function getFieldTitles(array $schemaIdentifiers, $contentType, array $fieldMap, $languageCode) - { - $fieldTitles = []; - - foreach ($schemaIdentifiers as $fieldDefinitionIdentifier) { - if (isset($fieldMap[$fieldDefinitionIdentifier][$languageCode])) { - if ($contentType instanceof SPIContentType) { - $fieldDefinition = null; - foreach ($contentType->fieldDefinitions as $spiFieldDefinition) { - if ($spiFieldDefinition->identifier === $fieldDefinitionIdentifier) { - $fieldDefinition = $this->contentTypeDomainMapper->buildFieldDefinitionDomainObject( - $spiFieldDefinition, - // This is probably not main language, but as we don't expose it, it's ok for now. - $languageCode - ); - break; - } - } - - if ($fieldDefinition === null) { - $fieldTitles[$fieldDefinitionIdentifier] = ''; - continue; - } - } elseif ($contentType instanceof ContentType) { - $fieldDefinition = $contentType->getFieldDefinition($fieldDefinitionIdentifier); - } else { - throw new InvalidArgumentType('$contentType', 'API or SPI variant of a Content Type'); - } - - $fieldTypeService = $this->fieldTypeRegistry->getFieldType( - $fieldDefinition->fieldTypeIdentifier - ); - - $fieldTitles[$fieldDefinitionIdentifier] = $fieldTypeService->getName( - $fieldMap[$fieldDefinitionIdentifier][$languageCode], - $fieldDefinition, - $languageCode - ); - } - } - - return $fieldTitles; - } - - /** - * Extract all tokens from $namePattern. - * - * Example: - * - * Text <token> more text ==> <token> - * - * - * @param string $nameSchema - * - * @return array - */ - protected function extractTokens($nameSchema) - { - preg_match_all( - '|<([^>]+)>|U', - $nameSchema, - $tokenArray - ); - - return $tokenArray[0]; - } - - /** - * Looks up the value $token should be replaced with and returns this as - * a string. Meta strings denoting token groups are automatically - * inferred. - * - * @param string $token - * @param array $titles - * @param array $groupLookupTable - * - * @return string - */ - protected function resolveToken($token, $titles, $groupLookupTable) - { - $replaceString = ''; - $tokenParts = $this->tokenParts($token); - - foreach ($tokenParts as $tokenPart) { - if ($this->isTokenGroup($tokenPart)) { - $replaceString = $groupLookupTable[$tokenPart]; - $groupTokenArray = $this->extractTokens($replaceString); - - foreach ($groupTokenArray as $groupToken) { - $replaceString = str_replace( - $groupToken, - $this->resolveToken( - $groupToken, - $titles, - $groupLookupTable - ), - $replaceString - ); - } - - // We want to stop after the first matching token part / identifier is found - // if id1 has a value, id2 will not be used. - // In this case id1 or id1 is a token group. - break; - } else { - if (array_key_exists($tokenPart, $titles) && $titles[$tokenPart] !== '' && $titles[$tokenPart] !== null) { - $replaceString = $titles[$tokenPart]; - // We want to stop after the first matching token part / identifier is found - // if id1 has a value, id2 will not be used. - break; - } - } - } - - return $replaceString; - } - - /** - * Checks whether $identifier is a placeholder for a token group. - * - * @param string $identifier - * - * @return bool - */ - protected function isTokenGroup($identifier) - { - if (strpos($identifier, self::META_STRING) === false) { - return false; - } - - return true; - } - - /** - * Returns the different constituents of $token in an array. - * The normal case here is that the different identifiers within one token - * will be tokenized and returned. - * - * Example: - * - * "<title|text>" ==> array( 'title', 'text' ) - * - * - * @param string $token - * - * @return array - */ - protected function tokenParts($token) - { - return preg_split('#\\W#', $token, -1, PREG_SPLIT_NO_EMPTY); - } - - /** - * Builds a lookup / translation table for groups in the $namePattern. - * The groups are referenced with a generated meta-token in the original - * name pattern. - * - * Returns intermediate name pattern where groups are replaced with meta- - * tokens. - * - * @param string $nameSchema - * - * @return string - */ - protected function filterNameSchema($nameSchema) - { - $retNamePattern = ''; - $foundGroups = preg_match_all('/[<|\\|](\\(.+\\))[\\||>]/U', $nameSchema, $groupArray); - $groupLookupTable = []; - - if ($foundGroups) { - $i = 0; - foreach ($groupArray[1] as $group) { - // Create meta-token for group - $metaToken = self::META_STRING . $i; - - // Insert the group with its placeholder token - $retNamePattern = str_replace($group, $metaToken, $nameSchema); - - // Remove the pattern "(" ")" from the tokens - $group = str_replace(['(', ')'], '', $group); - - $groupLookupTable[$metaToken] = $group; - ++$i; - } - $nameSchema = $retNamePattern; - } - - return [$nameSchema, $groupLookupTable]; - } - - /** - * Returns all identifiers from all tokens in the name schema. - * - * @param string $schemaString - * - * @return array - */ - protected function getIdentifiers($schemaString) - { - $allTokens = '#<(.*)>#U'; - $identifiers = '#\\W#'; - - $tmpArray = []; - preg_match_all($allTokens, $schemaString, $matches); - - foreach ($matches[1] as $match) { - $tmpArray[] = preg_split($identifiers, $match, -1, PREG_SPLIT_NO_EMPTY); - } - - $retArray = []; - foreach ($tmpArray as $matchGroup) { - if (is_array($matchGroup)) { - foreach ($matchGroup as $item) { - $retArray[] = $item; - } - } else { - $retArray[] = $matchGroup; - } - } - - return $retArray; - } } class_alias(NameSchemaService::class, 'eZ\Publish\Core\Repository\Helper\NameSchemaService'); diff --git a/src/lib/Repository/LocationService.php b/src/lib/Repository/LocationService.php index acc65b184b..6c71114051 100644 --- a/src/lib/Repository/LocationService.php +++ b/src/lib/Repository/LocationService.php @@ -18,6 +18,7 @@ use Ibexa\Contracts\Core\Repository\ContentTypeService; use Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException as APINotFoundException; use Ibexa\Contracts\Core\Repository\LocationService as LocationServiceInterface; +use Ibexa\Contracts\Core\Repository\NameSchema\NameSchemaServiceInterface; use Ibexa\Contracts\Core\Repository\PermissionCriterionResolver; use Ibexa\Contracts\Core\Repository\PermissionResolver; use Ibexa\Contracts\Core\Repository\Repository as RepositoryInterface; @@ -66,8 +67,7 @@ class LocationService implements LocationServiceInterface /** @var \Ibexa\Core\Repository\Mapper\ContentDomainMapper */ protected $contentDomainMapper; - /** @var \Ibexa\Core\Repository\Helper\NameSchemaService */ - protected $nameSchemaService; + protected NameSchemaServiceInterface $nameSchemaService; /** @var \Ibexa\Contracts\Core\Repository\PermissionCriterionResolver */ protected $permissionCriterionResolver; @@ -100,7 +100,7 @@ public function __construct( RepositoryInterface $repository, Handler $handler, ContentDomainMapper $contentDomainMapper, - Helper\NameSchemaService $nameSchemaService, + NameSchemaServiceInterface $nameSchemaService, PermissionCriterionResolver $permissionCriterionResolver, PermissionResolver $permissionResolver, LocationFilteringHandler $locationFilteringHandler, diff --git a/src/lib/Repository/NameSchema/NameSchemaService.php b/src/lib/Repository/NameSchema/NameSchemaService.php new file mode 100644 index 0000000000..2e02747ffe --- /dev/null +++ b/src/lib/Repository/NameSchema/NameSchemaService.php @@ -0,0 +1,398 @@ + + * <attribute_identifier> + * <attribute_identifier> <2nd-identifier> + * User text <attribute_identifier>|(<2nd-identifier><3rd-identifier>) + * + * + * Example: + * + * <nickname|(<firstname> <lastname>)> + * + * + * Tokens are looked up from left to right. If a match is found for the + * leftmost token, the 2nd token will not be used. Tokens are representations + * of fields. So a match means that the current field has data. + * + * Tokens are the field definition identifiers which are used in the class edit-interface. + */ +class NameSchemaService implements NameSchemaServiceInterface +{ + /** + * The string to use to signify group tokens. + * + * @var string + */ + public const META_STRING = 'EZMETAGROUP_'; + + protected FieldTypeRegistry $fieldTypeRegistry; + + /** + * @param array{limit?: integer, sequence?: string} $settings + */ + protected array $settings; + + private EventDispatcherInterface $eventDispatcher; + + private SchemaIdentifierExtractorInterface $schemaIdentifierExtractor; + + /** + * @param array{limit?: integer, sequence?: string} $settings + */ + public function __construct( + FieldTypeRegistry $fieldTypeRegistry, + SchemaIdentifierExtractorInterface $schemaIdentifierExtractor, + EventDispatcherInterface $eventDispatcher, + array $settings = [] + ) { + $this->fieldTypeRegistry = $fieldTypeRegistry; + // Union makes sure default settings are ignored if provided in argument + $this->settings = $settings + [ + 'limit' => 150, + 'sequence' => '...', + ]; + $this->eventDispatcher = $eventDispatcher; + $this->schemaIdentifierExtractor = $schemaIdentifierExtractor; + } + + public function resolveUrlAliasSchema(Content $content, ContentType $contentType = null): array + { + $contentType ??= $content->getContentType(); + $schemaName = $contentType->urlAliasSchema ?: $contentType->nameSchema; + [$filteredNameSchema, $groupLookupTable] = $this->filterNameSchema($schemaName); + $schemaIdentifiers = $this->schemaIdentifierExtractor->extract($schemaName); + $tokens = $this->extractTokens($filteredNameSchema); + + /** @var \Ibexa\Contracts\Core\Event\ResolveUrlAliasSchemaEvent $event */ + $event = $this->eventDispatcher->dispatch( + new ResolveUrlAliasSchemaEvent( + $schemaIdentifiers, + $content + ) + ); + $names = []; + $tokenValues = $event->getTokenValues(); + foreach ($tokenValues as $languageCode => $tokenValue) { + $name = $filteredNameSchema; + foreach ($tokens as $token) { + $string = $this->resolveToken($token, $tokenValue, $groupLookupTable); + $name = str_replace($token, $string, $name); + } + $name = $this->validateNameLength($name); + + $names[$languageCode] = $name; + } + + return $names; + } + + public function resolveNameSchema( + Content $content, + array $fieldMap = [], + array $languageCodes = [], + ContentType $contentType = null + ): array { + $contentType ??= $content->getContentType(); + + $languageCodes = $languageCodes ?: $content->versionInfo->languageCodes; + + return $this->resolve( + $contentType->nameSchema, + $contentType, + $this->mergeFieldMap( + $content, + $fieldMap, + $languageCodes + ), + $languageCodes + ); + } + + /** + * Convenience method for resolving name schema. + * + * @param \Ibexa\Contracts\Core\Repository\Values\Content\Content $content + * @param array $fieldMap + * @param array $languageCodes + * + * @return array + */ + protected function mergeFieldMap(Content $content, array $fieldMap, array $languageCodes): array + { + if (empty($fieldMap)) { + return $content->fields; + } + + $mergedFieldMap = []; + + foreach ($content->fields as $fieldIdentifier => $fieldLanguageMap) { + foreach ($languageCodes as $languageCode) { + $mergedFieldMap[$fieldIdentifier][$languageCode] + = $fieldMap[$fieldIdentifier][$languageCode] ?? $fieldLanguageMap[$languageCode]; + } + } + + return $mergedFieldMap; + } + + public function resolve(string $nameSchema, ContentType $contentType, array $fieldMap, array $languageCodes): array + { + [$filteredNameSchema, $groupLookupTable] = $this->filterNameSchema($nameSchema); + $tokens = $this->extractTokens($filteredNameSchema); + $schemaIdentifiers = $this->getIdentifiers($nameSchema); + + $names = []; + + foreach ($languageCodes as $languageCode) { + // Fetch titles for language code + $titles = $this->getFieldTitles($schemaIdentifiers, $contentType, $fieldMap, $languageCode); + $name = $filteredNameSchema; + + // Replace tokens with real values + foreach ($tokens as $token) { + $string = $this->resolveToken($token, $titles, $groupLookupTable); + $name = str_replace($token, $string, $name); + } + $name = $this->validateNameLength($name); + + $names[$languageCode] = $name; + } + + return $names; + } + + /** + * Fetches the list of available Field identifiers in the token and returns + * an array of their current title value. + * + * @param array $schemaIdentifiers + * @param array> $fieldMap + * + * @return string[] Key is the field identifier, value is the title value + * + * @see \Ibexa\Core\Repository\Values\ContentType\FieldType::getName() + */ + protected function getFieldTitles( + array $schemaIdentifiers, + ContentType $contentType, + array $fieldMap, + string $languageCode + ): array { + $fieldTitles = []; + + foreach ($schemaIdentifiers as $fieldDefinitionIdentifier) { + if (!isset($fieldMap[$fieldDefinitionIdentifier][$languageCode])) { + continue; + } + + $fieldDefinition = $contentType->getFieldDefinition($fieldDefinitionIdentifier); + $persistenceFieldType = $this->fieldTypeRegistry->getFieldType( + $fieldDefinition->fieldTypeIdentifier + ); + + $fieldTitles[$fieldDefinitionIdentifier] = $persistenceFieldType->getName( + $fieldMap[$fieldDefinitionIdentifier][$languageCode], + $fieldDefinition, + $languageCode + ); + } + + return $fieldTitles; + } + + /** + * Extract all tokens from $namePattern. + * + * Example: + * + * Text more text ==> + * + */ + protected function extractTokens(string $nameSchema): array + { + preg_match_all( + '|<([^>]+)>|U', + $nameSchema, + $tokenArray + ); + + return $tokenArray[0]; + } + + /** + * Looks up the value $token should be replaced with and returns this as + * a string. Meta strings denoting token groups are automatically + * inferred. + */ + protected function resolveToken(string $token, array $titles, array $groupLookupTable): string + { + $replaceString = ''; + $tokenParts = $this->tokenParts($token); + + foreach ($tokenParts as $tokenPart) { + if ($this->isTokenGroup($tokenPart)) { + $replaceString = $groupLookupTable[$tokenPart]; + $groupTokenArray = $this->extractTokens($replaceString); + + foreach ($groupTokenArray as $groupToken) { + $replaceString = str_replace( + $groupToken, + $this->resolveToken( + $groupToken, + $titles, + $groupLookupTable + ), + $replaceString + ); + } + + // We want to stop after the first matching token part / identifier is found + // if id1 has a value, id2 will not be used. + // In this case id1 or id1 is a token group. + break; + } + if (array_key_exists($tokenPart, $titles) + && $titles[$tokenPart] !== '' + && $titles[$tokenPart] !== null + ) { + $replaceString = $titles[$tokenPart]; + // We want to stop after the first matching token part / identifier is found + // if id1 has a value, id2 will not be used. + break; + } + } + + return $replaceString; + } + + /** + * Checks whether $identifier is a placeholder for a token group. + */ + protected function isTokenGroup(string $identifier): bool + { + return strpos($identifier, self::META_STRING) !== false; + } + + /** + * Returns the different constituents of $token in an array. + * The normal case here is that the different identifiers within one token + * will be tokenized and returned. + * + * Example: + * + * "<title|text>" ==> array( 'title', 'text' ) + * + * + * @param string $token + * + * @return array + */ + protected function tokenParts(string $token): array + { + return preg_split('/[^\w:]+/', $token, -1, PREG_SPLIT_NO_EMPTY); + } + + /** + * Builds a lookup / translation table for groups in the $namePattern. + * The groups are referenced with a generated meta-token in the original + * name pattern. + * + * Returns intermediate name pattern where groups are replaced with meta-tokens. + * + * @param string $nameSchema + * + * @return array{string, array} + */ + protected function filterNameSchema(string $nameSchema): array + { + $retNamePattern = ''; + $foundGroups = preg_match_all('/\((.+)\)/U', $nameSchema, $groupArray); + $groupLookupTable = []; + + if ($foundGroups) { + $i = 0; + foreach ($groupArray[1] as $group) { + // Create meta-token for group + $metaToken = self::META_STRING . $i; + + // Insert the group with its placeholder token + /** @var string $retNamePattern */ + $retNamePattern = str_replace($group, $metaToken, $nameSchema); + + // Remove the pattern "(" ")" from the tokens + $group = str_replace(['(', ')'], '', $group); + + $groupLookupTable[$metaToken] = $group; + ++$i; + } + $nameSchema = $retNamePattern; + } + + return [$nameSchema, $groupLookupTable]; + } + + /** + * @return array + */ + protected function getIdentifiers(string $schemaString): array + { + $allTokens = '#<(.*)>#U'; + $identifiers = '#\\W#'; + + $tmpArray = []; + preg_match_all($allTokens, $schemaString, $matches); + + foreach ($matches[1] as $match) { + $tmpArray[] = preg_split($identifiers, $match, -1, PREG_SPLIT_NO_EMPTY); + } + + $retArray = []; + foreach ($tmpArray as $matchGroup) { + if (is_array($matchGroup)) { + foreach ($matchGroup as $item) { + $retArray[] = $item; + } + } else { + $retArray[] = $matchGroup; + } + } + + return $retArray; + } + + public function validateNameLength(string $name): string + { + // Make sure length is not longer than $limit unless it's 0 + if ($this->settings['limit'] && mb_strlen($name) > $this->settings['limit']) { + $name = rtrim( + mb_substr($name, 0, $this->settings['limit'] - strlen($this->settings['sequence'])) + ) . $this->settings['sequence']; + } + + return $name; + } +} diff --git a/src/lib/Repository/NameSchema/SchemaIdentifierExtractor.php b/src/lib/Repository/NameSchema/SchemaIdentifierExtractor.php new file mode 100644 index 0000000000..cb7d76c51f --- /dev/null +++ b/src/lib/Repository/NameSchema/SchemaIdentifierExtractor.php @@ -0,0 +1,58 @@ +> + * + * @example + * $extractor = new SchemaIdentifierExtractor(); + * $schemaString = '--'; + * $result = $extractor->extract($schemaString); + * // $result will be: + * // [ + * // 'field' => ['foo', 'bar'], + * // 'attribute' => ['bar', 'baz'], + * // ] + */ + public function extract(string $schemaString): array + { + $allTokens = '/<([^>-]+)>/'; + + if (false === preg_match_all($allTokens, $schemaString, $matches)) { + return []; + } + + $strategyIdentifiers = []; + foreach ($matches[1] as $tokenExpression) { + $tokens = explode('|', $tokenExpression); + foreach ($tokens as $token) { + $strategyToken = explode(':', $token, 2); + + if (count($strategyToken) === 2) { + [$strategy, $token] = $strategyToken; + } else { + $token = $strategyToken[0]; + $strategy = 'field'; + } + + $token = preg_replace('/[()<>\[\]]/', '', $token); + $strategyIdentifiers[$strategy][] = $token; + } + + $strategyIdentifiers[$strategy] = array_unique($strategyIdentifiers[$strategy]); + } + + return $strategyIdentifiers; + } +} diff --git a/src/lib/Repository/Repository.php b/src/lib/Repository/Repository.php index 0255311edc..1b087379ac 100644 --- a/src/lib/Repository/Repository.php +++ b/src/lib/Repository/Repository.php @@ -19,6 +19,7 @@ use Ibexa\Contracts\Core\Repository\LanguageResolver; use Ibexa\Contracts\Core\Repository\LanguageService as LanguageServiceInterface; use Ibexa\Contracts\Core\Repository\LocationService as LocationServiceInterface; +use Ibexa\Contracts\Core\Repository\NameSchema\NameSchemaServiceInterface; use Ibexa\Contracts\Core\Repository\NotificationService as NotificationServiceInterface; use Ibexa\Contracts\Core\Repository\ObjectStateService as ObjectStateServiceInterface; use Ibexa\Contracts\Core\Repository\PasswordHashService; @@ -40,7 +41,6 @@ use Ibexa\Contracts\Core\Search\Handler as SearchHandler; use Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface; use Ibexa\Core\FieldType\FieldTypeRegistry; -use Ibexa\Core\Repository\Helper\NameSchemaService; use Ibexa\Core\Repository\Helper\RelationProcessor; use Ibexa\Core\Repository\Permission\LimitationService; use Ibexa\Core\Repository\ProxyFactory\ProxyDomainMapperFactoryInterface; @@ -150,12 +150,7 @@ class Repository implements RepositoryInterface /** @var \Ibexa\Core\FieldType\FieldTypeRegistry */ private $fieldTypeRegistry; - /** - * Instance of name schema resolver service. - * - * @var \Ibexa\Core\Repository\Helper\NameSchemaService - */ - protected $nameSchemaService; + protected NameSchemaServiceInterface $nameSchemaService; /** * Instance of relation processor service. @@ -287,6 +282,7 @@ public function __construct( LocationFilteringHandler $locationFilteringHandler, PasswordValidatorInterface $passwordValidator, ConfigResolverInterface $configResolver, + NameSchemaServiceInterface $nameSchemaService, array $serviceSettings = [], ?LoggerInterface $logger = null ) { @@ -332,11 +328,12 @@ public function __construct( $this->serviceSettings['language']['languages'] = $this->serviceSettings['languages']; } - $this->logger = null !== $logger ? $logger : new NullLogger(); + $this->logger = $logger ?? new NullLogger(); $this->contentMapper = $contentMapper; $this->contentValidator = $contentValidator; $this->passwordValidator = $passwordValidator; $this->configResolver = $configResolver; + $this->nameSchemaService = $nameSchemaService; } public function sudo(callable $callback, ?RepositoryInterface $outerRepository = null) @@ -727,29 +724,11 @@ public function getPermissionResolver(): PermissionResolverInterface } /** - * Get NameSchemaResolverService. - * - * - * @todo Move out from this & other repo instances when services becomes proper services in DIC terms using factory. - * * @internal * @private - * - * @return \Ibexa\Core\Repository\Helper\NameSchemaService */ - public function getNameSchemaService(): NameSchemaService + public function getNameSchemaService(): NameSchemaServiceInterface { - if ($this->nameSchemaService !== null) { - return $this->nameSchemaService; - } - - $this->nameSchemaService = new Helper\NameSchemaService( - $this->persistenceHandler->contentTypeHandler(), - $this->contentTypeDomainMapper, - $this->fieldTypeRegistry, - $this->serviceSettings['nameSchema'] - ); - return $this->nameSchemaService; } diff --git a/src/lib/Repository/TrashService.php b/src/lib/Repository/TrashService.php index 15f5340d6f..b88b705111 100644 --- a/src/lib/Repository/TrashService.php +++ b/src/lib/Repository/TrashService.php @@ -14,6 +14,7 @@ use Ibexa\Contracts\Core\Persistence\Content\Location\Trashed; use Ibexa\Contracts\Core\Persistence\Handler; use Ibexa\Contracts\Core\Repository\Exceptions\UnauthorizedException as APIUnauthorizedException; +use Ibexa\Contracts\Core\Repository\NameSchema\NameSchemaServiceInterface; use Ibexa\Contracts\Core\Repository\PermissionCriterionResolver; use Ibexa\Contracts\Core\Repository\PermissionResolver; use Ibexa\Contracts\Core\Repository\Repository as RepositoryInterface; @@ -50,8 +51,7 @@ class TrashService implements TrashServiceInterface /** @var array */ protected $settings; - /** @var \Ibexa\Core\Repository\Helper\NameSchemaService */ - protected $nameSchemaService; + protected NameSchemaServiceInterface $nameSchemaService; /** @var \Ibexa\Contracts\Core\Repository\PermissionCriterionResolver */ private $permissionCriterionResolver; @@ -67,7 +67,6 @@ class TrashService implements TrashServiceInterface * * @param \Ibexa\Contracts\Core\Repository\Repository $repository * @param \Ibexa\Contracts\Core\Persistence\Handler $handler - * @param \Ibexa\Core\Repository\Helper\NameSchemaService $nameSchemaService * @param \Ibexa\Contracts\Core\Repository\PermissionCriterionResolver $permissionCriterionResolver * @param \Ibexa\Contracts\Core\Repository\PermissionResolver $permissionResolver * @param array $settings @@ -75,7 +74,7 @@ class TrashService implements TrashServiceInterface public function __construct( RepositoryInterface $repository, Handler $handler, - Helper\NameSchemaService $nameSchemaService, + NameSchemaServiceInterface $nameSchemaService, PermissionCriterionResolver $permissionCriterionResolver, PermissionResolver $permissionResolver, ProxyDomainMapperInterface $proxyDomainMapper, diff --git a/src/lib/Repository/URLAliasService.php b/src/lib/Repository/URLAliasService.php index e4dd6db102..fb342f58cf 100644 --- a/src/lib/Repository/URLAliasService.php +++ b/src/lib/Repository/URLAliasService.php @@ -13,6 +13,7 @@ use Ibexa\Contracts\Core\Persistence\Content\UrlAlias\Handler; use Ibexa\Contracts\Core\Repository\Exceptions\ForbiddenException; use Ibexa\Contracts\Core\Repository\LanguageResolver; +use Ibexa\Contracts\Core\Repository\NameSchema\NameSchemaServiceInterface; use Ibexa\Contracts\Core\Repository\PermissionResolver; use Ibexa\Contracts\Core\Repository\Repository as RepositoryInterface; use Ibexa\Contracts\Core\Repository\URLAliasService as URLAliasServiceInterface; @@ -33,8 +34,7 @@ class URLAliasService implements URLAliasServiceInterface /** @var \Ibexa\Contracts\Core\Persistence\Content\UrlAlias\Handler */ protected $urlAliasHandler; - /** @var \Ibexa\Core\Repository\Helper\NameSchemaService */ - protected $nameSchemaService; + protected NameSchemaServiceInterface $nameSchemaService; /** @var \Ibexa\Contracts\Core\Repository\PermissionResolver */ private $permissionResolver; @@ -45,7 +45,7 @@ class URLAliasService implements URLAliasServiceInterface public function __construct( RepositoryInterface $repository, Handler $urlAliasHandler, - Helper\NameSchemaService $nameSchemaService, + NameSchemaServiceInterface $nameSchemaService, PermissionResolver $permissionResolver, LanguageResolver $languageResolver ) { diff --git a/src/lib/Resources/settings/events.yml b/src/lib/Resources/settings/events.yml index 185f72926e..4027da9755 100644 --- a/src/lib/Resources/settings/events.yml +++ b/src/lib/Resources/settings/events.yml @@ -5,3 +5,5 @@ services: public: false Ibexa\Core\Repository\EventSubscriber\DeleteUserSubscriber: ~ + + Ibexa\Core\Repository\EventSubscriber\NameSchemaSubscriber: ~ diff --git a/src/lib/Resources/settings/repository/inner.yml b/src/lib/Resources/settings/repository/inner.yml index 50eb0fd3ce..f36d0b9a7e 100644 --- a/src/lib/Resources/settings/repository/inner.yml +++ b/src/lib/Resources/settings/repository/inner.yml @@ -1,3 +1,6 @@ +imports: + - { resource: inner/name_schema.yaml } + parameters: ibexa.kernel.proxy_cache_dir: 'var/cache/repository/proxy' @@ -34,6 +37,7 @@ services: - '@Ibexa\Contracts\Core\Persistence\Filter\Location\Handler' - '@Ibexa\Core\Repository\User\PasswordValidatorInterface' - '@ibexa.config.resolver' + - '@Ibexa\Contracts\Core\Repository\NameSchema\NameSchemaServiceInterface' - '%languages%' Ibexa\Core\Repository\ContentService: diff --git a/src/lib/Resources/settings/repository/inner/name_schema.yaml b/src/lib/Resources/settings/repository/inner/name_schema.yaml new file mode 100644 index 0000000000..45f8fcad8e --- /dev/null +++ b/src/lib/Resources/settings/repository/inner/name_schema.yaml @@ -0,0 +1,20 @@ +parameters: + ibexa.core.repository.name_schema.settings: [] + +services: + _defaults: + autowire: true + autoconfigure: true + public: false + + Ibexa\Core\Repository\NameSchema\SchemaIdentifierExtractor: ~ + + Ibexa\Contracts\Core\Repository\NameSchema\SchemaIdentifierExtractorInterface: + alias: 'Ibexa\Core\Repository\NameSchema\SchemaIdentifierExtractor' + + Ibexa\Core\Repository\NameSchema\NameSchemaService: + arguments: + $settings: '%ibexa.core.repository.name_schema.settings%' + + Ibexa\Contracts\Core\Repository\NameSchema\NameSchemaServiceInterface: + alias: 'Ibexa\Core\Repository\NameSchema\NameSchemaService' diff --git a/tests/integration/Core/Repository/LocationServiceTest.php b/tests/integration/Core/Repository/LocationServiceTest.php index f7619d4faf..350347bda4 100644 --- a/tests/integration/Core/Repository/LocationServiceTest.php +++ b/tests/integration/Core/Repository/LocationServiceTest.php @@ -1728,14 +1728,14 @@ public function testSwapLocationForMainAndSecondaryLocation(): array $folder2 = $this->createFolder(['eng-GB' => 'Folder2'], 2); $folder3 = $this->createFolder(['eng-GB' => 'Folder3'], 2); - $primaryLocation = $locationService->loadLocation($folder1->contentInfo->mainLocationId); - $parentLocation = $locationService->loadLocation($folder2->contentInfo->mainLocationId); + $primaryLocation = $folder1->getVersionInfo()->getContentInfo()->getMainLocation(); + $parentLocation = $folder2->getVersionInfo()->getContentInfo()->getMainLocation(); $secondaryLocation = $locationService->createLocation( - $folder1->contentInfo, + $folder1->getVersionInfo()->getContentInfo(), $locationService->newLocationCreateStruct($parentLocation->id) ); - $targetLocation = $locationService->loadLocation($folder3->contentInfo->mainLocationId); + $targetLocation = $folder3->getVersionInfo()->getContentInfo()->getMainLocation(); // perform sanity checks $this->assertContentHasExpectedLocations([$primaryLocation, $secondaryLocation], $folder1); @@ -1748,26 +1748,26 @@ public function testSwapLocationForMainAndSecondaryLocation(): array $secondaryLocation = $locationService->loadLocation($secondaryLocation->id); $targetLocation = $locationService->loadLocation($targetLocation->id); - self::assertEquals($folder1->id, $primaryLocation->contentInfo->id); - self::assertEquals($folder1->id, $targetLocation->contentInfo->id); - self::assertEquals($folder3->id, $secondaryLocation->contentInfo->id); + self::assertEquals($folder1->id, $primaryLocation->getContentInfo()->getId()); + self::assertEquals($folder1->id, $targetLocation->getContentInfo()->getId()); + self::assertEquals($folder3->id, $secondaryLocation->getContentInfo()->getId()); $this->assertContentHasExpectedLocations([$primaryLocation, $targetLocation], $folder1); self::assertEquals( - $folder1, - $contentService->loadContent($folder1->id, Language::ALL) + $primaryLocation->id, + $contentService->loadContent($folder1->id)->getVersionInfo()->getContentInfo()->getMainLocationId() ); self::assertEquals( - $folder2, - $contentService->loadContent($folder2->id, Language::ALL) + $parentLocation->id, + $contentService->loadContent($folder2->id)->getVersionInfo()->getContentInfo()->getMainLocationId() ); // only in case of Folder 3, main location id changed due to swap self::assertEquals( $secondaryLocation->id, - $contentService->loadContent($folder3->id)->contentInfo->mainLocationId + $contentService->loadContent($folder3->id)->getVersionInfo()->getContentInfo()->getMainLocation()->id ); return [$folder1, $folder2, $folder3]; @@ -1781,7 +1781,7 @@ public function testSwapLocationForMainAndSecondaryLocation(): array * * @throws \Ibexa\Contracts\Core\Repository\Exceptions\BadStateException */ - private function assertContentHasExpectedLocations(array $expectedLocations, Content $content) + private function assertContentHasExpectedLocations(array $expectedLocations, Content $content): void { $repository = $this->getRepository(false); $locationService = $repository->getLocationService(); @@ -1871,25 +1871,24 @@ static function (SearchHit $searchHit) { * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException * @throws \Ibexa\Contracts\Core\Repository\Exceptions\UnauthorizedException */ - public function testSwapLocationForSecondaryLocations() + public function testSwapLocationForSecondaryLocations(): void { $repository = $this->getRepository(); $locationService = $repository->getLocationService(); - $contentService = $repository->getContentService(); $folder1 = $this->createFolder(['eng-GB' => 'Folder1'], 2); $folder2 = $this->createFolder(['eng-GB' => 'Folder2'], 2); $parentFolder1 = $this->createFolder(['eng-GB' => 'Parent1'], 2); $parentFolder2 = $this->createFolder(['eng-GB' => 'Parent2'], 2); - $parentLocation1 = $locationService->loadLocation($parentFolder1->contentInfo->mainLocationId); - $parentLocation2 = $locationService->loadLocation($parentFolder2->contentInfo->mainLocationId); + $parentLocation1 = $parentFolder1->getVersionInfo()->getContentInfo()->getMainLocation(); + $parentLocation2 = $parentFolder2->getVersionInfo()->getContentInfo()->getMainLocation(); $secondaryLocation1 = $locationService->createLocation( - $folder1->contentInfo, + $folder1->getVersionInfo()->getContentInfo(), $locationService->newLocationCreateStruct($parentLocation1->id) ); $secondaryLocation2 = $locationService->createLocation( - $folder2->contentInfo, + $folder2->getVersionInfo()->getContentInfo(), $locationService->newLocationCreateStruct($parentLocation2->id) ); @@ -1900,17 +1899,23 @@ public function testSwapLocationForSecondaryLocations() $secondaryLocation1 = $locationService->loadLocation($secondaryLocation1->id); $secondaryLocation2 = $locationService->loadLocation($secondaryLocation2->id); - self::assertEquals($folder2->id, $secondaryLocation1->contentInfo->id); - self::assertEquals($folder1->id, $secondaryLocation2->contentInfo->id); + self::assertEquals($folder2->id, $secondaryLocation1->getContentInfo()->getId()); + self::assertEquals($folder1->id, $secondaryLocation2->getContentInfo()->getId()); - self::assertEquals( - $folder1, - $contentService->loadContent($folder1->id, Language::ALL) + self::assertEqualsCanonicalizing( + [$folder1->getVersionInfo()->getContentInfo()->getMainLocationId(), $secondaryLocation2->id], + array_map( + static fn (Location $location): int => $location->id, + $locationService->loadLocations($folder1->getVersionInfo()->getContentInfo()) + ) ); - self::assertEquals( - $folder2, - $contentService->loadContent($folder2->id, Language::ALL) + self::assertEqualsCanonicalizing( + [$folder2->getVersionInfo()->getContentInfo()->getMainLocationId(), $secondaryLocation1->id], + array_map( + static fn (Location $location): int => $location->id, + $locationService->loadLocations($folder2->getVersionInfo()->getContentInfo()) + ) ); } diff --git a/tests/integration/Core/Repository/URLAliasService/UrlAliasLookupTest.php b/tests/integration/Core/Repository/URLAliasService/UrlAliasLookupTest.php new file mode 100644 index 0000000000..d61140be32 --- /dev/null +++ b/tests/integration/Core/Repository/URLAliasService/UrlAliasLookupTest.php @@ -0,0 +1,35 @@ +createFolder(['eng-GB' => 'Foo']); + $folderMainLocation = $folder->getVersionInfo()->getContentInfo()->getMainLocation(); + $urlAlias = $urlAliasService->lookup('/Foo'); + self::assertSame( + $folderMainLocation->id, + $urlAlias->destination + ); + $systemUrlAliasList = $urlAliasService->listLocationAliases($folderMainLocation, false); + self::assertCount(1, $systemUrlAliasList); + self::assertEquals($urlAlias, $systemUrlAliasList[0]); + } +} diff --git a/tests/integration/Core/Repository/UserServiceTest.php b/tests/integration/Core/Repository/UserServiceTest.php index 61fd46d42c..e33f4a24e4 100644 --- a/tests/integration/Core/Repository/UserServiceTest.php +++ b/tests/integration/Core/Repository/UserServiceTest.php @@ -918,10 +918,7 @@ public function testNewUserCreateStructWithFifthParameter() $this->assertSame($userType, $userCreate->contentType); } - /** - * Test for creating user with Active Directory login name. - */ - public function testNewUserWithDomainName() + public function testNewUserWithDomainName(): void { $repository = $this->getRepository(); $userService = $repository->getUserService(); @@ -931,7 +928,7 @@ public function testNewUserWithDomainName() ); $loadedUser = $userService->loadUserByLogin('ibexa-user-Domain\username-by-login', Language::ALL); - $this->assertEquals($createdUser, $loadedUser); + $this->assertIsSameUser($createdUser, $loadedUser); } /** @@ -1334,12 +1331,13 @@ public function testCreateUserWithStrongPassword() } /** - * Test for the loadUser() method. - * * @covers \Ibexa\Contracts\Core\Repository\UserService::loadUser() + * * @depends testCreateUser + * + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException */ - public function testLoadUser() + public function testLoadUser(): void { $repository = $this->getRepository(); @@ -1352,11 +1350,11 @@ public function testLoadUser() $userReloaded = $userService->loadUser($user->id, Language::ALL); /* END: Use Case */ - $this->assertEquals($user, $userReloaded); + $this->assertIsSameUser($user, $userReloaded); // User happens to also be a Content; isUser() should be true and isUserGroup() should be false - $this->assertTrue($userService->isUser($user), 'isUser() => false on a user'); - $this->assertFalse($userService->isUserGroup($user), 'isUserGroup() => true on a user group'); + self::assertTrue($userService->isUser($user), 'isUser() => false on a user'); + self::assertFalse($userService->isUserGroup($user), 'isUserGroup() => true on a user group'); } /** @@ -1541,25 +1539,23 @@ public function testLoadUserByLoginThrowsNotFoundExceptionForUnknownLoginByEmail } /** - * Test for the loadUsersByEmail() method. - * * @covers \Ibexa\Contracts\Core\Repository\UserService::loadUsersByEmail() + * * @depends testCreateUser */ - public function testLoadUserByEmail() + public function testLoadUserByEmail(): void { $repository = $this->getRepository(); $userService = $repository->getUserService(); - /* BEGIN: Use Case */ $user = $this->createUserVersion1(); // Load the newly created user $usersReloaded = $userService->loadUsersByEmail('user@example.com', Language::ALL); - /* END: Use Case */ - $this->assertEquals([$user], $usersReloaded); + self::assertCount(1, $usersReloaded); + $this->assertIsSameUser($user, $usersReloaded[0]); } /** @@ -2944,11 +2940,9 @@ public function testCreateUserWithDefaultPasswordHashTypeWhenHashTypeIsUnsupport } /** - * Test loading User by Token. - * * @covers \Ibexa\Contracts\Core\Repository\UserService::loadUserByToken */ - public function testLoadUserByToken() + public function testLoadUserByToken(): string { $repository = $this->getRepository(); $userService = $repository->getUserService(); @@ -2962,7 +2956,7 @@ public function testLoadUserByToken() $userService->updateUserToken($user, $userTokenUpdateStruct); $loadedUser = $userService->loadUserByToken($userTokenUpdateStruct->hashKey, Language::ALL); - self::assertEquals($user, $loadedUser); + $this->assertIsSameUser($user, $loadedUser); return $userTokenUpdateStruct->hashKey; } @@ -3445,6 +3439,14 @@ protected function updateRawPasswordHash(int $userId, int $newHashType): void $queryBuilder->execute(); } + + private function assertIsSameUser(User $expectedUser, User $actualUser): void + { + self::assertSame($expectedUser->getUserId(), $actualUser->getUserId()); + self::assertSame($expectedUser->getName(), $actualUser->getName()); + self::assertSame($expectedUser->login, $actualUser->login); + self::assertSame($expectedUser->email, $actualUser->email); + } } class_alias(UserServiceTest::class, 'eZ\Publish\API\Repository\Tests\UserServiceTest'); diff --git a/tests/integration/Core/RepositoryTestCase.php b/tests/integration/Core/RepositoryTestCase.php new file mode 100644 index 0000000000..6a4955b112 --- /dev/null +++ b/tests/integration/Core/RepositoryTestCase.php @@ -0,0 +1,73 @@ + $names + * + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\Exception + */ + public function createFolder(array $names, int $parentLocationId = self::CONTENT_TREE_ROOT_ID): Content + { + $contentService = self::getContentService(); + $draft = $this->createFolderDraft($names, $parentLocationId); + + return $contentService->publishVersion($draft->getVersionInfo()); + } + + /** + * @param array $names + * + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\Exception + */ + public function createFolderDraft(array $names, int $parentLocationId = self::CONTENT_TREE_ROOT_ID): Content + { + if (empty($names)) { + throw new InvalidArgumentException(__METHOD__ . ' requires $names to be not empty'); + } + + $contentService = self::getContentService(); + $contentTypeService = self::getContentTypeService(); + $locationService = self::getLocationService(); + + $folderType = $contentTypeService->loadContentTypeByIdentifier(self::CONTENT_TYPE_FOLDER_IDENTIFIER); + $mainLanguageCode = array_keys($names)[0]; + $contentCreateStruct = $contentService->newContentCreateStruct($folderType, $mainLanguageCode); + foreach ($names as $languageCode => $name) { + $contentCreateStruct->setField('name', $name, $languageCode); + } + + return $contentService->createContent( + $contentCreateStruct, + [ + $locationService->newLocationCreateStruct($parentLocationId), + ] + ); + } +} diff --git a/tests/integration/Core/Resources/settings/common.yml b/tests/integration/Core/Resources/settings/common.yml index 3858800506..d2aad48c13 100644 --- a/tests/integration/Core/Resources/settings/common.yml +++ b/tests/integration/Core/Resources/settings/common.yml @@ -12,7 +12,16 @@ services: logger: class: Psr\Log\NullLogger - Symfony\Component\EventDispatcher\EventDispatcher: ~ + Symfony\Component\EventDispatcher\EventDispatcher: + calls: + - [ 'addSubscriber', [ '@Ibexa\Core\Search\Common\EventSubscriber\ContentEventSubscriber' ] ] + - [ 'addSubscriber', [ '@Ibexa\Core\Search\Common\EventSubscriber\LocationEventSubscriber' ] ] + - [ 'addSubscriber', [ '@Ibexa\Core\Search\Common\EventSubscriber\ObjectStateEventSubscriber' ] ] + - [ 'addSubscriber', [ '@Ibexa\Core\Search\Common\EventSubscriber\SectionEventSubscriber' ] ] + - [ 'addSubscriber', [ '@Ibexa\Core\Search\Common\EventSubscriber\TrashEventSubscriber' ] ] + - [ 'addSubscriber', [ '@Ibexa\Core\Search\Common\EventSubscriber\UserEventSubscriber' ] ] + - [ 'addSubscriber', [ '@Ibexa\Core\Repository\EventSubscriber\NameSchemaSubscriber' ] ] + Symfony\Contracts\EventDispatcher\EventDispatcherInterface: '@Symfony\Component\EventDispatcher\EventDispatcher' # By default use in-memory cache for tests to avoid disk IO but still make sure we tests cache clearing works diff --git a/tests/integration/Core/Resources/settings/integration_legacy.yml b/tests/integration/Core/Resources/settings/integration_legacy.yml index f1cdd2294f..d7caf0c52d 100644 --- a/tests/integration/Core/Resources/settings/integration_legacy.yml +++ b/tests/integration/Core/Resources/settings/integration_legacy.yml @@ -27,15 +27,6 @@ services: Ibexa\DoctrineSchema\Database\DbPlatform\DbPlatformInterface: tags: [ ibexa.doctrine.db.platform ] - Symfony\Component\EventDispatcher\EventDispatcher: - calls: - - ['addSubscriber', ['@Ibexa\Core\Search\Common\EventSubscriber\ContentEventSubscriber']] - - ['addSubscriber', ['@Ibexa\Core\Search\Common\EventSubscriber\LocationEventSubscriber']] - - ['addSubscriber', ['@Ibexa\Core\Search\Common\EventSubscriber\ObjectStateEventSubscriber']] - - ['addSubscriber', ['@Ibexa\Core\Search\Common\EventSubscriber\SectionEventSubscriber']] - - ['addSubscriber', ['@Ibexa\Core\Search\Common\EventSubscriber\TrashEventSubscriber']] - - ['addSubscriber', ['@Ibexa\Core\Search\Common\EventSubscriber\UserEventSubscriber']] - Doctrine\Common\EventManager: ~ Ibexa\DoctrineSchema\Database\DbPlatform\SqliteDbPlatform: diff --git a/tests/lib/Repository/ContentServiceTest.php b/tests/lib/Repository/ContentServiceTest.php index 93fccf69f2..ce2e14a2aa 100644 --- a/tests/lib/Repository/ContentServiceTest.php +++ b/tests/lib/Repository/ContentServiceTest.php @@ -10,13 +10,13 @@ use Ibexa\Contracts\Core\Persistence\Filter\Content\Handler as ContentFilteringHandler; use Ibexa\Contracts\Core\Persistence\Handler as PersistenceHandler; +use Ibexa\Contracts\Core\Repository\NameSchema\NameSchemaServiceInterface; use Ibexa\Contracts\Core\Repository\PermissionService; use Ibexa\Contracts\Core\Repository\Repository; use Ibexa\Contracts\Core\Repository\Validator\ContentValidator; use Ibexa\Contracts\Core\Repository\Values\Filter\Filter; use Ibexa\Core\FieldType\FieldTypeRegistry; use Ibexa\Core\Repository\ContentService; -use Ibexa\Core\Repository\Helper\NameSchemaService; use Ibexa\Core\Repository\Helper\RelationProcessor; use Ibexa\Core\Repository\Mapper\ContentDomainMapper; use Ibexa\Core\Repository\Mapper\ContentMapper; @@ -37,7 +37,7 @@ protected function setUp(): void $this->createMock(PersistenceHandler::class), $this->createMock(ContentDomainMapper::class), $this->createMock(RelationProcessor::class), - $this->createMock(NameSchemaService::class), + $this->createMock(NameSchemaServiceInterface::class), $this->createMock(FieldTypeRegistry::class), $this->createMock(PermissionService::class), $this->createMock(ContentMapper::class), diff --git a/tests/lib/Repository/NameSchema/NameSchemaServiceTest.php b/tests/lib/Repository/NameSchema/NameSchemaServiceTest.php new file mode 100644 index 0000000000..a1c6bdfa03 --- /dev/null +++ b/tests/lib/Repository/NameSchema/NameSchemaServiceTest.php @@ -0,0 +1,340 @@ +'; + + public function testResolveUrlAliasSchema(): void + { + $content = $this->buildTestContentObject(); + $contentType = $this->buildTestContentTypeStub(); + + $nameSchemaService = $this->buildNameSchemaService( + ['field' => ['']], + $content, + ['eng-GB' => ['url_alias_schema' => 'foo']] + ); + + $result = $nameSchemaService->resolveUrlAliasSchema($content, $contentType); + + self::assertEquals(['eng-GB' => 'foo'], $result); + } + + public function testResolveUrlAliasSchemaFallbackToNameSchema(): void + { + $content = $this->buildTestContentObject(); + $contentType = $this->buildTestContentTypeStub(self::NAME_SCHEMA, ''); + + $nameSchemaService = $this->buildNameSchemaService( + ['field' => [self::NAME_SCHEMA]], + $content, + ['eng-GB' => ['name_schema' => 'bar']] + ); + + $result = $nameSchemaService->resolveUrlAliasSchema($content, $contentType); + + self::assertEquals(['eng-GB' => 'bar'], $result); + } + + /** + * @return iterable>, array, array> + */ + public static function getDataForTestResolveNameSchema(): iterable + { + yield 'Default: Field Map and Languages taken from Content Version' => [ + [], + [], + [ + 'eng-GB' => 'two', + 'cro-HR' => 'dva', + ], + ]; + + yield 'Field Map and Languages for update' => [ + [ + 'text1' => ['cro-HR' => new TextLineValue('jedan'), 'eng-GB' => new TextLineValue('one')], + 'text2' => ['cro-HR' => new TextLineValue('Dva'), 'eng-GB' => new TextLineValue('two')], + 'text3' => ['eng-GB' => new TextLineValue('three')], + ], + ['eng-GB', 'cro-HR'], + [ + 'eng-GB' => 'three', + 'cro-HR' => 'Dva', + ], + ]; + } + + /** + * @dataProvider getDataForTestResolveNameSchema + * + * @param array> $fieldMap A map of Field Definition Identifier and Language Code to Field Value + * @param array $languageCodes + * @param array $expectedNames + */ + public function testResolveNameSchema(array $fieldMap, array $languageCodes, array $expectedNames): void + { + $content = $this->buildTestContentObject(); + $nameSchema = ''; + $nameSchemaService = $this->buildNameSchemaService( + ['field' => [$nameSchema]], + $content, + [] + ); + $contentType = $this->buildTestContentTypeStub($nameSchema, $nameSchema); + + $result = $nameSchemaService->resolveNameSchema($content, $fieldMap, $languageCodes, $contentType); + + self::assertEquals( + $expectedNames, + $result + ); + } + + /** + * Data provider for the testResolve method. + * + * @see testResolve + */ + public static function getDataForTestResolve(): array + { + return [ + [ + ['text1'], + '', + [ + 'eng-GB' => 'one', + 'cro-HR' => 'jedan', + ], + [ + 'eng-GB' => ['text1' => 'one'], + 'cro-HR' => ['text1' => 'jedan'], + ], + ], + [ + ['text2'], + '', + [ + 'eng-GB' => 'two', + 'cro-HR' => 'dva', + ], + [ + 'eng-GB' => ['text2' => 'two'], + 'cro-HR' => ['text2' => 'dva'], + ], + ], + [ + ['text1', 'text2'], + 'Hello, and and then goodbye and hello again', + [ + 'eng-GB' => 'Hello, one and two and then goodbye...', + 'cro-HR' => 'Hello, jedan and dva and then goodb...', + ], + [ + 'eng-GB' => ['text1' => 'one', 'text2' => 'two'], + 'cro-HR' => ['text1' => 'jedan', 'text2' => 'dva'], + ], + [ + 'limit' => 38, + 'sequence' => '...', + ], + ], + ]; + } + + /** + * @dataProvider getDataForTestResolve + * + * @param string[] $schemaIdentifiers + * @param string[] $languageFieldValues field value translations + * @param string[] $fieldTitles [language => [field_identifier => title]] + * @param array $settings NameSchemaService settings + */ + public function testResolve( + array $schemaIdentifiers, + string $nameSchema, + array $languageFieldValues, + array $fieldTitles, + array $settings = [] + ): void { + $content = $this->buildTestContentObject(); + $nameSchemaService = $this->buildNameSchemaService( + ['field' => [$nameSchema]], + $content, + [], + $settings + ); + $contentType = $this->buildTestContentTypeStub($nameSchema, $nameSchema); + + $result = $nameSchemaService->resolve( + $nameSchema, + $contentType, + $content->fields, + $content->versionInfo->languageCodes + ); + + self::assertEquals($languageFieldValues, $result); + } + + /** + * @return \Traversable<\Ibexa\Contracts\Core\Repository\Values\Content\Field> + */ + protected function getFields(): Traversable + { + $translatedFieldValueMap = [ + 'eng-GB' => [ + 'text1' => 'one', + 'text2' => 'two', + 'text3' => '', + ], + 'cro-HR' => [ + 'text1' => 'jedan', + 'text2' => 'dva', + 'text3' => '', + ], + ]; + + foreach ($translatedFieldValueMap as $languageCode => $fieldValues) { + foreach ($fieldValues as $fieldDefinitionIdentifier => $textValue) { + yield new Field( + [ + 'languageCode' => $languageCode, + 'fieldDefIdentifier' => $fieldDefinitionIdentifier, + 'value' => new TextLineValue($textValue), + 'fieldTypeIdentifier' => 'ezstring', + ] + ); + } + } + } + + protected function getFieldDefinitions(): APIFieldDefinitionCollection + { + return new FieldDefinitionCollection( + [ + new FieldDefinition( + [ + 'id' => '1', + 'identifier' => 'text1', + 'fieldTypeIdentifier' => 'ezstring', + ] + ), + new FieldDefinition( + [ + 'id' => '2', + 'identifier' => 'text2', + 'fieldTypeIdentifier' => 'ezstring', + ] + ), + new FieldDefinition( + [ + 'id' => '3', + 'identifier' => 'text3', + 'fieldTypeIdentifier' => 'ezstring', + ] + ), + ] + ); + } + + /** + * Build Content Object stub for testing purpose. + * + * @return \Ibexa\Contracts\Core\Repository\Values\Content\Content + */ + protected function buildTestContentObject() + { + return new Content( + [ + 'internalFields' => iterator_to_array($this->getFields()), + 'versionInfo' => new VersionInfo( + [ + 'languageCodes' => ['eng-GB', 'cro-HR'], + ] + ), + ] + ); + } + + protected function buildTestContentTypeStub( + string $nameSchema = '', + string $urlAliasSchema = '' + ): ContentType { + return new ContentType( + [ + 'nameSchema' => $nameSchema, + 'urlAliasSchema' => $urlAliasSchema, + 'fieldDefinitions' => $this->getFieldDefinitions(), + ] + ); + } + + /** + * @param array> $schemaIdentifiers + */ + protected function getEventDispatcherMock( + array $schemaIdentifiers, + Content $content, + array $tokenValues + ): EventDispatcherInterface { + $event = new ResolveUrlAliasSchemaEvent($schemaIdentifiers, $content); + $event->setTokenValues($tokenValues); + + $eventDispatcherMock = $this->getEventDispatcher(); + $eventDispatcherMock->method('dispatch') + ->willReturn($event); + + return $eventDispatcherMock; + } + + /** + * @param array> $schemaIdentifiers + * @param array> $tokenValues + * @param array{limit?: integer, sequence?: string} $settings + */ + private function buildNameSchemaService( + array $schemaIdentifiers, + Content $content, + array $tokenValues, + array $settings = [] + ): NameSchemaService { + $fieldTypeRegistryMock = $this->getFieldTypeRegistryMock(); + $fieldTypeRegistryMock + ->method('getFieldType') + ->with('ezstring') + ->willReturn(new TextLineFieldType()); + + return new NameSchemaService( + $fieldTypeRegistryMock, + new SchemaIdentifierExtractor(), + $this->getEventDispatcherMock($schemaIdentifiers, $content, $tokenValues), + $settings + ); + } +} diff --git a/tests/lib/Repository/NameSchema/SchemaIdentifierExtractorTest.php b/tests/lib/Repository/NameSchema/SchemaIdentifierExtractorTest.php new file mode 100644 index 0000000000..e40378ba0e --- /dev/null +++ b/tests/lib/Repository/NameSchema/SchemaIdentifierExtractorTest.php @@ -0,0 +1,104 @@ +>}> + */ + public function getDataForTestExtract(): iterable + { + $schemaString = ''; + yield $schemaString => [ + $schemaString, + [ + 'field' => ['short_name', 'name'], + ], + ]; + + $schemaString = ''; + yield $schemaString => [ + $schemaString, + [ + 'custom_strategy' => ['foo'], + 'field' => ['bar'], + ], + ]; + + $schemaString = ''; + yield $schemaString => [ + $schemaString, + [ + 'custom_strategy' => ['bar'], + 'field' => ['baz'], + ], + ]; + + $schemaString = '-'; + yield $schemaString => [ + $schemaString, + [ + 'custom_strategy' => ['foo'], + 'field' => ['bar'], + ], + ]; + + $schemaString = '-'; + yield $schemaString => [ + $schemaString, + [ + 'custom_strategy' => ['foo', 'bar'], + 'field' => ['bar', 'baz'], + ], + ]; + + $schemaString = ' )--'; + yield $schemaString => [ + $schemaString, + [ + 'field' => ['specification', 'name', 'image1', 'baz', 'bar'], + 'custom' => ['bar'], + ], + ]; + + $schemaString = ' )-()-'; + yield $schemaString => [ + $schemaString, + [ + 'field' => ['specification', 'name', 'image1', 'baz', 'bar'], + 'custom' => ['bar'], + ], + ]; + } + + protected function setUp(): void + { + $this->extractor = new SchemaIdentifierExtractor(); + } + + /** + * @dataProvider getDataForTestExtract + * + * @param array> $expectedStrategyIdentifierMap + */ + public function testExtract(string $schemaString, array $expectedStrategyIdentifierMap): void + { + self::assertSame($expectedStrategyIdentifierMap, $this->extractor->extract($schemaString)); + } +} diff --git a/tests/lib/Repository/Service/Mock/Base.php b/tests/lib/Repository/Service/Mock/Base.php index a2df11176d..dfda628e4f 100644 --- a/tests/lib/Repository/Service/Mock/Base.php +++ b/tests/lib/Repository/Service/Mock/Base.php @@ -10,6 +10,7 @@ use Ibexa\Contracts\Core\Persistence\Filter\Location\Handler as LocationFilteringHandler; use Ibexa\Contracts\Core\Persistence\Handler; use Ibexa\Contracts\Core\Repository\LanguageResolver; +use Ibexa\Contracts\Core\Repository\NameSchema\NameSchemaServiceInterface; use Ibexa\Contracts\Core\Repository\PasswordHashService; use Ibexa\Contracts\Core\Repository\PermissionService; use Ibexa\Contracts\Core\Repository\Repository as APIRepository; @@ -38,6 +39,7 @@ use Ibexa\Core\Search\Common\BackgroundIndexer\NullIndexer; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** * Base test case for tests on services using Mock testing. @@ -126,6 +128,7 @@ protected function getRepository(array $serviceSettings = []) $this->getLocationFilteringHandlerMock(), $this->createMock(PasswordValidatorInterface::class), $this->createMock(ConfigResolverInterface::class), + $this->createMock(NameSchemaServiceInterface::class), $serviceSettings, ); @@ -167,6 +170,20 @@ protected function getFieldTypeRegistryMock() return $this->fieldTypeRegistryMock; } + protected EventDispatcherInterface $eventDispatcher; + + /** + * @return \Symfony\Contracts\EventDispatcher\EventDispatcherInterface&\PHPUnit\Framework\MockObject\MockObject + */ + protected function getEventDispatcher(): EventDispatcherInterface + { + if (!isset($this->eventDispatcher)) { + $this->eventDispatcher = $this->createMock(EventDispatcherInterface::class); + } + + return $this->eventDispatcher; + } + /** * @return \Ibexa\Contracts\Core\Repository\Strategy\ContentThumbnail\ThumbnailStrategy|\PHPUnit\Framework\MockObject\MockObject */ diff --git a/tests/lib/Repository/Service/Mock/ContentTest.php b/tests/lib/Repository/Service/Mock/ContentTest.php index c60cbb92e0..5f6a4558c7 100644 --- a/tests/lib/Repository/Service/Mock/ContentTest.php +++ b/tests/lib/Repository/Service/Mock/ContentTest.php @@ -27,6 +27,7 @@ use Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException as APINotFoundException; use Ibexa\Contracts\Core\Repository\Exceptions\UnauthorizedException; use Ibexa\Contracts\Core\Repository\LocationService as APILocationService; +use Ibexa\Contracts\Core\Repository\NameSchema\NameSchemaServiceInterface; use Ibexa\Contracts\Core\Repository\Repository; use Ibexa\Contracts\Core\Repository\Values\Content\Content as APIContent; use Ibexa\Contracts\Core\Repository\Values\Content\ContentCreateStruct as APIContentCreateStruct; @@ -45,7 +46,6 @@ use Ibexa\Core\FieldType\ValidationError; use Ibexa\Core\FieldType\Value; use Ibexa\Core\Repository\ContentService; -use Ibexa\Core\Repository\Helper\NameSchemaService; use Ibexa\Core\Repository\Helper\RelationProcessor; use Ibexa\Core\Repository\Values\Content\Content; use Ibexa\Core\Repository\Values\Content\ContentCreateStruct; @@ -6215,15 +6215,20 @@ protected function getRelationProcessorMock() return $this->relationProcessorMock; } - protected $nameSchemaServiceMock; + /** + * @var \PHPUnit\Framework\MockObject\MockObject + * &\Ibexa\Contracts\Core\Repository\NameSchema\NameSchemaServiceInterface + */ + protected NameSchemaServiceInterface $nameSchemaServiceMock; /** - * @return \PHPUnit\Framework\MockObject\MockObject|\Ibexa\Core\Repository\Helper\NameSchemaService + * @return \PHPUnit\Framework\MockObject\MockObject + * &\Ibexa\Contracts\Core\Repository\NameSchema\NameSchemaServiceInterface */ - protected function getNameSchemaServiceMock() + protected function getNameSchemaServiceMock(): NameSchemaServiceInterface { if (!isset($this->nameSchemaServiceMock)) { - $this->nameSchemaServiceMock = $this->createMock(NameSchemaService::class); + $this->nameSchemaServiceMock = $this->createMock(NameSchemaServiceInterface::class); } return $this->nameSchemaServiceMock; diff --git a/tests/lib/Repository/Service/Mock/NameSchemaTest.php b/tests/lib/Repository/Service/Mock/NameSchemaTest.php deleted file mode 100644 index 47169233d1..0000000000 --- a/tests/lib/Repository/Service/Mock/NameSchemaTest.php +++ /dev/null @@ -1,374 +0,0 @@ -getPartlyMockedNameSchemaService(['resolve']); - - $content = $this->buildTestContentObject(); - $contentType = $this->buildTestContentType(); - - $serviceMock->expects( - $this->once() - )->method( - 'resolve' - )->with( - '', - $this->equalTo($contentType), - $this->equalTo($content->fields), - $this->equalTo($content->versionInfo->languageCodes) - )->will( - $this->returnValue(42) - ); - - $result = $serviceMock->resolveUrlAliasSchema($content, $contentType); - - self::assertEquals(42, $result); - } - - public function testResolveUrlAliasSchemaFallbackToNameSchema() - { - $serviceMock = $this->getPartlyMockedNameSchemaService(['resolve']); - - $content = $this->buildTestContentObject(); - $contentType = $this->buildTestContentType('', ''); - - $serviceMock->expects( - $this->once() - )->method( - 'resolve' - )->with( - '', - $this->equalTo($contentType), - $this->equalTo($content->fields), - $this->equalTo($content->versionInfo->languageCodes) - )->will( - $this->returnValue(42) - ); - - $result = $serviceMock->resolveUrlAliasSchema($content, $contentType); - - self::assertEquals(42, $result); - } - - public function testResolveNameSchema() - { - $serviceMock = $this->getPartlyMockedNameSchemaService(['resolve']); - - $content = $this->buildTestContentObject(); - $contentType = $this->buildTestContentType(); - - $serviceMock->expects( - $this->once() - )->method( - 'resolve' - )->with( - '', - $this->equalTo($contentType), - $this->equalTo($content->fields), - $this->equalTo($content->versionInfo->languageCodes) - )->will( - $this->returnValue(42) - ); - - $result = $serviceMock->resolveNameSchema($content, [], [], $contentType); - - self::assertEquals(42, $result); - } - - public function testResolveNameSchemaWithFields() - { - $serviceMock = $this->getPartlyMockedNameSchemaService(['resolve']); - - $content = $this->buildTestContentObject(); - $contentType = $this->buildTestContentType(); - - $fields = []; - $fields['text3']['cro-HR'] = new TextLineValue('tri'); - $fields['text1']['ger-DE'] = new TextLineValue('ein'); - $fields['text2']['ger-DE'] = new TextLineValue('zwei'); - $fields['text3']['ger-DE'] = new TextLineValue('drei'); - $mergedFields = $fields; - $mergedFields['text1']['cro-HR'] = new TextLineValue('jedan'); - $mergedFields['text2']['cro-HR'] = new TextLineValue('dva'); - $mergedFields['text1']['eng-GB'] = new TextLineValue('one'); - $mergedFields['text2']['eng-GB'] = new TextLineValue('two'); - $mergedFields['text3']['eng-GB'] = new TextLineValue(''); - $languages = ['eng-GB', 'cro-HR', 'ger-DE']; - - $serviceMock->expects( - $this->once() - )->method( - 'resolve' - )->with( - '', - $this->equalTo($contentType), - $this->equalTo($mergedFields), - $this->equalTo($languages) - )->will( - $this->returnValue(42) - ); - - $result = $serviceMock->resolveNameSchema($content, $fields, $languages, $contentType); - - self::assertEquals(42, $result); - } - - /** - * @dataProvider resolveDataProvider - * - * @param string[] $schemaIdentifiers - * @param string $nameSchema - * @param string[] $languageFieldValues field value translations - * @param string[] $fieldTitles [language => [field_identifier => title]] - * @param array $settings NameSchemaService settings - */ - public function testResolve( - array $schemaIdentifiers, - $nameSchema, - $languageFieldValues, - $fieldTitles, - $settings = [] - ) { - $serviceMock = $this->getPartlyMockedNameSchemaService(['getFieldTitles'], $settings); - - $content = $this->buildTestContentObject(); - $contentType = $this->buildTestContentType(); - - $index = 0; - foreach ($languageFieldValues as $languageCode => $fieldValue) { - $serviceMock->expects( - $this->at($index++) - )->method( - 'getFieldTitles' - )->with( - $schemaIdentifiers, - $contentType, - $content->fields, - $languageCode - )->will( - $this->returnValue($fieldTitles[$languageCode]) - ); - } - - $result = $serviceMock->resolve($nameSchema, $contentType, $content->fields, $content->versionInfo->languageCodes); - - self::assertEquals($languageFieldValues, $result); - } - - /** - * Data provider for the @see testResolve method. - * - * @return array - */ - public function resolveDataProvider() - { - return [ - [ - ['text1'], - '', - [ - 'eng-GB' => 'one', - 'cro-HR' => 'jedan', - ], - [ - 'eng-GB' => ['text1' => 'one'], - 'cro-HR' => ['text1' => 'jedan'], - ], - ], - [ - ['text2'], - '', - [ - 'eng-GB' => 'two', - 'cro-HR' => 'dva', - ], - [ - 'eng-GB' => ['text2' => 'two'], - 'cro-HR' => ['text2' => 'dva'], - ], - ], - [ - ['text1', 'text2'], - 'Hello, and and then goodbye and hello again', - [ - 'eng-GB' => 'Hello, one and two and then goodbye...', - 'cro-HR' => 'Hello, jedan and dva and then goodb...', - ], - [ - 'eng-GB' => ['text1' => 'one', 'text2' => 'two'], - 'cro-HR' => ['text1' => 'jedan', 'text2' => 'dva'], - ], - [ - 'limit' => 38, - 'sequence' => '...', - ], - ], - ]; - } - - /** - * @return \Ibexa\Contracts\Core\Repository\Values\Content\Field[] - */ - protected function getFields() - { - return [ - new Field( - [ - 'languageCode' => 'eng-GB', - 'fieldDefIdentifier' => 'text1', - 'value' => new TextLineValue('one'), - ] - ), - new Field( - [ - 'languageCode' => 'eng-GB', - 'fieldDefIdentifier' => 'text2', - 'value' => new TextLineValue('two'), - ] - ), - new Field( - [ - 'languageCode' => 'eng-GB', - 'fieldDefIdentifier' => 'text3', - 'value' => new TextLineValue(''), - ] - ), - new Field( - [ - 'languageCode' => 'cro-HR', - 'fieldDefIdentifier' => 'text1', - 'value' => new TextLineValue('jedan'), - ] - ), - new Field( - [ - 'languageCode' => 'cro-HR', - 'fieldDefIdentifier' => 'text2', - 'value' => new TextLineValue('dva'), - ] - ), - new Field( - [ - 'languageCode' => 'cro-HR', - 'fieldDefIdentifier' => 'text3', - 'value' => new TextLineValue(''), - ] - ), - ]; - } - - /** - * @return \Ibexa\Core\Repository\Values\ContentType\FieldDefinition[] - */ - protected function getFieldDefinitions() - { - return [ - new FieldDefinition( - [ - 'id' => '1', - 'identifier' => 'text1', - 'fieldTypeIdentifier' => 'ezstring', - ] - ), - new FieldDefinition( - [ - 'id' => '2', - 'identifier' => 'text2', - 'fieldTypeIdentifier' => 'ezstring', - ] - ), - new FieldDefinition( - [ - 'id' => '3', - 'identifier' => 'text3', - 'fieldTypeIdentifier' => 'ezstring', - ] - ), - ]; - } - - /** - * Build Content Object stub for testing purpose. - * - * @return \Ibexa\Contracts\Core\Repository\Values\Content\Content - */ - protected function buildTestContentObject() - { - return new Content( - [ - 'internalFields' => $this->getFields(), - 'versionInfo' => new VersionInfo( - [ - 'languageCodes' => ['eng-GB', 'cro-HR'], - ] - ), - ] - ); - } - - /** - * Build ContentType stub for testing purpose. - * - * @param string $nameSchema - * @param string $urlAliasSchema - * - * @return \Ibexa\Core\Repository\Values\ContentType\ContentType - */ - protected function buildTestContentType($nameSchema = '', $urlAliasSchema = '') - { - return new ContentType( - [ - 'nameSchema' => $nameSchema, - 'urlAliasSchema' => $urlAliasSchema, - 'fieldDefinitions' => $this->getFieldDefinitions(), - ] - ); - } - - /** - * Returns the content service to test with $methods mocked. - * - * Injected Repository comes from {@see getRepositoryMock()} - * - * @param string[] $methods - * @param array $settings - * - * @return \Ibexa\Core\Repository\Helper\NameSchemaService|\PHPUnit\Framework\MockObject\MockObject - */ - protected function getPartlyMockedNameSchemaService(array $methods = null, array $settings = []) - { - return $this->getMockBuilder(NameSchemaService::class) - ->setMethods($methods) - ->setConstructorArgs( - [ - $this->getPersistenceMock()->contentTypeHandler(), - $this->getContentTypeDomainMapperMock(), - $this->getFieldTypeRegistryMock(), - $settings, - ] - ) - ->getMock(); - } -} - -class_alias(NameSchemaTest::class, 'eZ\Publish\Core\Repository\Tests\Service\Mock\NameSchemaTest');