From 78de60d1b91f9fe2bdacb5ed18625ecfa92a7cad Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Fri, 14 Mar 2025 14:15:36 +0100 Subject: [PATCH 1/5] IBX-9631: Implement parser fetching content fields from regular expression --- .../Resources/config/services/utils.yaml | 10 + ...ntentTypeFieldsExpressionDoctrineLexer.php | 74 ++++++++ .../ContentTypeFieldsExpressionParser.php | 173 ++++++++++++++++++ ...entTypeFieldsExpressionParserInterface.php | 19 ++ src/lib/Util/ContentTypeFieldsExtractor.php | 152 +++++++++++++++ .../ContentTypeFieldsExtractorInterface.php | 19 ++ tests/integration/AdminUiIbexaTestKernel.php | 11 ++ .../Util/ContentTypeFieldsExtractorTest.php | 78 ++++++++ .../ContentTypeFieldsExpressionParserTest.php | 133 ++++++++++++++ 9 files changed, 669 insertions(+) create mode 100644 src/lib/Util/ContentTypeFieldsExpressionDoctrineLexer.php create mode 100644 src/lib/Util/ContentTypeFieldsExpressionParser.php create mode 100644 src/lib/Util/ContentTypeFieldsExpressionParserInterface.php create mode 100644 src/lib/Util/ContentTypeFieldsExtractor.php create mode 100644 src/lib/Util/ContentTypeFieldsExtractorInterface.php create mode 100644 tests/integration/Util/ContentTypeFieldsExtractorTest.php create mode 100644 tests/lib/Util/ContentTypeFieldsExpressionParserTest.php diff --git a/src/bundle/Resources/config/services/utils.yaml b/src/bundle/Resources/config/services/utils.yaml index 45f619f827..80b95f6891 100644 --- a/src/bundle/Resources/config/services/utils.yaml +++ b/src/bundle/Resources/config/services/utils.yaml @@ -6,3 +6,13 @@ services: Ibexa\AdminUi\Util\: resource: "../../../../lib/Util" + + Ibexa\AdminUi\Util\ContentTypeFieldsExpressionParserInterface: + alias: Ibexa\AdminUi\Util\ContentTypeFieldsExpressionParser + + Ibexa\AdminUi\Util\ContentTypeFieldsExpressionParser: ~ + + Ibexa\AdminUi\Util\ContentTypeFieldsExtractorInterface: + alias: Ibexa\AdminUi\Util\ContentTypeFieldsExtractor + + Ibexa\AdminUi\Util\ContentTypeFieldsExtractor: ~ diff --git a/src/lib/Util/ContentTypeFieldsExpressionDoctrineLexer.php b/src/lib/Util/ContentTypeFieldsExpressionDoctrineLexer.php new file mode 100644 index 0000000000..da413839a3 --- /dev/null +++ b/src/lib/Util/ContentTypeFieldsExpressionDoctrineLexer.php @@ -0,0 +1,74 @@ + + */ +final class ContentTypeFieldsExpressionDoctrineLexer extends AbstractLexer +{ + public const T_LBRACE = 1; + public const T_RBRACE = 2; + public const T_COMMA = 3; + public const T_SLASH = 4; + public const T_WILDCARD = 5; + public const T_IDENTIFIER = 6; + + /** + * @return list + */ + protected function getCatchablePatterns(): array + { + return [ + '[a-zA-Z_][a-zA-Z0-9_-]*', + '\*', + '[\{\},\/]', + ]; + } + + /** + * @return list + */ + protected function getNonCatchablePatterns(): array + { + return [ + '\s+', + ]; + } + + /** + * @param string $value + */ + protected function getType(&$value): int + { + if ($value === '{') { + return self::T_LBRACE; + } + + if ($value === '}') { + return self::T_RBRACE; + } + + if ($value === ',') { + return self::T_COMMA; + } + + if ($value === '/') { + return self::T_SLASH; + } + + if ($value === '*') { + return self::T_WILDCARD; + } + + return self::T_IDENTIFIER; + } +} diff --git a/src/lib/Util/ContentTypeFieldsExpressionParser.php b/src/lib/Util/ContentTypeFieldsExpressionParser.php new file mode 100644 index 0000000000..5c64baed50 --- /dev/null +++ b/src/lib/Util/ContentTypeFieldsExpressionParser.php @@ -0,0 +1,173 @@ +lexer = new ContentTypeFieldsExpressionDoctrineLexer(); + } + + public function parseExpression(string $expression): array + { + // Content type group can be omitted therefore we need to know how many parts are there + $slashCount = substr_count($expression, '/'); + + $this->lexer->setInput($expression); + $this->lexer->moveNext(); + + $groupTokens = null; // Content type groups are optional + $contentTypeTokens = null; + $fieldTokens = null; + + while ($this->lexer->lookahead !== null) { + $this->lexer->moveNext(); + + if ($slashCount === 2) { + $groupTokens = $this->parseSection(); + $this->expectSlash(); + $contentTypeTokens = $this->parseSection(); + $this->expectSlash(); + $fieldTokens = $this->parseSection(); + } elseif ($slashCount === 1) { + $groupTokens = null; + $contentTypeTokens = $this->parseSection(); + $this->expectSlash(); + $fieldTokens = $this->parseSection(); + } else { + throw new RuntimeException('Invalid expression, expected one or two T_SLASH delimiters.'); + } + } + + $parsedTokens = [ + $groupTokens, + $contentTypeTokens, + $fieldTokens, + ]; + + if (array_filter($parsedTokens) === []) { + throw new RuntimeException('Choosing every possible content type field is not allowed.'); + } + + return $parsedTokens; + } + + /** + * @return non-empty-list|null + */ + private function parseSection(): ?array + { + $items = []; + + if ($this->lexer->token === null) { + throw new RuntimeException('A token inside a section cannot be empty.'); + } + + // Multiple elements between braces + if ($this->lexer->token->isA(ContentTypeFieldsExpressionDoctrineLexer::T_LBRACE)) { + $items[] = $this->getTokenFromInsideBracket(); + + while ($this->lexer->token->isA(ContentTypeFieldsExpressionDoctrineLexer::T_COMMA)) { + $items[] = $this->getTokenFromInsideBracket(); + } + + if (!$this->lexer->token->isA(ContentTypeFieldsExpressionDoctrineLexer::T_RBRACE)) { + throw new RuntimeException('Expected T_RBRACE to close the list.'); + } + + $this->lexer->moveNext(); + } else { + // Otherwise, expect a single identifier or wildcard. + $token = $this->expectIdentifierOrWildcard(); + + if ($token === null) { + return null; + } + + $items[] = $token; + } + + return $items; + } + + private function getTokenFromInsideBracket(): string + { + $this->lexer->moveNext(); + + $token = $this->expectIdentifierOrWildcard(); + if ($token === null) { + throw new RuntimeException('Wildcards cannot be mixed with identifiers inside the expression.'); + } + + return $token; + } + + /** + * @throws \RuntimeException + */ + private function expectSlash(): void + { + if ($this->lexer->token === null) { + throw new RuntimeException( + sprintf( + 'Expected token of type "%s" but got "null"', + ContentTypeFieldsExpressionDoctrineLexer::T_SLASH, + ), + ); + } + + if (!$this->lexer->token->isA(ContentTypeFieldsExpressionDoctrineLexer::T_SLASH)) { + throw new RuntimeException( + sprintf( + 'Expected token of type "%s" but got "%s"', + ContentTypeFieldsExpressionDoctrineLexer::T_SLASH, + $this->lexer->token->type, + ), + ); + } + + $this->lexer->moveNext(); + } + + private function expectIdentifierOrWildcard(): ?string + { + if ($this->lexer->token === null) { + throw new RuntimeException( + sprintf( + 'Expected token of type "%s" but got "null"', + ContentTypeFieldsExpressionDoctrineLexer::T_SLASH, + ), + ); + } + + if (!in_array( + $this->lexer->token->type, + [ + ContentTypeFieldsExpressionDoctrineLexer::T_IDENTIFIER, + ContentTypeFieldsExpressionDoctrineLexer::T_WILDCARD, + ], + true, + )) { + throw new RuntimeException('Expected an identifier or wildcard.'); + } + + $value = $this->lexer->token->isA(ContentTypeFieldsExpressionDoctrineLexer::T_WILDCARD) + ? null + : $this->lexer->token->value; + + $this->lexer->moveNext(); + + return $value; + } +} diff --git a/src/lib/Util/ContentTypeFieldsExpressionParserInterface.php b/src/lib/Util/ContentTypeFieldsExpressionParserInterface.php new file mode 100644 index 0000000000..4a24ad599e --- /dev/null +++ b/src/lib/Util/ContentTypeFieldsExpressionParserInterface.php @@ -0,0 +1,19 @@ +|null, non-empty-list|null, non-empty-list|null} + * + * @throws \RuntimeException + */ + public function parseExpression(string $expression): array; +} diff --git a/src/lib/Util/ContentTypeFieldsExtractor.php b/src/lib/Util/ContentTypeFieldsExtractor.php new file mode 100644 index 0000000000..cc04662a5f --- /dev/null +++ b/src/lib/Util/ContentTypeFieldsExtractor.php @@ -0,0 +1,152 @@ +expressionParser = $expressionParser; + $this->contentTypeService = $contentTypeService; + } + + public function extractFieldsFromExpression(string $expression): array + { + $extractedMetadata = $this->expressionParser->parseExpression($expression); + + $contentTypes = $this->resolveContentTypes($extractedMetadata); + + return $this->mergeFieldIds($extractedMetadata[2], $contentTypes); + } + + /** + * @param array{non-empty-list|null, non-empty-list|null, non-empty-list|null} $extractedMetadata + * + * @return list + */ + private function resolveContentTypes(array $extractedMetadata): array + { + $contentTypeGroupIdentifiers = $extractedMetadata[0]; + $contentTypeIdentifiers = $extractedMetadata[1]; + + // Resolve content type groups first + if ($contentTypeGroupIdentifiers === null) { + $contentTypeGroups = $this->contentTypeService->loadContentTypeGroups(); + } else { + $contentTypeGroups = []; + foreach ($contentTypeGroupIdentifiers as $contentTypeGroupIdentifier) { + $contentTypeGroups[] = $this->contentTypeService->loadContentTypeGroupByIdentifier($contentTypeGroupIdentifier); + } + } + + $contentTypes = []; + + // Then resolve content types + if ($contentTypeIdentifiers === null) { + foreach ($contentTypeGroups as $contentTypeGroup) { + $contentTypesInsideGroup = $this->contentTypeService->loadContentTypes($contentTypeGroup); + foreach ($contentTypesInsideGroup as $contentType) { + $contentTypes[] = $contentType; + } + } + } else { + $contentTypes = array_map( + [$this->contentTypeService, 'loadContentTypeByIdentifier'], + $contentTypeIdentifiers, + ); + + $this->validateContentTypesInsideGroups($contentTypes, $contentTypeGroupIdentifiers); + } + + return $contentTypes; + } + + /** + * @param array|null $fieldIdentifiers + * + * @return array + */ + private function resolveFieldIds(?array $fieldIdentifiers, ContentType $contentType): array + { + $fieldDefinitions = $contentType->getFieldDefinitions(); + + if ($fieldIdentifiers === null) { + return $fieldDefinitions->map( + static fn (FieldDefinition $fieldDefinition): int => $fieldDefinition->getId(), + ); + } + + return $fieldDefinitions + ->filter( + static fn (FieldDefinition $fieldDefinition): bool => in_array($fieldDefinition->getIdentifier(), $fieldIdentifiers, true), + ) + ->map(static fn (FieldDefinition $fieldDefinition): int => $fieldDefinition->getId()); + } + + /** + * @param non-empty-list $contentTypes + * @param list $contentTypeGroupIdentifiers + */ + private function validateContentTypesInsideGroups( + array $contentTypes, + ?array $contentTypeGroupIdentifiers + ): void { + if ($contentTypeGroupIdentifiers === null) { + return; + } + + foreach ($contentTypes as $contentType) { + $groupsIdentifiers = array_map( + static fn (ContentTypeGroup $group): string => $group->identifier, + $contentType->getContentTypeGroups(), + ); + + if (array_intersect($contentTypeGroupIdentifiers, $groupsIdentifiers) === []) { + throw new LogicException( + sprintf( + 'Groups of content type "%s" have no common identifiers with chosen groups: "%s".', + $contentType->getIdentifier(), + implode(', ', $contentTypeGroupIdentifiers), + ), + ); + } + } + } + + /** + * @param list|null $fieldIdentifiers + * @param iterable $contentTypes + * + * @return list + */ + private function mergeFieldIds(?array $fieldIdentifiers, iterable $contentTypes): array + { + $finalFieldIds = []; + foreach ($contentTypes as $contentType) { + $finalFieldIds = array_merge( + $finalFieldIds, + $this->resolveFieldIds($fieldIdentifiers, $contentType), + ); + } + + return $finalFieldIds; + } +} diff --git a/src/lib/Util/ContentTypeFieldsExtractorInterface.php b/src/lib/Util/ContentTypeFieldsExtractorInterface.php new file mode 100644 index 0000000000..663d7bbc92 --- /dev/null +++ b/src/lib/Util/ContentTypeFieldsExtractorInterface.php @@ -0,0 +1,19 @@ + + * + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException + */ + public function extractFieldsFromExpression(string $expression): array; +} diff --git a/tests/integration/AdminUiIbexaTestKernel.php b/tests/integration/AdminUiIbexaTestKernel.php index c7ecfe11a3..9742bdcbe0 100644 --- a/tests/integration/AdminUiIbexaTestKernel.php +++ b/tests/integration/AdminUiIbexaTestKernel.php @@ -10,6 +10,7 @@ use DAMA\DoctrineTestBundle\DAMADoctrineTestBundle; use Hautelook\TemplatedUriBundle\HautelookTemplatedUriBundle; +use Ibexa\AdminUi\Util\ContentTypeFieldsExtractorInterface; use Ibexa\Bundle\AdminUi\IbexaAdminUiBundle; use Ibexa\Bundle\ContentForms\IbexaContentFormsBundle; use Ibexa\Bundle\DesignEngine\IbexaDesignEngineBundle; @@ -19,6 +20,7 @@ use Ibexa\Bundle\Test\Rest\IbexaTestRestBundle; use Ibexa\Bundle\TwigComponents\IbexaTwigComponentsBundle; use Ibexa\Bundle\User\IbexaUserBundle; +use Ibexa\Contracts\Core\Persistence\Content\Type\Handler as ContentTypeHandler; use Ibexa\Contracts\Core\Repository\BookmarkService; use Ibexa\Contracts\Test\Core\IbexaTestKernel; use Ibexa\Rest\Server\Controller\JWT; @@ -63,6 +65,15 @@ protected static function getExposedServicesByClass(): iterable yield from parent::getExposedServicesByClass(); yield BookmarkService::class; + + yield ContentTypeFieldsExtractorInterface::class; + + yield ContentTypeHandler::class; + } + + protected static function getExposedServicesById(): iterable + { + yield from parent::getExposedServicesById(); } public function registerContainerConfiguration(LoaderInterface $loader): void diff --git a/tests/integration/Util/ContentTypeFieldsExtractorTest.php b/tests/integration/Util/ContentTypeFieldsExtractorTest.php new file mode 100644 index 0000000000..419030174e --- /dev/null +++ b/tests/integration/Util/ContentTypeFieldsExtractorTest.php @@ -0,0 +1,78 @@ +contentTypeFieldsExtractor = self::getServiceByClassName(ContentTypeFieldsExtractorInterface::class); + $this->contentTypeHandler = self::getServiceByClassName(ContentTypeHandler::class); + } + + public function testExtractWithContentTypeGroupNames(): void + { + $expression = '{Media,Content}/*/name'; + + $extractedFieldIds = $this->contentTypeFieldsExtractor->extractFieldsFromExpression($expression); + + foreach ($extractedFieldIds as $fieldId) { + $fieldDefinition = $this->contentTypeHandler->getFieldDefinition($fieldId, 0); + + self::assertSame('name', $fieldDefinition->identifier); + } + } + + public function testExtractWithContentTypeNames(): void + { + $expression = '*/user/{first_name,last_name}'; + + $extractedFieldIds = $this->contentTypeFieldsExtractor->extractFieldsFromExpression($expression); + + $firstNameField = $this->contentTypeHandler->getFieldDefinition($extractedFieldIds[0], 0); + $lastNameField = $this->contentTypeHandler->getFieldDefinition($extractedFieldIds[1], 0); + + self::assertSame('first_name', $firstNameField->identifier); + self::assertSame('last_name', $lastNameField->identifier); + } + + public function testExtractWithContentTypeAndGroupNames(): void + { + $expression = 'Users/user/{first_name,last_name}'; + + $extractedFieldIds = $this->contentTypeFieldsExtractor->extractFieldsFromExpression($expression); + + $firstNameField = $this->contentTypeHandler->getFieldDefinition($extractedFieldIds[0], 0); + $lastNameField = $this->contentTypeHandler->getFieldDefinition($extractedFieldIds[1], 0); + + self::assertSame('first_name', $firstNameField->identifier); + self::assertSame('last_name', $lastNameField->identifier); + } + + public function testExtractWithContentTypeAndGroupNamesFailsWithTypesOutsideGroups(): void + { + self::expectException(LogicException::class); + + $expression = 'Content/user/{first_name,last_name}'; + + $this->contentTypeFieldsExtractor->extractFieldsFromExpression($expression); + } +} diff --git a/tests/lib/Util/ContentTypeFieldsExpressionParserTest.php b/tests/lib/Util/ContentTypeFieldsExpressionParserTest.php new file mode 100644 index 0000000000..d2c893308c --- /dev/null +++ b/tests/lib/Util/ContentTypeFieldsExpressionParserTest.php @@ -0,0 +1,133 @@ +contentTypeFieldsExpressionExtractor = new ContentTypeFieldsExpressionParser(); + } + + /** + * @param array{0: non-empty-list|null, 1: non-empty-list|null, 2: non-empty-list|null} $expectedResult + * + * @dataProvider dataProviderForTestParse + */ + public function testParse(string $expression, array $expectedResult): void + { + $result = $this->contentTypeFieldsExpressionExtractor->parseExpression($expression); + + self::assertSame($expectedResult, $result); + } + + /** + * @dataProvider dataProviderForTestParseInvalidExpressions + */ + public function testParseInvalidExpression(string $expression): void + { + $this->expectException(RuntimeException::class); + + $this->contentTypeFieldsExpressionExtractor->parseExpression($expression); + } + + /** + * @return iterable|null, 1: non-empty-list|null, 2: non-empty-list|null}}> + */ + public function dataProviderForTestParse(): iterable + { + yield 'product content type group, every content type, few fields' => [ + 'product/*/{name, description}', + [ + ['product'], + null, + ['name', 'description'], + ], + ]; + + yield 'product content type group, every content type, singular field' => [ + 'product/*/name', + [ + ['product'], + null, + ['name'], + ], + ]; + + yield 'media content type group, file content type, singular field' => [ + 'media/file/name', + [ + ['media'], + ['file'], + ['name'], + ], + ]; + + yield 'media content type group, file content type, few field' => [ + 'media/file/{name,path}', + [ + ['media'], + ['file'], + ['name', 'path'], + ], + ]; + + yield 'file content type, few fields, without group' => [ + 'file/{name, description}', + [ + null, + ['file'], + ['name', 'description'], + ], + ]; + + yield 'file content type, all fields, without group' => [ + 'file/*', + [ + null, + ['file'], + null, + ], + ]; + } + + /** + * @return iterable + */ + public function dataProviderForTestParseInvalidExpressions(): iterable + { + yield 'file content type, without fields' => [ + 'file/', + ]; + + yield 'file content type, without fields, two slashes' => [ + '/file/', + ]; + + yield 'file content type, two fields, starts with slash' => [ + '/file/{field1, field2}', + ]; + + yield 'file content type' => [ + 'file', + ]; + + yield 'file content type, fields being identifier and wildcard' => [ + 'file/{field1, *}', + ]; + } +} From d07abc9b752963dd09a25c60ec15f129006bc712 Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Wed, 30 Jul 2025 13:44:02 +0200 Subject: [PATCH 2/5] IBX-9631: Added method validating if field def id is within an expression --- .../Resources/config/services/services.yaml | 5 ++ ...TypeFieldsByExpressionServiceInterface.php | 26 ++++++++ .../ContentTypeFieldsByExpressionService.php | 61 ++++++++++++++++++ src/lib/Util/ContentTypeFieldsExtractor.php | 7 +++ .../ContentTypeFieldsExtractorInterface.php | 5 ++ tests/integration/AdminUiIbexaTestKernel.php | 3 + ...ntentTypeFieldsByExpressionServiceTest.php | 60 ++++++++++++++++++ .../Util/ContentTypeFieldsExtractorTest.php | 63 +++++++++++++++++++ 8 files changed, 230 insertions(+) create mode 100644 src/contracts/Service/ContentTypeFieldsByExpressionServiceInterface.php create mode 100644 src/lib/Service/ContentTypeFieldsByExpressionService.php create mode 100644 tests/integration/Service/ContentTypeFieldsByExpressionServiceTest.php diff --git a/src/bundle/Resources/config/services/services.yaml b/src/bundle/Resources/config/services/services.yaml index 258d80607e..a29275b503 100644 --- a/src/bundle/Resources/config/services/services.yaml +++ b/src/bundle/Resources/config/services/services.yaml @@ -8,3 +8,8 @@ services: Ibexa\AdminUi\Service\MetaFieldType\MetaFieldDefinitionServiceInterface: '@Ibexa\AdminUi\Service\MetaFieldType\MetaFieldDefinitionService' + + Ibexa\AdminUi\Service\ContentTypeFieldsByExpressionService: ~ + + Ibexa\Contracts\AdminUi\Service\ContentTypeFieldsByExpressionServiceInterface: + '@Ibexa\AdminUi\Service\ContentTypeFieldsByExpressionService' \ No newline at end of file diff --git a/src/contracts/Service/ContentTypeFieldsByExpressionServiceInterface.php b/src/contracts/Service/ContentTypeFieldsByExpressionServiceInterface.php new file mode 100644 index 0000000000..1b16ed9a8f --- /dev/null +++ b/src/contracts/Service/ContentTypeFieldsByExpressionServiceInterface.php @@ -0,0 +1,26 @@ + + * + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException + */ + public function getFieldsFromExpression(string $expression): array; + + /** + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException + */ + public function isFieldIncludedInExpression(FieldDefinition $fieldDefinition, string $expression): bool; +} diff --git a/src/lib/Service/ContentTypeFieldsByExpressionService.php b/src/lib/Service/ContentTypeFieldsByExpressionService.php new file mode 100644 index 0000000000..c02c18b7a5 --- /dev/null +++ b/src/lib/Service/ContentTypeFieldsByExpressionService.php @@ -0,0 +1,61 @@ +fieldsExtractor = $fieldsExtractor; + $this->contentTypeHandler = $contentTypeHandler; + $this->contentTypeDomainMapper = $contentTypeDomainMapper; + } + + public function getFieldsFromExpression(string $expression): array + { + $contentTypeFieldIds = $this->fieldsExtractor->extractFieldsFromExpression($expression); + + $contentTypeFieldDefinitions = []; + foreach ($contentTypeFieldIds as $contentTypeFieldId) { + $persistenceFieldDefinition = $this->contentTypeHandler->getFieldDefinition( + $contentTypeFieldId, + ContentType::STATUS_DEFINED, + ); + $apiFieldDefinition = $this->contentTypeDomainMapper->buildFieldDefinitionDomainObject( + $persistenceFieldDefinition, + $persistenceFieldDefinition->mainLanguageCode, + ); + + $contentTypeFieldDefinitions[] = $apiFieldDefinition; + } + + return $contentTypeFieldDefinitions; + } + + public function isFieldIncludedInExpression(FieldDefinition $fieldDefinition, string $expression): bool + { + return $this->fieldsExtractor->isFieldWithinExpression($fieldDefinition->getId(), $expression); + } +} diff --git a/src/lib/Util/ContentTypeFieldsExtractor.php b/src/lib/Util/ContentTypeFieldsExtractor.php index cc04662a5f..df2007c125 100644 --- a/src/lib/Util/ContentTypeFieldsExtractor.php +++ b/src/lib/Util/ContentTypeFieldsExtractor.php @@ -37,6 +37,13 @@ public function extractFieldsFromExpression(string $expression): array return $this->mergeFieldIds($extractedMetadata[2], $contentTypes); } + public function isFieldWithinExpression(int $fieldDefinitionId, string $expression): bool + { + $fieldsFromExpression = $this->extractFieldsFromExpression($expression); + + return in_array($fieldDefinitionId, $fieldsFromExpression, true); + } + /** * @param array{non-empty-list|null, non-empty-list|null, non-empty-list|null} $extractedMetadata * diff --git a/src/lib/Util/ContentTypeFieldsExtractorInterface.php b/src/lib/Util/ContentTypeFieldsExtractorInterface.php index 663d7bbc92..124d79b645 100644 --- a/src/lib/Util/ContentTypeFieldsExtractorInterface.php +++ b/src/lib/Util/ContentTypeFieldsExtractorInterface.php @@ -16,4 +16,9 @@ interface ContentTypeFieldsExtractorInterface * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException */ public function extractFieldsFromExpression(string $expression): array; + + /** + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException + */ + public function isFieldWithinExpression(int $fieldDefinitionId, string $expression): bool; } diff --git a/tests/integration/AdminUiIbexaTestKernel.php b/tests/integration/AdminUiIbexaTestKernel.php index 9742bdcbe0..bbe2ad0d22 100644 --- a/tests/integration/AdminUiIbexaTestKernel.php +++ b/tests/integration/AdminUiIbexaTestKernel.php @@ -20,6 +20,7 @@ use Ibexa\Bundle\Test\Rest\IbexaTestRestBundle; use Ibexa\Bundle\TwigComponents\IbexaTwigComponentsBundle; use Ibexa\Bundle\User\IbexaUserBundle; +use Ibexa\Contracts\AdminUi\Service\ContentTypeFieldsByExpressionServiceInterface; use Ibexa\Contracts\Core\Persistence\Content\Type\Handler as ContentTypeHandler; use Ibexa\Contracts\Core\Repository\BookmarkService; use Ibexa\Contracts\Test\Core\IbexaTestKernel; @@ -69,6 +70,8 @@ protected static function getExposedServicesByClass(): iterable yield ContentTypeFieldsExtractorInterface::class; yield ContentTypeHandler::class; + + yield ContentTypeFieldsByExpressionServiceInterface::class; } protected static function getExposedServicesById(): iterable diff --git a/tests/integration/Service/ContentTypeFieldsByExpressionServiceTest.php b/tests/integration/Service/ContentTypeFieldsByExpressionServiceTest.php new file mode 100644 index 0000000000..16eaa3daff --- /dev/null +++ b/tests/integration/Service/ContentTypeFieldsByExpressionServiceTest.php @@ -0,0 +1,60 @@ +fieldsFromExpressionService = self::getServiceByClassName(ContentTypeFieldsByExpressionServiceInterface::class); + $this->contentTypeService = self::getServiceByClassName(ContentTypeService::class); + } + + public function testExtractWithContentTypeGroupNames(): void + { + $expression = '{Content}/folder/name'; + + $extractedFieldDefinitions = $this->fieldsFromExpressionService->getFieldsFromExpression($expression); + + self::assertCount(1, $extractedFieldDefinitions); + $fieldDefinition = $extractedFieldDefinitions[0]; + self::assertSame('name', $fieldDefinition->identifier); + self::assertSame('ezstring', $fieldDefinition->fieldTypeIdentifier); + } + + public function testFieldIdWithinExpression(): void + { + $expression = '{Content}/folder/name'; + + $contentType = $this->contentTypeService->loadContentTypeByIdentifier('folder'); + $fieldDefinitions = $contentType->getFieldDefinitions(); + $nameFieldDefinition = $fieldDefinitions->filter( + static fn (FieldDefinition $fieldDefinition): bool => $fieldDefinition->getIdentifier() === 'name' + )->first(); + + $result = $this->fieldsFromExpressionService->isFieldIncludedInExpression( + $nameFieldDefinition, + $expression, + ); + + self::assertTrue($result); + } +} diff --git a/tests/integration/Util/ContentTypeFieldsExtractorTest.php b/tests/integration/Util/ContentTypeFieldsExtractorTest.php index 419030174e..c12429fed3 100644 --- a/tests/integration/Util/ContentTypeFieldsExtractorTest.php +++ b/tests/integration/Util/ContentTypeFieldsExtractorTest.php @@ -10,6 +10,8 @@ use Ibexa\AdminUi\Util\ContentTypeFieldsExtractorInterface; use Ibexa\Contracts\Core\Persistence\Content\Type\Handler as ContentTypeHandler; +use Ibexa\Contracts\Core\Repository\ContentTypeService; +use Ibexa\Contracts\Core\Repository\Values\ContentType\FieldDefinition; use Ibexa\Contracts\Core\Test\IbexaKernelTestCase; use LogicException; @@ -19,6 +21,8 @@ final class ContentTypeFieldsExtractorTest extends IbexaKernelTestCase private ContentTypeHandler $contentTypeHandler; + private ContentTypeService $contentTypeService; + protected function setUp(): void { self::bootKernel(); @@ -26,6 +30,7 @@ protected function setUp(): void $this->contentTypeFieldsExtractor = self::getServiceByClassName(ContentTypeFieldsExtractorInterface::class); $this->contentTypeHandler = self::getServiceByClassName(ContentTypeHandler::class); + $this->contentTypeService = self::getServiceByClassName(ContentTypeService::class); } public function testExtractWithContentTypeGroupNames(): void @@ -75,4 +80,62 @@ public function testExtractWithContentTypeAndGroupNamesFailsWithTypesOutsideGrou $this->contentTypeFieldsExtractor->extractFieldsFromExpression($expression); } + + /** + * @dataProvider dataProviderForTestFieldIdWithinExpression + */ + public function testFieldIdWithinExpression(string $expression): void + { + $contentType = $this->contentTypeService->loadContentTypeByIdentifier('folder'); + $fieldDefinitions = $contentType->getFieldDefinitions(); + $nameFieldDefinition = $fieldDefinitions->filter( + static fn (FieldDefinition $fieldDefinition): bool => $fieldDefinition->getIdentifier() === 'name' + )->first(); + + $result = $this->contentTypeFieldsExtractor->isFieldWithinExpression($nameFieldDefinition->getId(), $expression); + + self::assertTrue($result); + } + + /** + * @dataProvider dataProviderForTestFieldIdNotWithinExpression + */ + public function testFieldIdNotWithinExpression(string $expression): void + { + $contentType = $this->contentTypeService->loadContentTypeByIdentifier('folder'); + $fieldDefinitions = $contentType->getFieldDefinitions(); + $nameFieldDefinition = $fieldDefinitions->filter( + static fn (FieldDefinition $fieldDefinition): bool => $fieldDefinition->getIdentifier() === 'name' + )->first(); + + $result = $this->contentTypeFieldsExtractor->isFieldWithinExpression($nameFieldDefinition->getId(), $expression); + + self::assertFalse($result); + } + + /** + * @return iterable> + */ + public function dataProviderForTestFieldIdWithinExpression(): iterable + { + yield '{Media,Content}/*/name' => ['{Media,Content}/*/name']; + + yield '*/folder/name' => ['*/folder/name']; + + yield '*/*/name' => ['*/*/name']; + + yield '*/folder/*' => ['*/folder/*']; + } + + /** + * @return iterable> + */ + public function dataProviderForTestFieldIdNotWithinExpression(): iterable + { + yield '{Users}/*/name' => ['{Users}/*/name']; + + yield '*/article/name' => ['*/article/name']; + + yield '*/user/*' => ['*/user/*']; + } } From 3d9d7f5c044e81f0de88f3c2cc0f0a53ef6e7caf Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Thu, 31 Jul 2025 13:27:20 +0200 Subject: [PATCH 3/5] IBX-9631: Applied review remarks --- .../Resources/config/services/services.yaml | 2 +- .../FieldTypeExpressionParserException.php | 15 +++++++++++++ .../ContentTypeFieldsExpressionParser.php | 22 +++++++++---------- src/lib/Util/ContentTypeFieldsExtractor.php | 4 ++-- 4 files changed, 29 insertions(+), 14 deletions(-) create mode 100644 src/lib/Exception/FieldTypeExpressionParserException.php diff --git a/src/bundle/Resources/config/services/services.yaml b/src/bundle/Resources/config/services/services.yaml index a29275b503..d135e785b5 100644 --- a/src/bundle/Resources/config/services/services.yaml +++ b/src/bundle/Resources/config/services/services.yaml @@ -12,4 +12,4 @@ services: Ibexa\AdminUi\Service\ContentTypeFieldsByExpressionService: ~ Ibexa\Contracts\AdminUi\Service\ContentTypeFieldsByExpressionServiceInterface: - '@Ibexa\AdminUi\Service\ContentTypeFieldsByExpressionService' \ No newline at end of file + '@Ibexa\AdminUi\Service\ContentTypeFieldsByExpressionService' diff --git a/src/lib/Exception/FieldTypeExpressionParserException.php b/src/lib/Exception/FieldTypeExpressionParserException.php new file mode 100644 index 0000000000..0686621f3e --- /dev/null +++ b/src/lib/Exception/FieldTypeExpressionParserException.php @@ -0,0 +1,15 @@ +expectSlash(); $fieldTokens = $this->parseSection(); } else { - throw new RuntimeException('Invalid expression, expected one or two T_SLASH delimiters.'); + throw new FieldTypeExpressionParserException('Invalid expression, expected one or two T_SLASH delimiters.'); } } @@ -57,7 +57,7 @@ public function parseExpression(string $expression): array ]; if (array_filter($parsedTokens) === []) { - throw new RuntimeException('Choosing every possible content type field is not allowed.'); + throw new FieldTypeExpressionParserException('Choosing every possible content type field is not allowed.'); } return $parsedTokens; @@ -71,7 +71,7 @@ private function parseSection(): ?array $items = []; if ($this->lexer->token === null) { - throw new RuntimeException('A token inside a section cannot be empty.'); + throw new FieldTypeExpressionParserException('A token inside a section cannot be empty.'); } // Multiple elements between braces @@ -83,7 +83,7 @@ private function parseSection(): ?array } if (!$this->lexer->token->isA(ContentTypeFieldsExpressionDoctrineLexer::T_RBRACE)) { - throw new RuntimeException('Expected T_RBRACE to close the list.'); + throw new FieldTypeExpressionParserException('Expected T_RBRACE to close the list.'); } $this->lexer->moveNext(); @@ -107,19 +107,19 @@ private function getTokenFromInsideBracket(): string $token = $this->expectIdentifierOrWildcard(); if ($token === null) { - throw new RuntimeException('Wildcards cannot be mixed with identifiers inside the expression.'); + throw new FieldTypeExpressionParserException('Wildcards cannot be mixed with identifiers inside the expression.'); } return $token; } /** - * @throws \RuntimeException + * @throws \Ibexa\AdminUi\Exception\FieldTypeExpressionParserException */ private function expectSlash(): void { if ($this->lexer->token === null) { - throw new RuntimeException( + throw new FieldTypeExpressionParserException( sprintf( 'Expected token of type "%s" but got "null"', ContentTypeFieldsExpressionDoctrineLexer::T_SLASH, @@ -128,7 +128,7 @@ private function expectSlash(): void } if (!$this->lexer->token->isA(ContentTypeFieldsExpressionDoctrineLexer::T_SLASH)) { - throw new RuntimeException( + throw new FieldTypeExpressionParserException( sprintf( 'Expected token of type "%s" but got "%s"', ContentTypeFieldsExpressionDoctrineLexer::T_SLASH, @@ -143,7 +143,7 @@ private function expectSlash(): void private function expectIdentifierOrWildcard(): ?string { if ($this->lexer->token === null) { - throw new RuntimeException( + throw new FieldTypeExpressionParserException( sprintf( 'Expected token of type "%s" but got "null"', ContentTypeFieldsExpressionDoctrineLexer::T_SLASH, @@ -159,7 +159,7 @@ private function expectIdentifierOrWildcard(): ?string ], true, )) { - throw new RuntimeException('Expected an identifier or wildcard.'); + throw new FieldTypeExpressionParserException('Expected an identifier or wildcard.'); } $value = $this->lexer->token->isA(ContentTypeFieldsExpressionDoctrineLexer::T_WILDCARD) diff --git a/src/lib/Util/ContentTypeFieldsExtractor.php b/src/lib/Util/ContentTypeFieldsExtractor.php index df2007c125..0c874d1d5d 100644 --- a/src/lib/Util/ContentTypeFieldsExtractor.php +++ b/src/lib/Util/ContentTypeFieldsExtractor.php @@ -47,7 +47,7 @@ public function isFieldWithinExpression(int $fieldDefinitionId, string $expressi /** * @param array{non-empty-list|null, non-empty-list|null, non-empty-list|null} $extractedMetadata * - * @return list + * @return list<\Ibexa\Contracts\Core\Repository\Values\ContentType\ContentType> */ private function resolveContentTypes(array $extractedMetadata): array { @@ -109,7 +109,7 @@ private function resolveFieldIds(?array $fieldIdentifiers, ContentType $contentT } /** - * @param non-empty-list $contentTypes + * @param non-empty-list<\Ibexa\Contracts\Core\Repository\Values\ContentType\ContentType> $contentTypes * @param list $contentTypeGroupIdentifiers */ private function validateContentTypesInsideGroups( From b5b0c16f371fb0c4ab6dd9a73fd2c018d1548d33 Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Thu, 31 Jul 2025 15:23:14 +0200 Subject: [PATCH 4/5] IBX-9631: Namespaces --- src/bundle/Resources/config/services/services.yaml | 6 +++--- .../ContentTypeFieldsByExpressionServiceInterface.php | 2 +- .../ContentTypeFieldsByExpressionService.php | 4 ++-- tests/integration/AdminUiIbexaTestKernel.php | 2 +- .../Service/ContentTypeFieldsByExpressionServiceTest.php | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) rename src/contracts/{Service => ContentType}/ContentTypeFieldsByExpressionServiceInterface.php (94%) rename src/lib/{Service => ContentType}/ContentTypeFieldsByExpressionService.php (94%) diff --git a/src/bundle/Resources/config/services/services.yaml b/src/bundle/Resources/config/services/services.yaml index d135e785b5..048f2476ce 100644 --- a/src/bundle/Resources/config/services/services.yaml +++ b/src/bundle/Resources/config/services/services.yaml @@ -9,7 +9,7 @@ services: Ibexa\AdminUi\Service\MetaFieldType\MetaFieldDefinitionServiceInterface: '@Ibexa\AdminUi\Service\MetaFieldType\MetaFieldDefinitionService' - Ibexa\AdminUi\Service\ContentTypeFieldsByExpressionService: ~ + Ibexa\AdminUi\ContentType\ContentTypeFieldsByExpressionService: ~ - Ibexa\Contracts\AdminUi\Service\ContentTypeFieldsByExpressionServiceInterface: - '@Ibexa\AdminUi\Service\ContentTypeFieldsByExpressionService' + Ibexa\Contracts\AdminUi\ContentType\ContentTypeFieldsByExpressionServiceInterface: + '@Ibexa\AdminUi\ContentType\ContentTypeFieldsByExpressionService' diff --git a/src/contracts/Service/ContentTypeFieldsByExpressionServiceInterface.php b/src/contracts/ContentType/ContentTypeFieldsByExpressionServiceInterface.php similarity index 94% rename from src/contracts/Service/ContentTypeFieldsByExpressionServiceInterface.php rename to src/contracts/ContentType/ContentTypeFieldsByExpressionServiceInterface.php index 1b16ed9a8f..fdb52b8fc4 100644 --- a/src/contracts/Service/ContentTypeFieldsByExpressionServiceInterface.php +++ b/src/contracts/ContentType/ContentTypeFieldsByExpressionServiceInterface.php @@ -6,7 +6,7 @@ */ declare(strict_types=1); -namespace Ibexa\Contracts\AdminUi\Service; +namespace Ibexa\Contracts\AdminUi\ContentType; use Ibexa\Contracts\Core\Repository\Values\ContentType\FieldDefinition; diff --git a/src/lib/Service/ContentTypeFieldsByExpressionService.php b/src/lib/ContentType/ContentTypeFieldsByExpressionService.php similarity index 94% rename from src/lib/Service/ContentTypeFieldsByExpressionService.php rename to src/lib/ContentType/ContentTypeFieldsByExpressionService.php index c02c18b7a5..69e5044ece 100644 --- a/src/lib/Service/ContentTypeFieldsByExpressionService.php +++ b/src/lib/ContentType/ContentTypeFieldsByExpressionService.php @@ -6,10 +6,10 @@ */ declare(strict_types=1); -namespace Ibexa\AdminUi\Service; +namespace Ibexa\AdminUi\ContentType; use Ibexa\AdminUi\Util\ContentTypeFieldsExtractorInterface; -use Ibexa\Contracts\AdminUi\Service\ContentTypeFieldsByExpressionServiceInterface; +use Ibexa\Contracts\AdminUi\ContentType\ContentTypeFieldsByExpressionServiceInterface; use Ibexa\Contracts\Core\Persistence\Content\Type\Handler as ContentTypeHandler; use Ibexa\Contracts\Core\Repository\Values\ContentType\ContentType; use Ibexa\Contracts\Core\Repository\Values\ContentType\FieldDefinition; diff --git a/tests/integration/AdminUiIbexaTestKernel.php b/tests/integration/AdminUiIbexaTestKernel.php index bbe2ad0d22..6b4c8afa37 100644 --- a/tests/integration/AdminUiIbexaTestKernel.php +++ b/tests/integration/AdminUiIbexaTestKernel.php @@ -20,7 +20,7 @@ use Ibexa\Bundle\Test\Rest\IbexaTestRestBundle; use Ibexa\Bundle\TwigComponents\IbexaTwigComponentsBundle; use Ibexa\Bundle\User\IbexaUserBundle; -use Ibexa\Contracts\AdminUi\Service\ContentTypeFieldsByExpressionServiceInterface; +use Ibexa\Contracts\AdminUi\ContentType\ContentTypeFieldsByExpressionServiceInterface; use Ibexa\Contracts\Core\Persistence\Content\Type\Handler as ContentTypeHandler; use Ibexa\Contracts\Core\Repository\BookmarkService; use Ibexa\Contracts\Test\Core\IbexaTestKernel; diff --git a/tests/integration/Service/ContentTypeFieldsByExpressionServiceTest.php b/tests/integration/Service/ContentTypeFieldsByExpressionServiceTest.php index 16eaa3daff..52865d6bc0 100644 --- a/tests/integration/Service/ContentTypeFieldsByExpressionServiceTest.php +++ b/tests/integration/Service/ContentTypeFieldsByExpressionServiceTest.php @@ -8,7 +8,7 @@ namespace Ibexa\Tests\Integration\AdminUi\Service; -use Ibexa\Contracts\AdminUi\Service\ContentTypeFieldsByExpressionServiceInterface; +use Ibexa\Contracts\AdminUi\ContentType\ContentTypeFieldsByExpressionServiceInterface; use Ibexa\Contracts\Core\Repository\ContentTypeService; use Ibexa\Contracts\Core\Repository\Values\ContentType\FieldDefinition; use Ibexa\Contracts\Core\Test\IbexaKernelTestCase; From 3cf3ce8fe435c7caf9f87f177743e90c7a7dda92 Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Fri, 1 Aug 2025 00:28:32 +0200 Subject: [PATCH 5/5] IBX-9631: Used a better structure for parsed metadata --- .../ContentTypeFieldsExpressionParser.php | 12 ++-- ...entTypeFieldsExpressionParserInterface.php | 4 +- src/lib/Util/ContentTypeFieldsExtractor.php | 10 ++- .../Util/ContentTypeFieldsParsedStructure.php | 65 +++++++++++++++++++ .../ContentTypeFieldsExpressionParserTest.php | 35 +++++----- 5 files changed, 94 insertions(+), 32 deletions(-) create mode 100644 src/lib/Util/ContentTypeFieldsParsedStructure.php diff --git a/src/lib/Util/ContentTypeFieldsExpressionParser.php b/src/lib/Util/ContentTypeFieldsExpressionParser.php index c101e89e5b..9387e2e177 100644 --- a/src/lib/Util/ContentTypeFieldsExpressionParser.php +++ b/src/lib/Util/ContentTypeFieldsExpressionParser.php @@ -19,9 +19,9 @@ public function __construct() $this->lexer = new ContentTypeFieldsExpressionDoctrineLexer(); } - public function parseExpression(string $expression): array + public function parseExpression(string $expression): ContentTypeFieldsParsedStructure { - // Content type group can be omitted therefore we need to know how many parts are there + // Content type group can be omitted, therefore we need to know how many parts are there $slashCount = substr_count($expression, '/'); $this->lexer->setInput($expression); @@ -50,17 +50,17 @@ public function parseExpression(string $expression): array } } - $parsedTokens = [ + $structure = new ContentTypeFieldsParsedStructure( $groupTokens, $contentTypeTokens, $fieldTokens, - ]; + ); - if (array_filter($parsedTokens) === []) { + if ($structure->isAllChosen()) { throw new FieldTypeExpressionParserException('Choosing every possible content type field is not allowed.'); } - return $parsedTokens; + return $structure; } /** diff --git a/src/lib/Util/ContentTypeFieldsExpressionParserInterface.php b/src/lib/Util/ContentTypeFieldsExpressionParserInterface.php index 4a24ad599e..389a6dc58c 100644 --- a/src/lib/Util/ContentTypeFieldsExpressionParserInterface.php +++ b/src/lib/Util/ContentTypeFieldsExpressionParserInterface.php @@ -11,9 +11,7 @@ interface ContentTypeFieldsExpressionParserInterface { /** - * @return array{non-empty-list|null, non-empty-list|null, non-empty-list|null} - * * @throws \RuntimeException */ - public function parseExpression(string $expression): array; + public function parseExpression(string $expression): ContentTypeFieldsParsedStructure; } diff --git a/src/lib/Util/ContentTypeFieldsExtractor.php b/src/lib/Util/ContentTypeFieldsExtractor.php index 0c874d1d5d..7c8daf278a 100644 --- a/src/lib/Util/ContentTypeFieldsExtractor.php +++ b/src/lib/Util/ContentTypeFieldsExtractor.php @@ -34,7 +34,7 @@ public function extractFieldsFromExpression(string $expression): array $contentTypes = $this->resolveContentTypes($extractedMetadata); - return $this->mergeFieldIds($extractedMetadata[2], $contentTypes); + return $this->mergeFieldIds($extractedMetadata->getFields(), $contentTypes); } public function isFieldWithinExpression(int $fieldDefinitionId, string $expression): bool @@ -45,14 +45,12 @@ public function isFieldWithinExpression(int $fieldDefinitionId, string $expressi } /** - * @param array{non-empty-list|null, non-empty-list|null, non-empty-list|null} $extractedMetadata - * * @return list<\Ibexa\Contracts\Core\Repository\Values\ContentType\ContentType> */ - private function resolveContentTypes(array $extractedMetadata): array + private function resolveContentTypes(ContentTypeFieldsParsedStructure $extractedMetadata): array { - $contentTypeGroupIdentifiers = $extractedMetadata[0]; - $contentTypeIdentifiers = $extractedMetadata[1]; + $contentTypeGroupIdentifiers = $extractedMetadata->getGroups(); + $contentTypeIdentifiers = $extractedMetadata->getContentTypes(); // Resolve content type groups first if ($contentTypeGroupIdentifiers === null) { diff --git a/src/lib/Util/ContentTypeFieldsParsedStructure.php b/src/lib/Util/ContentTypeFieldsParsedStructure.php new file mode 100644 index 0000000000..02d1b90cb8 --- /dev/null +++ b/src/lib/Util/ContentTypeFieldsParsedStructure.php @@ -0,0 +1,65 @@ +|null */ + private ?array $groups; + + /** @var non-empty-list|null */ + private ?array $contentTypes; + + /** @var non-empty-list|null */ + private ?array $fields; + + /** + * @param non-empty-list|null $groups + * @param non-empty-list|null $contentTypes + * @param non-empty-list|null $fields + */ + public function __construct( + ?array $groups, + ?array $contentTypes, + ?array $fields + ) { + $this->groups = $groups; + $this->contentTypes = $contentTypes; + $this->fields = $fields; + } + + /** + * @return non-empty-list|null + */ + public function getGroups(): ?array + { + return $this->groups; + } + + /** + * @return non-empty-list|null + */ + public function getContentTypes(): ?array + { + return $this->contentTypes; + } + + /** + * @return non-empty-list|null + */ + public function getFields(): ?array + { + return $this->fields; + } + + public function isAllChosen(): bool + { + return $this->groups === null && $this->contentTypes === null && $this->fields === null; + } +} diff --git a/tests/lib/Util/ContentTypeFieldsExpressionParserTest.php b/tests/lib/Util/ContentTypeFieldsExpressionParserTest.php index d2c893308c..95fb612b52 100644 --- a/tests/lib/Util/ContentTypeFieldsExpressionParserTest.php +++ b/tests/lib/Util/ContentTypeFieldsExpressionParserTest.php @@ -9,6 +9,7 @@ namespace Ibexa\Tests\AdminUi\Util; use Ibexa\AdminUi\Util\ContentTypeFieldsExpressionParser; +use Ibexa\AdminUi\Util\ContentTypeFieldsParsedStructure; use PHPUnit\Framework\TestCase; use RuntimeException; @@ -24,15 +25,15 @@ protected function setUp(): void } /** - * @param array{0: non-empty-list|null, 1: non-empty-list|null, 2: non-empty-list|null} $expectedResult - * * @dataProvider dataProviderForTestParse */ - public function testParse(string $expression, array $expectedResult): void + public function testParse(string $expression, ContentTypeFieldsParsedStructure $expectedResult): void { $result = $this->contentTypeFieldsExpressionExtractor->parseExpression($expression); - self::assertSame($expectedResult, $result); + self::assertSame($expectedResult->getGroups(), $result->getGroups()); + self::assertSame($expectedResult->getContentTypes(), $result->getContentTypes()); + self::assertSame($expectedResult->getFields(), $result->getFields()); } /** @@ -46,62 +47,62 @@ public function testParseInvalidExpression(string $expression): void } /** - * @return iterable|null, 1: non-empty-list|null, 2: non-empty-list|null}}> + * @return iterable */ public function dataProviderForTestParse(): iterable { yield 'product content type group, every content type, few fields' => [ 'product/*/{name, description}', - [ + new ContentTypeFieldsParsedStructure( ['product'], null, ['name', 'description'], - ], + ), ]; yield 'product content type group, every content type, singular field' => [ 'product/*/name', - [ + new ContentTypeFieldsParsedStructure( ['product'], null, ['name'], - ], + ), ]; yield 'media content type group, file content type, singular field' => [ 'media/file/name', - [ + new ContentTypeFieldsParsedStructure( ['media'], ['file'], ['name'], - ], + ), ]; yield 'media content type group, file content type, few field' => [ 'media/file/{name,path}', - [ + new ContentTypeFieldsParsedStructure( ['media'], ['file'], ['name', 'path'], - ], + ), ]; yield 'file content type, few fields, without group' => [ 'file/{name, description}', - [ + new ContentTypeFieldsParsedStructure( null, ['file'], ['name', 'description'], - ], + ), ]; yield 'file content type, all fields, without group' => [ 'file/*', - [ + new ContentTypeFieldsParsedStructure( null, ['file'], null, - ], + ), ]; }