From baee7f74032a13d31a78f75612fc33357a03963a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20S=C5=82omka?= Date: Wed, 2 Aug 2023 23:59:46 +0200 Subject: [PATCH] Added attributes support for generating name schema x Fixed --- .../AbstractNameSchemaEvent.php} | 33 ++---- .../ResolveContentNameSchemaEvent.php | 29 +++++ .../NameSchema/ResolveNameSchemaEvent.php | 47 ++++++++ .../NameSchema/ResolveUrlAliasSchemaEvent.php | 29 +++++ .../EventSubscriber/NameSchemaSubscriber.php | 107 +++++++++++++++-- .../NameSchema/NameSchemaFilter.php | 52 ++++++++ .../NameSchema/NameSchemaService.php | 111 +++++------------- .../Repository/NameSchema/TokenHandler.php | 99 ++++++++++++++++ .../repository/inner/name_schema.yaml | 4 + .../NameSchema/NameSchemaServiceTest.php | 2 +- .../SchemaIdentifierExtractorTest.php | 9 ++ 11 files changed, 405 insertions(+), 117 deletions(-) rename src/contracts/Event/{ResolveUrlAliasSchemaEvent.php => NameSchema/AbstractNameSchemaEvent.php} (58%) create mode 100644 src/contracts/Event/NameSchema/ResolveContentNameSchemaEvent.php create mode 100644 src/contracts/Event/NameSchema/ResolveNameSchemaEvent.php create mode 100644 src/contracts/Event/NameSchema/ResolveUrlAliasSchemaEvent.php create mode 100644 src/lib/Repository/NameSchema/NameSchemaFilter.php create mode 100644 src/lib/Repository/NameSchema/TokenHandler.php diff --git a/src/contracts/Event/ResolveUrlAliasSchemaEvent.php b/src/contracts/Event/NameSchema/AbstractNameSchemaEvent.php similarity index 58% rename from src/contracts/Event/ResolveUrlAliasSchemaEvent.php rename to src/contracts/Event/NameSchema/AbstractNameSchemaEvent.php index 3f75eb3463..861e9ec7bf 100644 --- a/src/contracts/Event/ResolveUrlAliasSchemaEvent.php +++ b/src/contracts/Event/NameSchema/AbstractNameSchemaEvent.php @@ -6,48 +6,37 @@ */ declare(strict_types=1); -namespace Ibexa\Contracts\Core\Event; +namespace Ibexa\Contracts\Core\Event\NameSchema; -use Ibexa\Contracts\Core\Repository\Values\Content\Content; use Symfony\Contracts\EventDispatcher\Event; -final class ResolveUrlAliasSchemaEvent extends Event +abstract class AbstractNameSchemaEvent extends Event { /** @var array */ private array $schemaIdentifiers; - private Content $content; - /** * @var array> */ - private array $names = []; - - public function __construct( - array $schemaIdentifiers, - Content $content - ) { - $this->schemaIdentifiers = $schemaIdentifiers; - $this->content = $content; - } + private array $tokenValues = []; - public function getSchemaIdentifiers(): array + public function __construct(array $schemaIdentifiers) { - return $this->schemaIdentifiers; + $this->schemaIdentifiers = $schemaIdentifiers; } - public function getContent(): Content + public function getTokenValues(): array { - return $this->content; + return $this->tokenValues; } - public function getTokenValues(): array + public function setTokenValues(array $names): void { - return $this->names; + $this->tokenValues = $names; } - public function setTokenValues(array $names): void + public function getSchemaIdentifiers(): array { - $this->names = $names; + return $this->schemaIdentifiers; } } diff --git a/src/contracts/Event/NameSchema/ResolveContentNameSchemaEvent.php b/src/contracts/Event/NameSchema/ResolveContentNameSchemaEvent.php new file mode 100644 index 0000000000..4068cb6271 --- /dev/null +++ b/src/contracts/Event/NameSchema/ResolveContentNameSchemaEvent.php @@ -0,0 +1,29 @@ +content = $content; + } + + public function getContent(): Content + { + return $this->content; + } +} diff --git a/src/contracts/Event/NameSchema/ResolveNameSchemaEvent.php b/src/contracts/Event/NameSchema/ResolveNameSchemaEvent.php new file mode 100644 index 0000000000..4d42f3e9d5 --- /dev/null +++ b/src/contracts/Event/NameSchema/ResolveNameSchemaEvent.php @@ -0,0 +1,47 @@ +contentType = $contentType; + $this->fieldMap = $fieldMap; + $this->languageCodes = $languageCodes; + } + + public function getContentType(): ContentType + { + return $this->contentType; + } + + public function getFieldMap(): array + { + return $this->fieldMap; + } + + public function getLanguageCodes(): array + { + return $this->languageCodes; + } +} diff --git a/src/contracts/Event/NameSchema/ResolveUrlAliasSchemaEvent.php b/src/contracts/Event/NameSchema/ResolveUrlAliasSchemaEvent.php new file mode 100644 index 0000000000..e6f3851686 --- /dev/null +++ b/src/contracts/Event/NameSchema/ResolveUrlAliasSchemaEvent.php @@ -0,0 +1,29 @@ +content = $content; + } + + public function getContent(): Content + { + return $this->content; + } +} diff --git a/src/lib/Repository/EventSubscriber/NameSchemaSubscriber.php b/src/lib/Repository/EventSubscriber/NameSchemaSubscriber.php index 89bbfc8615..192a398a18 100644 --- a/src/lib/Repository/EventSubscriber/NameSchemaSubscriber.php +++ b/src/lib/Repository/EventSubscriber/NameSchemaSubscriber.php @@ -8,7 +8,12 @@ namespace Ibexa\Core\Repository\EventSubscriber; -use Ibexa\Contracts\Core\Event\ResolveUrlAliasSchemaEvent; +use Ibexa\Contracts\Core\Event\NameSchema\AbstractNameSchemaEvent; +use Ibexa\Contracts\Core\Event\NameSchema\ResolveContentNameSchemaEvent; +use Ibexa\Contracts\Core\Event\NameSchema\ResolveNameSchemaEvent; +use Ibexa\Contracts\Core\Event\NameSchema\ResolveUrlAliasSchemaEvent; +use Ibexa\Contracts\Core\Repository\Values\Content\Content; +use Ibexa\Contracts\Core\Repository\Values\ContentType\ContentType; use Ibexa\Core\FieldType\FieldTypeRegistry; use Symfony\Component\EventDispatcher\EventSubscriberInterface; @@ -27,29 +32,109 @@ public function __construct(FieldTypeRegistry $fieldTypeRegistry) public static function getSubscribedEvents(): array { return [ + ResolveNameSchemaEvent::class => [ + ['onResolveNameSchema', -100], + ], + ResolveContentNameSchemaEvent::class => [ + ['onResolveContentNameSchema', -100], + ], 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 + public function onResolveNameSchema(ResolveNameSchemaEvent $event): void { - if (!array_key_exists('field', $event->getSchemaIdentifiers())) { + if (!$this->isValid($event)) { return; } - $content = $event->getContent(); $identifiers = $event->getSchemaIdentifiers()['field']; - $languages = $event->getContent()->getVersionInfo()->getLanguages(); $tokenValues = $event->getTokenValues(); + $fieldMap = $event->getFieldMap(); + + $contentType = $event->getContentType(); + foreach ($event->getLanguageCodes() as $languageCode) { + foreach ($identifiers as $identifier) { + $fieldDefinition = $contentType->getFieldDefinition($identifier); + if (null === $fieldDefinition) { + continue; + } + $persistenceFieldType = $this->fieldTypeRegistry->getFieldType($fieldDefinition->fieldTypeIdentifier); + + $fieldValue = $fieldMap[$identifier][$languageCode] ?? ''; + $fieldValue = $persistenceFieldType->getName( + $fieldValue, + $fieldDefinition, + $languageCode + ); + + $tokenValues[$languageCode][$identifier] = $fieldValue; + } + } + + $event->setTokenValues($tokenValues); + } + public function onResolveContentNameSchema(ResolveContentNameSchemaEvent $event): void + { + if (!$this->isValid($event)) { + return; + } + + $content = $event->getContent(); $contentType = $content->getContentType(); + $tokenValues = $this->processEvent( + $event->getContent()->getVersionInfo()->getLanguages(), + $event->getSchemaIdentifiers()['field'], + $contentType, + $content, + $event->getTokenValues() + ); + + $event->setTokenValues($tokenValues); + } + + public function onResolveUrlAliasSchema(ResolveUrlAliasSchemaEvent $event): void + { + if (!$this->isValid($event)) { + return; + } + + $content = $event->getContent(); + $contentType = $content->getContentType(); + $tokenValues = $this->processEvent( + $event->getContent()->getVersionInfo()->getLanguages(), + $event->getSchemaIdentifiers()['field'], + $contentType, + $content, + $event->getTokenValues() + ); + + $event->setTokenValues($tokenValues); + } + + public function isValid(AbstractNameSchemaEvent $event): bool + { + return array_key_exists('field', $event->getSchemaIdentifiers()); + } + + /** + * @param array $tokens + * @param array $languageCodes + * @param array $attributes + * @param array $tokenValues + * + * @return array + */ + public function processEvent( + $languages, + $identifiers, + ContentType $contentType, + Content $content, + array $tokenValues + ): array { foreach ($languages as $language) { $languageCode = $language->getLanguageCode(); foreach ($identifiers as $identifier) { @@ -70,6 +155,6 @@ public function onResolveUrlAliasSchema(ResolveUrlAliasSchemaEvent $event): void } } - $event->setTokenValues($tokenValues); + return $tokenValues; } } diff --git a/src/lib/Repository/NameSchema/NameSchemaFilter.php b/src/lib/Repository/NameSchema/NameSchemaFilter.php new file mode 100644 index 0000000000..f01040460e --- /dev/null +++ b/src/lib/Repository/NameSchema/NameSchemaFilter.php @@ -0,0 +1,52 @@ +} + */ + public function filterNameSchema(string $nameSchema): array + { + $retNamePattern = $nameSchema; + $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, $retNamePattern); + + // Remove the pattern "(" ")" from the tokens + $group = str_replace(['(', ')'], '', $group); + + $groupLookupTable[$metaToken] = $group; + ++$i; + } + $nameSchema = $retNamePattern; + } + + return [$nameSchema, $groupLookupTable]; + } +} diff --git a/src/lib/Repository/NameSchema/NameSchemaService.php b/src/lib/Repository/NameSchema/NameSchemaService.php index 591642135d..93df504935 100644 --- a/src/lib/Repository/NameSchema/NameSchemaService.php +++ b/src/lib/Repository/NameSchema/NameSchemaService.php @@ -8,7 +8,9 @@ namespace Ibexa\Core\Repository\NameSchema; -use Ibexa\Contracts\Core\Event\ResolveUrlAliasSchemaEvent; +use Ibexa\Contracts\Core\Event\NameSchema\ResolveContentNameSchemaEvent; +use Ibexa\Contracts\Core\Event\NameSchema\ResolveNameSchemaEvent; +use Ibexa\Contracts\Core\Event\NameSchema\ResolveUrlAliasSchemaEvent; use Ibexa\Contracts\Core\Repository\NameSchema\NameSchemaServiceInterface; use Ibexa\Contracts\Core\Repository\NameSchema\SchemaIdentifierExtractorInterface; use Ibexa\Contracts\Core\Repository\Values\Content\Content; @@ -116,65 +118,47 @@ public function resolveNameSchema( 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 - ); - } + $schemaName = $contentType->urlAliasSchema ?: $contentType->nameSchema; + $schemaIdentifiers = $this->schemaIdentifierExtractor->extract($schemaName); - /** - * 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; - } + $event = new ResolveContentNameSchemaEvent($schemaIdentifiers, $content); + $this->eventDispatcher->dispatch($event); - $mergedFieldMap = []; + [$filteredNameSchema, $groupLookupTable] = $this->filterNameSchema($schemaName); + $tokens = $this->extractTokens($filteredNameSchema); - foreach ($content->fields as $fieldIdentifier => $fieldLanguageMap) { - foreach ($languageCodes as $languageCode) { - $mergedFieldMap[$fieldIdentifier][$languageCode] - = $fieldMap[$fieldIdentifier][$languageCode] ?? $fieldLanguageMap[$languageCode]; + $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 $mergedFieldMap; + return $names; } public function resolve(string $nameSchema, ContentType $contentType, array $fieldMap, array $languageCodes): array { + $schemaIdentifiers = $this->schemaIdentifierExtractor->extract($nameSchema); + $event = new ResolveNameSchemaEvent($schemaIdentifiers, $contentType, $fieldMap, $languageCodes); + + $this->eventDispatcher->dispatch($event); + $tokenValues = $event->getTokenValues(); + [$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); + foreach ($tokenValues as $languageCode => $tokenValue) { $name = $filteredNameSchema; - - // Replace tokens with real values foreach ($tokens as $token) { - $string = $this->resolveToken($token, $titles, $groupLookupTable); + $string = $this->resolveToken($token, $tokenValue, $groupLookupTable); $name = str_replace($token, $string, $name); } $name = $this->validateNameLength($name); @@ -185,45 +169,6 @@ public function resolve(string $nameSchema, ContentType $contentType, array $fie 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. * diff --git a/src/lib/Repository/NameSchema/TokenHandler.php b/src/lib/Repository/NameSchema/TokenHandler.php new file mode 100644 index 0000000000..7e5f9e4395 --- /dev/null +++ b/src/lib/Repository/NameSchema/TokenHandler.php @@ -0,0 +1,99 @@ + + * Text more text ==> + * + */ + public 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. + */ + public 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. + */ + public 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 + */ + public function tokenParts(string $token): array + { + return preg_split('/[^\w:]+/', $token, -1, PREG_SPLIT_NO_EMPTY); + } +} diff --git a/src/lib/Resources/settings/repository/inner/name_schema.yaml b/src/lib/Resources/settings/repository/inner/name_schema.yaml index 45f8fcad8e..35cb97d184 100644 --- a/src/lib/Resources/settings/repository/inner/name_schema.yaml +++ b/src/lib/Resources/settings/repository/inner/name_schema.yaml @@ -16,5 +16,9 @@ services: arguments: $settings: '%ibexa.core.repository.name_schema.settings%' + Ibexa\Core\Repository\NameSchema\NameSchemaFilter: ~ + + Ibexa\Core\Repository\NameSchema\TokenHandler: ~ + Ibexa\Contracts\Core\Repository\NameSchema\NameSchemaServiceInterface: alias: 'Ibexa\Core\Repository\NameSchema\NameSchemaService' diff --git a/tests/lib/Repository/NameSchema/NameSchemaServiceTest.php b/tests/lib/Repository/NameSchema/NameSchemaServiceTest.php index a1c6bdfa03..588773e1a8 100644 --- a/tests/lib/Repository/NameSchema/NameSchemaServiceTest.php +++ b/tests/lib/Repository/NameSchema/NameSchemaServiceTest.php @@ -8,7 +8,7 @@ namespace Ibexa\Tests\Core\Repository\NameSchema; -use Ibexa\Contracts\Core\Event\ResolveUrlAliasSchemaEvent; +use Ibexa\Contracts\Core\Event\NameSchema\ResolveUrlAliasSchemaEvent; use Ibexa\Contracts\Core\Repository\Values\Content\Field; use Ibexa\Contracts\Core\Repository\Values\ContentType\FieldDefinitionCollection as APIFieldDefinitionCollection; use Ibexa\Core\FieldType\TextLine\Type as TextLineFieldType; diff --git a/tests/lib/Repository/NameSchema/SchemaIdentifierExtractorTest.php b/tests/lib/Repository/NameSchema/SchemaIdentifierExtractorTest.php index a5bb17e369..02bdffcfa9 100644 --- a/tests/lib/Repository/NameSchema/SchemaIdentifierExtractorTest.php +++ b/tests/lib/Repository/NameSchema/SchemaIdentifierExtractorTest.php @@ -94,6 +94,15 @@ public function getDataForTestExtract(): iterable 'attribute' => ['mouse_type', 'mouse_weight'], ], ]; + + $schemaString = ' )> )>'; + yield $schemaString => [ + $schemaString, + [ + 'field' => ['description'], + 'attribute' => ['mouse_type', 'mouse_weight'], + ], + ]; } protected function setUp(): void