Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/bundle/Resources/config/services/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,8 @@ services:

Ibexa\AdminUi\Service\MetaFieldType\MetaFieldDefinitionServiceInterface:
'@Ibexa\AdminUi\Service\MetaFieldType\MetaFieldDefinitionService'

Ibexa\AdminUi\ContentType\ContentTypeFieldsByExpressionService: ~

Ibexa\Contracts\AdminUi\ContentType\ContentTypeFieldsByExpressionServiceInterface:
'@Ibexa\AdminUi\ContentType\ContentTypeFieldsByExpressionService'
10 changes: 10 additions & 0 deletions src/bundle/Resources/config/services/utils.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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: ~
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Contracts\AdminUi\ContentType;

use Ibexa\Contracts\Core\Repository\Values\ContentType\FieldDefinition;

interface ContentTypeFieldsByExpressionServiceInterface
{
/**
* @return list<\Ibexa\Contracts\Core\Repository\Values\ContentType\FieldDefinition>
*
* @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;
}
61 changes: 61 additions & 0 deletions src/lib/ContentType/ContentTypeFieldsByExpressionService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\AdminUi\ContentType;

use Ibexa\AdminUi\Util\ContentTypeFieldsExtractorInterface;
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;
use Ibexa\Core\Repository\Mapper\ContentTypeDomainMapper;

final class ContentTypeFieldsByExpressionService implements ContentTypeFieldsByExpressionServiceInterface
{
private ContentTypeFieldsExtractorInterface $fieldsExtractor;

private ContentTypeHandler $contentTypeHandler;

private ContentTypeDomainMapper $contentTypeDomainMapper;

public function __construct(
ContentTypeFieldsExtractorInterface $fieldsExtractor,
ContentTypeHandler $contentTypeHandler,
ContentTypeDomainMapper $contentTypeDomainMapper
) {
$this->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);
}
}
15 changes: 15 additions & 0 deletions src/lib/Exception/FieldTypeExpressionParserException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\AdminUi\Exception;

use RuntimeException;

final class FieldTypeExpressionParserException extends RuntimeException
{
}
74 changes: 74 additions & 0 deletions src/lib/Util/ContentTypeFieldsExpressionDoctrineLexer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\AdminUi\Util;

use Doctrine\Common\Lexer\AbstractLexer;

/**
* @extends AbstractLexer<ContentTypeFieldsExpressionDoctrineLexer::T_*, string>
*/
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<string>
*/
protected function getCatchablePatterns(): array
{
return [
'[a-zA-Z_][a-zA-Z0-9_-]*',
'\*',
'[\{\},\/]',
];
}

/**
* @return list<string>
*/
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;
}
}
173 changes: 173 additions & 0 deletions src/lib/Util/ContentTypeFieldsExpressionParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\AdminUi\Util;

use Ibexa\AdminUi\Exception\FieldTypeExpressionParserException;

final class ContentTypeFieldsExpressionParser implements ContentTypeFieldsExpressionParserInterface
{
private ContentTypeFieldsExpressionDoctrineLexer $lexer;

public function __construct()
{
$this->lexer = new ContentTypeFieldsExpressionDoctrineLexer();
}

public function parseExpression(string $expression): ContentTypeFieldsParsedStructure
{
// 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 FieldTypeExpressionParserException('Invalid expression, expected one or two T_SLASH delimiters.');
}
}

$structure = new ContentTypeFieldsParsedStructure(
$groupTokens,
$contentTypeTokens,
$fieldTokens,
);

if ($structure->isAllChosen()) {
throw new FieldTypeExpressionParserException('Choosing every possible content type field is not allowed.');
}

return $structure;
}

/**
* @return non-empty-list<string>|null
*/
private function parseSection(): ?array
{
$items = [];

if ($this->lexer->token === null) {
throw new FieldTypeExpressionParserException('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 FieldTypeExpressionParserException('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 FieldTypeExpressionParserException('Wildcards cannot be mixed with identifiers inside the expression.');
}

return $token;
}

/**
* @throws \Ibexa\AdminUi\Exception\FieldTypeExpressionParserException
*/
private function expectSlash(): void
{
if ($this->lexer->token === null) {
throw new FieldTypeExpressionParserException(
sprintf(
'Expected token of type "%s" but got "null"',
ContentTypeFieldsExpressionDoctrineLexer::T_SLASH,
),
);
}

if (!$this->lexer->token->isA(ContentTypeFieldsExpressionDoctrineLexer::T_SLASH)) {
throw new FieldTypeExpressionParserException(
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 FieldTypeExpressionParserException(
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 FieldTypeExpressionParserException('Expected an identifier or wildcard.');
}

$value = $this->lexer->token->isA(ContentTypeFieldsExpressionDoctrineLexer::T_WILDCARD)
? null
: $this->lexer->token->value;

$this->lexer->moveNext();

return $value;
}
}
17 changes: 17 additions & 0 deletions src/lib/Util/ContentTypeFieldsExpressionParserInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\AdminUi\Util;

interface ContentTypeFieldsExpressionParserInterface
{
/**
* @throws \RuntimeException
*/
public function parseExpression(string $expression): ContentTypeFieldsParsedStructure;
}
Loading
Loading