Skip to content

Commit de085b4

Browse files
authored
IBX-9840: Implemented service fetching content fields from expression and validating if field is within expression (#1500)
* IBX-9631: Implement parser fetching content fields from regular expression * IBX-9631: Added method validating if field def id is within an expression * IBX-9631: Applied review remarks * IBX-9631: Namespaces * IBX-9631: Used a better structure for parsed metadata * Review remarks
1 parent 2016a69 commit de085b4

15 files changed

+1001
-0
lines changed

src/bundle/Resources/config/services/services.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,8 @@ services:
88

99
Ibexa\AdminUi\Service\MetaFieldType\MetaFieldDefinitionServiceInterface:
1010
'@Ibexa\AdminUi\Service\MetaFieldType\MetaFieldDefinitionService'
11+
12+
Ibexa\AdminUi\ContentType\ContentTypeFieldsByExpressionService: ~
13+
14+
Ibexa\Contracts\AdminUi\ContentType\ContentTypeFieldsByExpressionServiceInterface:
15+
'@Ibexa\AdminUi\ContentType\ContentTypeFieldsByExpressionService'

src/bundle/Resources/config/services/utils.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,13 @@ services:
66

77
Ibexa\AdminUi\Util\:
88
resource: "../../../../lib/Util"
9+
10+
Ibexa\AdminUi\Util\ContentTypeFieldsExpressionParserInterface:
11+
alias: Ibexa\AdminUi\Util\ContentTypeFieldsExpressionParser
12+
13+
Ibexa\AdminUi\Util\ContentTypeFieldsExpressionParser: ~
14+
15+
Ibexa\AdminUi\Util\ContentTypeFieldsExtractorInterface:
16+
alias: Ibexa\AdminUi\Util\ContentTypeFieldsExtractor
17+
18+
Ibexa\AdminUi\Util\ContentTypeFieldsExtractor: ~
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
/**
4+
* @copyright Copyright (C) Ibexa AS. All rights reserved.
5+
* @license For full copyright and license information view LICENSE file distributed with this source code.
6+
*/
7+
declare(strict_types=1);
8+
9+
namespace Ibexa\Contracts\AdminUi\ContentType;
10+
11+
use Ibexa\Contracts\Core\Repository\Values\ContentType\FieldDefinition;
12+
13+
interface ContentTypeFieldsByExpressionServiceInterface
14+
{
15+
/**
16+
* @return list<\Ibexa\Contracts\Core\Repository\Values\ContentType\FieldDefinition>
17+
*
18+
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException
19+
*/
20+
public function getFieldsFromExpression(string $expression): array;
21+
22+
/**
23+
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException
24+
*/
25+
public function isFieldIncludedInExpression(FieldDefinition $fieldDefinition, string $expression): bool;
26+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
/**
4+
* @copyright Copyright (C) Ibexa AS. All rights reserved.
5+
* @license For full copyright and license information view LICENSE file distributed with this source code.
6+
*/
7+
declare(strict_types=1);
8+
9+
namespace Ibexa\AdminUi\ContentType;
10+
11+
use Ibexa\AdminUi\Util\ContentTypeFieldsExtractorInterface;
12+
use Ibexa\Contracts\AdminUi\ContentType\ContentTypeFieldsByExpressionServiceInterface;
13+
use Ibexa\Contracts\Core\Persistence\Content\Type\Handler as ContentTypeHandler;
14+
use Ibexa\Contracts\Core\Repository\Values\ContentType\ContentType;
15+
use Ibexa\Contracts\Core\Repository\Values\ContentType\FieldDefinition;
16+
use Ibexa\Core\Repository\Mapper\ContentTypeDomainMapper;
17+
18+
final class ContentTypeFieldsByExpressionService implements ContentTypeFieldsByExpressionServiceInterface
19+
{
20+
private ContentTypeFieldsExtractorInterface $fieldsExtractor;
21+
22+
private ContentTypeHandler $contentTypeHandler;
23+
24+
private ContentTypeDomainMapper $contentTypeDomainMapper;
25+
26+
public function __construct(
27+
ContentTypeFieldsExtractorInterface $fieldsExtractor,
28+
ContentTypeHandler $contentTypeHandler,
29+
ContentTypeDomainMapper $contentTypeDomainMapper
30+
) {
31+
$this->fieldsExtractor = $fieldsExtractor;
32+
$this->contentTypeHandler = $contentTypeHandler;
33+
$this->contentTypeDomainMapper = $contentTypeDomainMapper;
34+
}
35+
36+
public function getFieldsFromExpression(string $expression): array
37+
{
38+
$contentTypeFieldIds = $this->fieldsExtractor->extractFieldsFromExpression($expression);
39+
40+
$contentTypeFieldDefinitions = [];
41+
foreach ($contentTypeFieldIds as $contentTypeFieldId) {
42+
$persistenceFieldDefinition = $this->contentTypeHandler->getFieldDefinition(
43+
$contentTypeFieldId,
44+
ContentType::STATUS_DEFINED,
45+
);
46+
$apiFieldDefinition = $this->contentTypeDomainMapper->buildFieldDefinitionDomainObject(
47+
$persistenceFieldDefinition,
48+
$persistenceFieldDefinition->mainLanguageCode,
49+
);
50+
51+
$contentTypeFieldDefinitions[] = $apiFieldDefinition;
52+
}
53+
54+
return $contentTypeFieldDefinitions;
55+
}
56+
57+
public function isFieldIncludedInExpression(FieldDefinition $fieldDefinition, string $expression): bool
58+
{
59+
return $this->fieldsExtractor->isFieldWithinExpression($fieldDefinition->getId(), $expression);
60+
}
61+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
/**
4+
* @copyright Copyright (C) Ibexa AS. All rights reserved.
5+
* @license For full copyright and license information view LICENSE file distributed with this source code.
6+
*/
7+
declare(strict_types=1);
8+
9+
namespace Ibexa\AdminUi\Exception;
10+
11+
use RuntimeException;
12+
13+
final class FieldTypeExpressionParserException extends RuntimeException
14+
{
15+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
/**
4+
* @copyright Copyright (C) Ibexa AS. All rights reserved.
5+
* @license For full copyright and license information view LICENSE file distributed with this source code.
6+
*/
7+
declare(strict_types=1);
8+
9+
namespace Ibexa\AdminUi\Util;
10+
11+
use Doctrine\Common\Lexer\AbstractLexer;
12+
13+
/**
14+
* @extends AbstractLexer<ContentTypeFieldsExpressionDoctrineLexer::T_*, string>
15+
*/
16+
final class ContentTypeFieldsExpressionDoctrineLexer extends AbstractLexer
17+
{
18+
public const T_LBRACE = 1;
19+
public const T_RBRACE = 2;
20+
public const T_COMMA = 3;
21+
public const T_SLASH = 4;
22+
public const T_WILDCARD = 5;
23+
public const T_IDENTIFIER = 6;
24+
25+
/**
26+
* @return list<string>
27+
*/
28+
protected function getCatchablePatterns(): array
29+
{
30+
return [
31+
'[a-zA-Z_][a-zA-Z0-9_-]*',
32+
'\*',
33+
'[\{\},\/]',
34+
];
35+
}
36+
37+
/**
38+
* @return list<string>
39+
*/
40+
protected function getNonCatchablePatterns(): array
41+
{
42+
return [
43+
'\s+',
44+
];
45+
}
46+
47+
/**
48+
* @param string $value
49+
*/
50+
protected function getType(&$value): int
51+
{
52+
if ($value === '{') {
53+
return self::T_LBRACE;
54+
}
55+
56+
if ($value === '}') {
57+
return self::T_RBRACE;
58+
}
59+
60+
if ($value === ',') {
61+
return self::T_COMMA;
62+
}
63+
64+
if ($value === '/') {
65+
return self::T_SLASH;
66+
}
67+
68+
if ($value === '*') {
69+
return self::T_WILDCARD;
70+
}
71+
72+
return self::T_IDENTIFIER;
73+
}
74+
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
<?php
2+
3+
/**
4+
* @copyright Copyright (C) Ibexa AS. All rights reserved.
5+
* @license For full copyright and license information view LICENSE file distributed with this source code.
6+
*/
7+
declare(strict_types=1);
8+
9+
namespace Ibexa\AdminUi\Util;
10+
11+
use Ibexa\AdminUi\Exception\FieldTypeExpressionParserException;
12+
13+
final class ContentTypeFieldsExpressionParser implements ContentTypeFieldsExpressionParserInterface
14+
{
15+
private ContentTypeFieldsExpressionDoctrineLexer $lexer;
16+
17+
public function __construct()
18+
{
19+
$this->lexer = new ContentTypeFieldsExpressionDoctrineLexer();
20+
}
21+
22+
public function parseExpression(string $expression): ContentTypeFieldsParsedStructure
23+
{
24+
// Content type group can be omitted, therefore we need to know how many parts are there
25+
$slashCount = substr_count($expression, '/');
26+
27+
$this->lexer->setInput($expression);
28+
$this->lexer->moveNext();
29+
30+
$groupTokens = null; // Content type groups are optional
31+
$contentTypeTokens = null;
32+
$fieldTokens = null;
33+
34+
while ($this->lexer->lookahead !== null) {
35+
$this->lexer->moveNext();
36+
37+
if ($slashCount === 2) {
38+
$groupTokens = $this->parseSection();
39+
$this->expectSlash();
40+
$contentTypeTokens = $this->parseSection();
41+
$this->expectSlash();
42+
$fieldTokens = $this->parseSection();
43+
} elseif ($slashCount === 1) {
44+
$groupTokens = null;
45+
$contentTypeTokens = $this->parseSection();
46+
$this->expectSlash();
47+
$fieldTokens = $this->parseSection();
48+
} else {
49+
throw new FieldTypeExpressionParserException('Invalid expression, expected one or two T_SLASH delimiters.');
50+
}
51+
}
52+
53+
$structure = new ContentTypeFieldsParsedStructure(
54+
$groupTokens,
55+
$contentTypeTokens,
56+
$fieldTokens,
57+
);
58+
59+
if ($structure->isAllChosen()) {
60+
throw new FieldTypeExpressionParserException('Choosing every possible content type field is not allowed.');
61+
}
62+
63+
return $structure;
64+
}
65+
66+
/**
67+
* @return non-empty-list<string>|null
68+
*/
69+
private function parseSection(): ?array
70+
{
71+
$items = [];
72+
73+
if ($this->lexer->token === null) {
74+
throw new FieldTypeExpressionParserException('A token inside a section cannot be empty.');
75+
}
76+
77+
// Multiple elements between braces
78+
if ($this->lexer->token->isA(ContentTypeFieldsExpressionDoctrineLexer::T_LBRACE)) {
79+
$token = $this->getTokenFromInsideBracket();
80+
$items[] = $token;
81+
82+
while ($this->lexer->token->isA(ContentTypeFieldsExpressionDoctrineLexer::T_COMMA)) {
83+
$token = $this->getTokenFromInsideBracket();
84+
if (!in_array($token, $items, true)) {
85+
$items[] = $token;
86+
}
87+
}
88+
89+
if (!$this->lexer->token->isA(ContentTypeFieldsExpressionDoctrineLexer::T_RBRACE)) {
90+
throw new FieldTypeExpressionParserException('Expected T_RBRACE to close the list.');
91+
}
92+
93+
$this->lexer->moveNext();
94+
} else {
95+
// Otherwise, expect a single identifier or wildcard.
96+
$token = $this->expectIdentifierOrWildcard();
97+
98+
if ($token === null) {
99+
return null;
100+
}
101+
102+
$items[] = $token;
103+
}
104+
105+
return $items;
106+
}
107+
108+
private function getTokenFromInsideBracket(): string
109+
{
110+
$this->lexer->moveNext();
111+
112+
$token = $this->expectIdentifierOrWildcard();
113+
if ($token === null) {
114+
throw new FieldTypeExpressionParserException('Wildcards cannot be mixed with identifiers inside the expression.');
115+
}
116+
117+
return $token;
118+
}
119+
120+
/**
121+
* @throws \Ibexa\AdminUi\Exception\FieldTypeExpressionParserException
122+
*/
123+
private function expectSlash(): void
124+
{
125+
if ($this->lexer->token === null) {
126+
throw new FieldTypeExpressionParserException(
127+
sprintf(
128+
'Expected token of type "%s" but got "null"',
129+
ContentTypeFieldsExpressionDoctrineLexer::T_SLASH,
130+
),
131+
);
132+
}
133+
134+
if (!$this->lexer->token->isA(ContentTypeFieldsExpressionDoctrineLexer::T_SLASH)) {
135+
throw new FieldTypeExpressionParserException(
136+
sprintf(
137+
'Expected token of type "%s" but got "%s"',
138+
ContentTypeFieldsExpressionDoctrineLexer::T_SLASH,
139+
$this->lexer->token->type,
140+
),
141+
);
142+
}
143+
144+
$this->lexer->moveNext();
145+
}
146+
147+
private function expectIdentifierOrWildcard(): ?string
148+
{
149+
if ($this->lexer->token === null) {
150+
throw new FieldTypeExpressionParserException(
151+
sprintf(
152+
'Expected token of type "%s" but got "null"',
153+
ContentTypeFieldsExpressionDoctrineLexer::T_SLASH,
154+
),
155+
);
156+
}
157+
158+
if (!in_array(
159+
$this->lexer->token->type,
160+
[
161+
ContentTypeFieldsExpressionDoctrineLexer::T_IDENTIFIER,
162+
ContentTypeFieldsExpressionDoctrineLexer::T_WILDCARD,
163+
],
164+
true,
165+
)) {
166+
throw new FieldTypeExpressionParserException('Expected an identifier or wildcard.');
167+
}
168+
169+
$value = $this->lexer->token->isA(ContentTypeFieldsExpressionDoctrineLexer::T_WILDCARD)
170+
? null
171+
: $this->lexer->token->value;
172+
173+
$this->lexer->moveNext();
174+
175+
return $value;
176+
}
177+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
/**
4+
* @copyright Copyright (C) Ibexa AS. All rights reserved.
5+
* @license For full copyright and license information view LICENSE file distributed with this source code.
6+
*/
7+
declare(strict_types=1);
8+
9+
namespace Ibexa\AdminUi\Util;
10+
11+
interface ContentTypeFieldsExpressionParserInterface
12+
{
13+
/**
14+
* @throws \Ibexa\AdminUi\Exception\FieldTypeExpressionParserException
15+
*/
16+
public function parseExpression(string $expression): ContentTypeFieldsParsedStructure;
17+
}

0 commit comments

Comments
 (0)