limitationValues for '%identifier%' Limitation can not be empty]]>
- limitationValues for '%identifier%' Limitation can not be empty]]>
+ limitationValues for '%identifier%' Limitation can not be empty]]>key: $limitationValue->limitationValues for '%identifier%' Limitation can not be empty
+
+ limitationValues[%key%] => Invalid SiteAccess value '%value%']]>
+ limitationValues[%key%] => Invalid SiteAccess value '%value%']]>
+ key: $limitationValue->limitationValues[%key%] => Invalid SiteAccess value '%value%'
+ '%actualValue%' is incorrect value'%actualValue%' is incorrect value
@@ -23,17 +28,17 @@
A valid file is required. The following file extensions are not allowed: %extensionsBlackList%
- A valid file is required. The following file extensions are not allowed: %extensionsBlackList%
+ A valid file is required. The following file extensions are not allowed: %extensionsBlackList%key: A valid file is required. The following file extensions are not allowed: %extensionsBlackList%A valid image file is required.
- A valid image file is required.
+ A valid image file is required.key: A valid image file is required.Alternative text is required.
- Alternative text is required.
+ Alternative text is required.key: Alternative text is required.
@@ -73,7 +78,7 @@
Content %type% is not a valid asset target
- Content %type% is not a valid asset target
+ Content %type% is not a valid asset targetkey: Content %type% is not a valid asset target
@@ -83,7 +88,7 @@
Content type %contentTypeIdentifier% is not a valid relation target
- Content type %contentTypeIdentifier% is not a valid relation target
+ Content type %contentTypeIdentifier% is not a valid relation targetkey: Content type %contentTypeIdentifier% is not a valid relation target
@@ -93,7 +98,7 @@
Content with identifier %contentId% is not a valid relation target
- Content with identifier %contentId% is not a valid relation target
+ Content with identifier %contentId% is not a valid relation targetkey: Content with identifier %contentId% is not a valid relation target
@@ -113,27 +118,27 @@
Country with Alpha2 code '%alpha2%' is not defined in FieldType settings.
- Country with Alpha2 code '%alpha2%' is not defined in FieldType settings.
+ Country with Alpha2 code '%alpha2%' is not defined in FieldType settings.key: Country with Alpha2 code '%alpha2%' is not defined in FieldType settings.Each keyword must be a string.
- Each keyword must be a string.
+ Each keyword must be a string.key: Each keyword must be a string.Email '%email%' is used by another user. You must enter a unique email.
- Email '%email%' is used by another user. You must enter a unique email.
+ Email '%email%' is used by another user. You must enter a unique email.key: Email '%email%' is used by another user. You must enter a unique email.Email required
- Email required
+ Email requiredkey: Email requiredEnabled must be boolean value
- Enabled must be boolean value
+ Enabled must be boolean valuekey: Enabled must be boolean value
@@ -153,39 +158,49 @@
Field definition does not allow multiple countries to be selected.
- Field definition does not allow multiple countries to be selected.
+ Field definition does not allow multiple countries to be selected.key: Field definition does not allow multiple countries to be selected.Field definition does not allow multiple options to be selected.
- Field definition does not allow multiple options to be selected.
+ Field definition does not allow multiple options to be selected.key: Field definition does not allow multiple options to be selected.
+
+ Field type %field_type_identifier% is not searchable
+ Field type %field_type_identifier% is not searchable
+ key: Field type %field_type_identifier% is not searchable
+ FieldType '%fieldType%' does not accept settings
- FieldType '%fieldType%' does not accept settings
+ FieldType '%fieldType%' does not accept settingskey: FieldType '%fieldType%' does not accept settingsFieldType '%fieldType%' expects setting '%setting%' to be of type '%type%'
- FieldType '%fieldType%' expects setting '%setting%' to be of type '%type%'
+ FieldType '%fieldType%' expects setting '%setting%' to be of type '%type%'key: FieldType '%fieldType%' expects setting '%setting%' to be of type '%type%'ISBN value must be in a valid ISBN-10 format
- ISBN value must be in a valid ISBN-10 format
+ ISBN value must be in a valid ISBN-10 formatkey: ISBN value must be in a valid ISBN-10 formatISBN-10 must be 10 character length
- ISBN-10 must be 10 character length
+ ISBN-10 must be 10 character lengthkey: ISBN-10 must be 10 character lengthInvalid login format
- Invalid login format
+ Invalid login formatkey: Invalid login format
+
+ Keyword value must be less than or equal to 255 characters.
+ Keyword value must be less than or equal to 255 characters.
+ key: Keyword value must be less than or equal to 255 characters.
+ Limitation '%limitation%' not found. It must be implemented or configured to use %blockingLimitation%Limitation '%limitation%' not found. It must be implemented or configured to use %blockingLimitation%
@@ -198,27 +213,27 @@
Login required
- Login required
+ Login requiredkey: Login requiredNew password cannot be the same as old password
- New password cannot be the same as old password
+ New password cannot be the same as old passwordkey: New password cannot be the same as old passwordOption with index %index% does not exist in the field definition.
- Option with index %index% does not exist in the field definition.
+ Option with index %index% does not exist in the field definition.key: Option with index %index% does not exist in the field definition.Password expiration warning value should be lower then password expiration value
- Password expiration warning value should be lower then password expiration value
+ Password expiration warning value should be lower then password expiration valuekey: Password expiration warning value should be lower then password expiration valuePassword required
- Password required
+ Password requiredkey: Password required
@@ -233,77 +248,77 @@
Setting '%setting%' contains unsupported mime types
- Setting '%setting%' contains unsupported mime types
+ Setting '%setting%' contains unsupported mime typeskey: Setting '%setting%' contains unsupported mime typesSetting '%setting%' has unknown default value
- Setting '%setting%' has unknown default value
+ Setting '%setting%' has unknown default valuekey: Setting '%setting%' has unknown default valueSetting '%setting%' is of unknown type
- Setting '%setting%' is of unknown type
+ Setting '%setting%' is of unknown typekey: Setting '%setting%' is of unknown typeSetting '%setting%' is unknown
- Setting '%setting%' is unknown
+ Setting '%setting%' is unknownkey: Setting '%setting%' is unknownSetting '%setting%' must be either %selection_browse% or %selection_dropdown%
- Setting '%setting%' must be either %selection_browse% or %selection_dropdown%
+ Setting '%setting%' must be either %selection_browse% or %selection_dropdown%key: Setting '%setting%' must be either %selection_browse% or %selection_dropdown%Setting '%setting%' value cannot be lower than 0
- Setting '%setting%' value cannot be lower than 0
+ Setting '%setting%' value cannot be lower than 0key: Setting '%setting%' value cannot be lower than 0Setting '%setting%' value must be of array type
- Setting '%setting%' value must be of array type
+ Setting '%setting%' value must be of array typekey: Setting '%setting%' value must be of array typeSetting '%setting%' value must be of boolean type
- Setting '%setting%' value must be of boolean type
+ Setting '%setting%' value must be of boolean typekey: Setting '%setting%' value must be of boolean typeSetting '%setting%' value must be of either null or bool
- Setting '%setting%' value must be of either null or bool
+ Setting '%setting%' value must be of either null or boolkey: Setting '%setting%' value must be of either null or boolSetting '%setting%' value must be of either null, string or integer
- Setting '%setting%' value must be of either null, string or integer
+ Setting '%setting%' value must be of either null, string or integerkey: Setting '%setting%' value must be of either null, string or integerSetting '%setting%' value must be of integer type
- Setting '%setting%' value must be of integer type
+ Setting '%setting%' value must be of integer typekey: Setting '%setting%' value must be of integer typeSetting 'Current date and time adjusted by' can be used only when setting 'Default value' is set to 'Adjusted current datetime'
- Setting 'Current date and time adjusted by' can be used only when setting 'Default value' is set to 'Adjusted current datetime'
+ Setting 'Current date and time adjusted by' can be used only when setting 'Default value' is set to 'Adjusted current datetime'key: Setting 'Current date and time adjusted by' can be used only when setting 'Default value' is set to 'Adjusted current datetime'Setting 'Current date and time adjusted by' value must be an instance of 'DateInterval' class
- Setting 'Current date and time adjusted by' value must be an instance of 'DateInterval' class
+ Setting 'Current date and time adjusted by' value must be an instance of 'DateInterval' classkey: Setting 'Current date and time adjusted by' value must be an instance of 'DateInterval' classSetting 'Default value' is of unknown type
- Setting 'Default value' is of unknown type
+ Setting 'Default value' is of unknown typekey: Setting 'Default value' is of unknown typeSetting 'Use seconds' value must be of boolean type
- Setting 'Use seconds' value must be of boolean type
+ Setting 'Use seconds' value must be of boolean typekey: Setting 'Use seconds' value must be of boolean type
@@ -318,82 +333,82 @@
The file size cannot exceed %size% byte.
- The file size cannot exceed %size% byte.
+ The file size cannot exceed %size% byte.key: The file size cannot exceed %size% byte.The file size cannot exceed %size% bytes.
- The file size cannot exceed %size% bytes.
+ The file size cannot exceed %size% bytes.key: The file size cannot exceed %size% bytes.The file size cannot exceed %size% megabyte.
- The file size cannot exceed %size% megabyte.
+ The file size cannot exceed %size% megabyte.key: The file size cannot exceed %size% megabyte.The file size cannot exceed %size% megabytes.
- The file size cannot exceed %size% megabytes.
+ The file size cannot exceed %size% megabytes.key: The file size cannot exceed %size% megabytes.The following options are available for setting '%setting%': %selection_browse%, %selection_dropdown%, %selection_list_with_radio_buttons%, %selection_list_with_checkboxes%, %selection_multiple_selection_list%, %selection_template_based_multiple%, %selection_template_based_single%
- The following options are available for setting '%setting%': %selection_browse%, %selection_dropdown%, %selection_list_with_radio_buttons%, %selection_list_with_checkboxes%, %selection_multiple_selection_list%, %selection_template_based_multiple%, %selection_template_based_single%
+ The following options are available for setting '%setting%': %selection_browse%, %selection_dropdown%, %selection_list_with_radio_buttons%, %selection_list_with_checkboxes%, %selection_multiple_selection_list%, %selection_template_based_multiple%, %selection_template_based_single%key: The following options are available for setting '%setting%': %selection_browse%, %selection_dropdown%, %selection_list_with_radio_buttons%, %selection_list_with_checkboxes%, %selection_multiple_selection_list%, %selection_template_based_multiple%, %selection_template_based_single%The given e-mail '%email%' is invalid
- The given e-mail '%email%' is invalid
+ The given e-mail '%email%' is invalidkey: The given e-mail '%email%' is invalidThe mime type of the file is invalid (%mimeType%). Allowed mime types are %mimeTypes%.
- The mime type of the file is invalid (%mimeType%). Allowed mime types are %mimeTypes%.
+ The mime type of the file is invalid (%mimeType%). Allowed mime types are %mimeTypes%.key: The mime type of the file is invalid (%mimeType%). Allowed mime types are %mimeTypes%.The selected content items number cannot be higher than %limit%.
- The selected content items number cannot be higher than %limit%.
+ The selected content items number cannot be higher than %limit%.key: The selected content items number cannot be higher than %limit%.The string can not exceed %size% character.
- The string can not exceed %size% character.
+ The string can not exceed %size% character.key: The string can not exceed %size% character.The string can not exceed %size% characters.
- The string can not exceed %size% characters.
+ The string can not exceed %size% characters.key: The string can not exceed %size% characters.The string cannot be shorter than %size% character.
- The string cannot be shorter than %size% character.
+ The string cannot be shorter than %size% character.key: The string cannot be shorter than %size% character.The string cannot be shorter than %size% characters.
- The string cannot be shorter than %size% characters.
+ The string cannot be shorter than %size% characters.key: The string cannot be shorter than %size% characters.The user login '%login%' is used by another user. You must enter a unique login.
- The user login '%login%' is used by another user. You must enter a unique login.
+ The user login '%login%' is used by another user. You must enter a unique login.key: The user login '%login%' is used by another user. You must enter a unique login.The value can not be higher than %size%.
- The value can not be higher than %size%.
+ The value can not be higher than %size%.key: The value can not be higher than %size%.The value can not be lower than %size%.
- The value can not be lower than %size%.
+ The value can not be lower than %size%.key: The value can not be lower than %size%.The value must be a valid email address.
- The value must be a valid email address.
+ The value must be a valid email address.key: The value must be a valid email address.
@@ -408,62 +423,67 @@
Validator %validator% expects parameter %parameter% to be of %type% type.
- Validator %validator% expects parameter %parameter% to be of %type% type.
+ Validator %validator% expects parameter %parameter% to be of %type% type.key: Validator %validator% expects parameter %parameter% to be of %type% type.Validator %validator% expects parameter %parameter% to be of %type%.
- Validator %validator% expects parameter %parameter% to be of %type%.
+ Validator %validator% expects parameter %parameter% to be of %type%.key: Validator %validator% expects parameter %parameter% to be of %type%.Validator %validator% expects parameter %parameter% to be set.
- Validator %validator% expects parameter %parameter% to be set.
+ Validator %validator% expects parameter %parameter% to be set.key: Validator %validator% expects parameter %parameter% to be set.
+
+ Validator '%parameter%' is unknown
+ Validator '%parameter%' is unknown
+ key: Validator '%parameter%' is unknown
+ Validator '%validator%' is unknown
- Validator '%validator%' is unknown
+ Validator '%validator%' is unknownkey: Validator '%validator%' is unknownValidator parameter '%parameter%' is unknown
- Validator parameter '%parameter%' is unknown
+ Validator parameter '%parameter%' is unknownkey: Validator parameter '%parameter%' is unknownValidator parameter '%parameter%' value can't be negative
- Validator parameter '%parameter%' value can't be negative
+ Validator parameter '%parameter%' value can't be negativekey: Validator parameter '%parameter%' value can't be negativeValidator parameter '%parameter%' value must be an integer
- Validator parameter '%parameter%' value must be an integer
+ Validator parameter '%parameter%' value must be an integerkey: Validator parameter '%parameter%' value must be an integerValidator parameter '%parameter%' value must be equal to/greater than 0
- Validator parameter '%parameter%' value must be equal to/greater than 0
+ Validator parameter '%parameter%' value must be equal to/greater than 0key: Validator parameter '%parameter%' value must be equal to/greater than 0Validator parameter '%parameter%' value must be of integer type
- Validator parameter '%parameter%' value must be of integer type
+ Validator parameter '%parameter%' value must be of integer typekey: Validator parameter '%parameter%' value must be of integer typeValidator parameter '%parameter%' value must be regex for now
- Validator parameter '%parameter%' value must be regex for now
+ Validator parameter '%parameter%' value must be regex for nowkey: Validator parameter '%parameter%' value must be regex for nowValidator parameter 'maxStringLength' can't be shorter than validator parameter 'minStringLength' value
- Validator parameter 'maxStringLength' can't be shorter than validator parameter 'minStringLength' value
+ Validator parameter 'maxStringLength' can't be shorter than validator parameter 'minStringLength' valuekey: Validator parameter 'maxStringLength' can't be shorter than validator parameter 'minStringLength' valueValue for required field definition '%identifier%' with language '%languageCode%' is empty
- Value for required field definition '%identifier%' with language '%languageCode%' is empty
+ Value for required field definition '%identifier%' with language '%languageCode%' is emptykey: Value for required field definition '%identifier%' with language '%languageCode%' is empty
@@ -473,32 +493,32 @@
'%value%' does not equal Location's path string: '%path_string%']]>
- '%value%' does not equal Location's path string: '%path_string%']]>
+ '%value%' does not equal Location's path string: '%path_string%']]>key: limitationValues[%key%] => '%value%' does not equal Location's path string: '%path_string%' '%value%' does not exist in the backend]]>
- '%value%' does not exist in the backend]]>
+ '%value%' does not exist in the backend]]>key: limitationValues[%key%] => '%value%' does not exist in the backend '%value%' must be 1 (owner)]]>
- '%value%' must be 1 (owner)]]>
+ '%value%' must be 1 (owner)]]>key: limitationValues[%key%] => '%value%' must be 1 (owner) '%value%' must be an integer]]>
- '%value%' must be an integer]]>
+ '%value%' must be an integer]]>key: limitationValues[%key%] => '%value%' must be an integer '%value%' must be either 1 (owner) or 2 (session)]]>
- '%value%' must be either 1 (owner) or 2 (session)]]>
+ '%value%' must be either 1 (owner) or 2 (session)]]>key: limitationValues[%key%] => '%value%' must be either 1 (owner) or 2 (session) '%languageCodes%' translation(s) do not exist]]>
- '%languageCodes%' translation(s) do not exist]]>
+ '%languageCodes%' translation(s) do not exist]]>key: limitationValues[] => '%languageCodes%' translation(s) do not exist
diff --git a/src/bundle/Core/Resources/translations/validators.en.xlf b/src/bundle/Core/Resources/translations/validators.en.xlf
index 93900c2ff4..96dfe8c2cc 100644
--- a/src/bundle/Core/Resources/translations/validators.en.xlf
+++ b/src/bundle/Core/Resources/translations/validators.en.xlf
@@ -8,7 +8,7 @@
Identifier already exists.
- Identifier already exists.
+ Identifier already exists.key: ibexa.identifier_already_exists
diff --git a/src/lib/FieldType/BaseTextType.php b/src/lib/FieldType/BaseTextType.php
index 3e28d47f99..9ed0945650 100644
--- a/src/lib/FieldType/BaseTextType.php
+++ b/src/lib/FieldType/BaseTextType.php
@@ -63,11 +63,10 @@ protected function checkValueStructure(BaseValue $value): void
protected function buildUnknownValidatorError(string $parameterName, string $validatorIdentifier): ValidationError
{
return new ValidationError(
- "Validator '$parameterName' is unknown",
+ /** @Desc("Validator '%parameter%' is unknown") */
+ "Validator '%parameter%' is unknown",
null,
- [
- $parameterName => $validatorIdentifier,
- ]
+ ['%parameter%' => $parameterName]
);
}
diff --git a/src/lib/FieldType/ISBN/Type.php b/src/lib/FieldType/ISBN/Type.php
index 3f17ed6a7e..652ccf0488 100644
--- a/src/lib/FieldType/ISBN/Type.php
+++ b/src/lib/FieldType/ISBN/Type.php
@@ -13,6 +13,7 @@
use Ibexa\Core\FieldType\FieldType;
use Ibexa\Core\FieldType\ValidationError;
use Ibexa\Core\FieldType\Value as BaseValue;
+use JMS\TranslationBundle\Annotation\Ignore;
use JMS\TranslationBundle\Model\Message;
use JMS\TranslationBundle\Translation\TranslationContainerInterface;
@@ -157,7 +158,10 @@ public function validate(FieldDefinition $fieldDefinition, SPIValue $fieldValue)
} else {
// ISBN-13 check
if (!$this->validateISBN13Checksum($isbnTestNumber, $error)) {
+ // TODO: Replace the out-parameter error flow with stable translation keys/templates.
+ // JMS extraction cannot read a ValidationError ID passed through a variable.
$validationErrors[] = new ValidationError(
+ /** @Ignore */
$error,
null,
[],
diff --git a/src/lib/FieldType/Keyword/Type.php b/src/lib/FieldType/Keyword/Type.php
index fc740e4e32..128044974b 100644
--- a/src/lib/FieldType/Keyword/Type.php
+++ b/src/lib/FieldType/Keyword/Type.php
@@ -25,6 +25,7 @@
class Type extends FieldType implements TranslationContainerInterface
{
public const MAX_KEYWORD_LENGTH = 255;
+ private const string ERROR_MESSAGE_MAX_KEYWORD_LENGTH = 'Keyword value must be less than or equal to 255 characters.';
/**
* Returns the field type identifier for this field type.
@@ -120,7 +121,7 @@ public function validate(FieldDefinition $fieldDefinition, SPIValue $fieldValue)
);
} elseif (mb_strlen($keyword) > self::MAX_KEYWORD_LENGTH) {
$validationErrors[] = new ValidationError(
- 'Keyword value must be less than or equal to ' . self::MAX_KEYWORD_LENGTH . ' characters.',
+ self::ERROR_MESSAGE_MAX_KEYWORD_LENGTH,
null,
[],
'values'
diff --git a/src/lib/FieldType/Validator/BaseNumericValidator.php b/src/lib/FieldType/Validator/BaseNumericValidator.php
index d0741c91b3..e5cc869d6c 100644
--- a/src/lib/FieldType/Validator/BaseNumericValidator.php
+++ b/src/lib/FieldType/Validator/BaseNumericValidator.php
@@ -10,6 +10,7 @@
use Ibexa\Core\FieldType\ValidationError;
use Ibexa\Core\FieldType\Validator;
+use JMS\TranslationBundle\Annotation\Ignore;
abstract class BaseNumericValidator extends Validator
{
@@ -24,7 +25,10 @@ public function validateConstraints($constraints): array
foreach ($constraints as $name => $value) {
$validationErrorMessage = $this->getConstraintsValidationErrorMessage($name, $value);
if (null !== $validationErrorMessage) {
+ // TODO: Return stable translation keys/templates instead of a computed message string.
+ // JMS extraction cannot read a ValidationError ID passed through a variable.
$validationErrors[] = new ValidationError(
+ /** @Ignore */
$validationErrorMessage,
null,
[
diff --git a/src/lib/FieldType/Validator/StringLengthValidator.php b/src/lib/FieldType/Validator/StringLengthValidator.php
index 85c136967e..3f1babe225 100644
--- a/src/lib/FieldType/Validator/StringLengthValidator.php
+++ b/src/lib/FieldType/Validator/StringLengthValidator.php
@@ -11,6 +11,7 @@
use Ibexa\Core\FieldType\ValidationError;
use Ibexa\Core\FieldType\Validator;
use Ibexa\Core\FieldType\Value as BaseValue;
+use JMS\TranslationBundle\Annotation\Desc;
/**
* Validator for checking min. and max. length of strings.
@@ -47,11 +48,10 @@ public function validateConstraints($constraints)
case 'maxStringLength':
if ($value !== false && !is_int($value) && !(null === $value)) {
$validationErrors[] = new ValidationError(
- sprintf('Validator parameter \'%s\' value must be of integer type', self::PARAMETER_NAME),
+ /** @Desc("Validator parameter '%parameter%' value must be of integer type") */
+ "Validator parameter '%parameter%' value must be of integer type",
null,
- [
- self::PARAMETER_NAME => $name,
- ]
+ [self::PARAMETER_NAME => $name]
);
} elseif ($value < 0) {
$validationErrors[] = new ValidationError(
diff --git a/src/lib/Limitation/SiteAccessLimitationType.php b/src/lib/Limitation/SiteAccessLimitationType.php
index 7776be10c6..d7a323e35c 100644
--- a/src/lib/Limitation/SiteAccessLimitationType.php
+++ b/src/lib/Limitation/SiteAccessLimitationType.php
@@ -83,12 +83,9 @@ public function validate(APILimitationValue $limitationValue): array
foreach ($limitationValue->limitationValues as $key => $value) {
if (!isset($siteAccessList[$value])) {
$validationErrors[] = new ValidationError(
- "\$limitationValue->limitationValues[%key%] => Invalid SiteAccess value \"$value\"",
+ "\$limitationValue->limitationValues[%key%] => Invalid SiteAccess value '%value%'",
null,
- [
- 'value' => $value,
- 'key' => $key,
- ]
+ ['%key%' => $key, '%value%' => $value]
);
}
}
diff --git a/src/lib/MVC/Symfony/Translation/Annotation/Domain.php b/src/lib/MVC/Symfony/Translation/Annotation/Domain.php
new file mode 100644
index 0000000000..c54fc06684
--- /dev/null
+++ b/src/lib/MVC/Symfony/Translation/Annotation/Domain.php
@@ -0,0 +1,35 @@
+value = $values['value'];
+ }
+}
diff --git a/src/lib/MVC/Symfony/Translation/ValidationErrorFileVisitor.php b/src/lib/MVC/Symfony/Translation/ValidationErrorFileVisitor.php
index 40884bc9e8..4d892e41f7 100644
--- a/src/lib/MVC/Symfony/Translation/ValidationErrorFileVisitor.php
+++ b/src/lib/MVC/Symfony/Translation/ValidationErrorFileVisitor.php
@@ -4,10 +4,12 @@
* @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\Core\MVC\Symfony\Translation;
use Doctrine\Common\Annotations\DocParser;
+use Ibexa\Core\MVC\Symfony\Translation\Annotation\Domain;
use JMS\TranslationBundle\Annotation\Desc;
use JMS\TranslationBundle\Annotation\Ignore;
use JMS\TranslationBundle\Annotation\Meaning;
@@ -17,12 +19,20 @@
use JMS\TranslationBundle\Model\MessageCatalogue;
use JMS\TranslationBundle\Translation\Extractor\FileVisitorInterface;
use JMS\TranslationBundle\Translation\FileSourceFactory;
-use PhpParser\Comment\Doc;
use PhpParser\Node;
+use PhpParser\Node\Arg;
+use PhpParser\Node\Expr\ClassConstFetch;
+use PhpParser\Node\Expr\New_;
+use PhpParser\Node\Identifier;
+use PhpParser\Node\Name;
use PhpParser\Node\Scalar\String_;
+use PhpParser\Node\Stmt\ClassConst;
+use PhpParser\Node\Stmt\ClassLike;
+use PhpParser\Node\Stmt\Namespace_;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor;
use Psr\Log\LoggerInterface;
+use SplFileInfo;
use Twig\Node\Node as TwigNode;
/**
@@ -36,22 +46,25 @@ class ValidationErrorFileVisitor implements LoggerAwareInterface, FileVisitorInt
private MessageCatalogue $catalogue;
- private \SplFileInfo $file;
+ private SplFileInfo $file;
private DocParser $docParser;
- private LoggerInterface $logger;
+ private ?LoggerInterface $logger = null;
- private Node $previousNode;
+ private ?Node $previousNode = null;
+
+ /** @var list> */
+ private array $classStringConstantStack = [];
+
+ /** @var list */
+ private array $classNameStack = [];
+
+ /** @var list */
+ private array $namespaceStack = [];
protected string $defaultDomain = 'ibexa_repository_exceptions';
- /**
- * DefaultPhpFileExtractor constructor.
- *
- * @param \Doctrine\Common\Annotations\DocParser $docParser
- * @param \JMS\TranslationBundle\Translation\FileSourceFactory $fileSourceFactory
- */
public function __construct(DocParser $docParser, FileSourceFactory $fileSourceFactory)
{
$this->docParser = $docParser;
@@ -60,27 +73,47 @@ public function __construct(DocParser $docParser, FileSourceFactory $fileSourceF
$this->traverser->addVisitor($this);
}
- /**
- * @param \Psr\Log\LoggerInterface $logger
- */
- public function setLogger(LoggerInterface $logger)
+ public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
public function enterNode(Node $node): null
{
- if (!$node instanceof Node\Expr\New_
- || !$node->class instanceof Node\Name
- || strtolower((string)$node->class) !== 'validationerror'
+ if ($node instanceof Namespace_) {
+ $this->namespaceStack[] = $node->name?->toString();
+ $this->previousNode = $node;
+
+ return null;
+ }
+
+ if ($node instanceof ClassLike) {
+ $this->classStringConstantStack[] = $this->extractStringConstants($node);
+ $this->classNameStack[] = $node->name?->toString();
+ $this->previousNode = $node;
+
+ return null;
+ }
+
+ if (!$node instanceof New_
+ || !$node->class instanceof Name
+ || strtolower((string) $node->class) !== 'validationerror'
) {
$this->previousNode = $node;
return null;
}
+ $idArg = $this->getArgument($node, 0);
+ if (null === $idArg) {
+ $this->previousNode = $node;
+
+ return null;
+ }
+
$ignore = false;
$desc = $meaning = null;
+ $domain = $this->defaultDomain;
if (null !== $docComment = $this->getDocCommentForNode($node)) {
foreach ($this->docParser->parse($docComment, 'file ' . $this->file . ' near line ' . $node->getLine()) as $annot) {
if ($annot instanceof Ignore) {
@@ -89,18 +122,26 @@ public function enterNode(Node $node): null
$desc = $annot->text;
} elseif ($annot instanceof Meaning) {
$meaning = $annot->text;
+ } elseif ($annot instanceof Domain) {
+ $domain = $annot->value;
}
}
}
- if (!$node->args[0]->value instanceof String_) {
+ $id = $this->resolveTranslationId($idArg->value);
+ if (null === $id) {
if ($ignore) {
return null;
}
- $message = sprintf('Can only extract the translation ID from a scalar string, but got "%s". Refactor your code to make it extractable, or add the doc comment /** @Ignore */ to this code element (in %s on line %d).', get_class($node->args[0]->value), $this->file, $node->args[0]->value->getLine());
+ $message = sprintf(
+ 'Can only extract the translation ID from a scalar string, but got "%s". Refactor your code to make it extractable, or add the doc comment /** @Ignore */ to this code element (in %s on line %d).',
+ get_class($idArg->value),
+ $this->file,
+ $idArg->value->getLine()
+ );
- if (isset($this->logger)) {
+ if (null !== $this->logger) {
$this->logger->error($message);
return null;
@@ -109,15 +150,15 @@ public function enterNode(Node $node): null
throw new RuntimeException($message);
}
- $message = new Message($node->args[0]->value->value, $this->defaultDomain);
+ $message = new Message($id, $domain);
$message->setDesc($desc);
$message->setMeaning($meaning);
$message->addSource($this->fileSourceFactory->create($this->file, $node->getLine()));
$this->catalogue->add($message);
- // plural
- if (isset($node->args[1]) && $node->args[1]->value instanceof String_) {
- $message = new Message($node->args[1]->value->value, $this->defaultDomain);
+ $pluralArg = $this->getArgument($node, 1);
+ if (null !== $pluralArg && null !== ($pluralId = $this->resolveTranslationId($pluralArg->value))) {
+ $message = new Message($pluralId, $domain);
$message->setDesc($desc);
$message->setMeaning($meaning);
$message->addSource($this->fileSourceFactory->create($this->file, $node->getLine()));
@@ -128,24 +169,29 @@ public function enterNode(Node $node): null
}
/**
- * @param \SplFileInfo $file
- * @param \JMS\TranslationBundle\Model\MessageCatalogue $catalogue
- * @param \PhpParser\Node\Stmt[] $ast
+ * @param array<\PhpParser\Node> $nodes
*/
- public function visitPhpFile(\SplFileInfo $file, MessageCatalogue $catalogue, array $ast): void
- {
- $this->file = $file;
- $this->catalogue = $catalogue;
- $this->traverser->traverse($ast);
- }
-
public function beforeTraverse(array $nodes): null
{
+ $this->previousNode = null;
+ $this->classStringConstantStack = [];
+ $this->classNameStack = [];
+ $this->namespaceStack = [];
+
return null;
}
public function leaveNode(Node $node): null
{
+ if ($node instanceof ClassLike) {
+ array_pop($this->classStringConstantStack);
+ array_pop($this->classNameStack);
+ }
+
+ if ($node instanceof Namespace_) {
+ array_pop($this->namespaceStack);
+ }
+
return null;
}
@@ -155,46 +201,122 @@ public function afterTraverse(array $nodes): null
}
/**
- * @param \SplFileInfo $file
- * @param \JMS\TranslationBundle\Model\MessageCatalogue $catalogue
+ * @param array<\PhpParser\Node> $ast
*/
- public function visitFile(\SplFileInfo $file, MessageCatalogue $catalogue): void
+ public function visitPhpFile(SplFileInfo $file, MessageCatalogue $catalogue, array $ast): void
+ {
+ $this->file = $file;
+ $this->catalogue = $catalogue;
+ $this->traverser->traverse($ast);
+ }
+
+ public function visitFile(SplFileInfo $file, MessageCatalogue $catalogue): void
{
}
- /**
- * @param \SplFileInfo $file
- * @param \JMS\TranslationBundle\Model\MessageCatalogue $catalogue
- * @param \Twig\Node\Node $ast
- */
- public function visitTwigFile(\SplFileInfo $file, MessageCatalogue $catalogue, TwigNode $ast): void
+ public function visitTwigFile(SplFileInfo $file, MessageCatalogue $catalogue, TwigNode $ast): void
{
}
- /**
- * @param \PhpParser\Node\Expr\New_ $node
- *
- * @return string|null
- */
- private function getDocCommentForNode(Node $node): ?string
+ private function getDocCommentForNode(New_ $node): ?string
{
- // check if there is a doc comment for the ID argument
- // ->trans(/** @Desc("FOO") */ 'my.id')
- if (null !== $comment = $node->args[0]->getDocComment()) {
+ $idArg = $this->getArgument($node, 0);
+ if (null !== $idArg && null !== ($comment = $idArg->getDocComment())) {
return $comment->getText();
}
- // this may be placed somewhere up in the hierarchy,
- // -> /** @Desc("FOO") */ trans('my.id')
- // /** @Desc("FOO") */ ->trans('my.id')
- // /** @Desc("FOO") */ $translator->trans('my.id')
if (null !== $comment = $node->getDocComment()) {
return $comment->getText();
}
- return
- ($comment = $this->previousNode->getDocComment()) !== null
+ return null !== $this->previousNode && null !== ($comment = $this->previousNode->getDocComment())
? $comment->getText()
: null;
}
+
+ private function getArgument(New_ $node, int $index): ?Arg
+ {
+ return isset($node->args[$index]) && $node->args[$index] instanceof Arg
+ ? $node->args[$index]
+ : null;
+ }
+
+ private function resolveTranslationId(Node $node): ?string
+ {
+ if ($node instanceof String_) {
+ return $node->value;
+ }
+
+ if (!$node instanceof ClassConstFetch || !$node->name instanceof Identifier) {
+ return null;
+ }
+
+ if (!$this->isCurrentClassConstantFetch($node)) {
+ return null;
+ }
+
+ $constants = end($this->classStringConstantStack);
+ if (false === $constants) {
+ return null;
+ }
+
+ return $constants[$node->name->toString()] ?? null;
+ }
+
+ private function isCurrentClassConstantFetch(ClassConstFetch $node): bool
+ {
+ if (!$node->class instanceof Name || [] === $this->classStringConstantStack) {
+ return false;
+ }
+
+ $className = strtolower($node->class->toString());
+ if (in_array($className, ['self', 'static'], true)) {
+ return true;
+ }
+
+ $currentClass = $this->getCurrentClassName();
+ if (null === $currentClass) {
+ return false;
+ }
+
+ if ($className === strtolower($currentClass)) {
+ return true;
+ }
+
+ $namespace = end($this->namespaceStack);
+ if (false === $namespace || null === $namespace) {
+ return false;
+ }
+
+ return ltrim($className, '\\') === strtolower($namespace . '\\' . $currentClass);
+ }
+
+ private function getCurrentClassName(): ?string
+ {
+ $className = end($this->classNameStack);
+
+ return false === $className ? null : $className;
+ }
+
+ /**
+ * @return array
+ */
+ private function extractStringConstants(ClassLike $class): array
+ {
+ $constants = [];
+
+ foreach ($class->stmts ?? [] as $statement) {
+ if (!$statement instanceof ClassConst) {
+ continue;
+ }
+
+ foreach ($statement->consts as $const) {
+ if ($const->value instanceof String_) {
+ $constants[$const->name->toString()] = $const->value->value;
+ }
+ }
+ }
+
+ return $constants;
+ }
}
diff --git a/src/lib/Repository/ContentTypeService.php b/src/lib/Repository/ContentTypeService.php
index 21fe013dec..a0dc65b809 100644
--- a/src/lib/Repository/ContentTypeService.php
+++ b/src/lib/Repository/ContentTypeService.php
@@ -708,7 +708,9 @@ protected function validateFieldDefinitionCreateStruct(FieldDefinitionCreateStru
if ($fieldDefinitionCreateStruct->isSearchable && !$fieldType->isSearchable()) {
$validationErrors[] = new ValidationError(
- "FieldType '{$fieldDefinitionCreateStruct->fieldTypeIdentifier}' is not searchable"
+ 'Field type %field_type_identifier% is not searchable',
+ null,
+ ['%field_type_identifier%' => $fieldDefinitionCreateStruct->fieldTypeIdentifier]
);
}
diff --git a/src/lib/Repository/Mapper/ContentTypeDomainMapper.php b/src/lib/Repository/Mapper/ContentTypeDomainMapper.php
index 881de51212..b0854acaf6 100644
--- a/src/lib/Repository/Mapper/ContentTypeDomainMapper.php
+++ b/src/lib/Repository/Mapper/ContentTypeDomainMapper.php
@@ -277,7 +277,11 @@ public function buildSPIFieldDefinitionFromUpdateStruct(
$validationErrors = [];
if ($fieldDefinitionUpdateStruct->isSearchable && !$fieldType->isSearchable()) {
$validationErrors[] = new ValidationError(
- "FieldType '{$fieldDefinition->fieldTypeIdentifier}' is not searchable"
+ 'Field type %field_type_identifier% is not searchable',
+ null,
+ [
+ '%field_type_identifier%' => $fieldDefinition->fieldTypeIdentifier,
+ ]
);
}
$validationErrors = array_merge(
diff --git a/src/lib/Repository/Validator/UserPasswordValidator.php b/src/lib/Repository/Validator/UserPasswordValidator.php
index a802df09ef..11f0c3a2b0 100644
--- a/src/lib/Repository/Validator/UserPasswordValidator.php
+++ b/src/lib/Repository/Validator/UserPasswordValidator.php
@@ -9,6 +9,7 @@
namespace Ibexa\Core\Repository\Validator;
use Ibexa\Core\FieldType\ValidationError;
+use JMS\TranslationBundle\Annotation\Ignore;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Validation;
@@ -198,6 +199,14 @@ private function isCompromised(
*/
private function createValidationError(string $message, array $values = []): ValidationError
{
- return new ValidationError($message, null, $values, 'password');
+ // TODO: Refactor this helper to use stable translation keys/templates.
+ // JMS extraction cannot read a ValidationError ID passed through a variable.
+ return new ValidationError(
+ /** @Ignore */
+ $message,
+ null,
+ $values,
+ 'password'
+ );
}
}
diff --git a/tests/lib/Limitation/SiteAccessLimitationTypeTest.php b/tests/lib/Limitation/SiteAccessLimitationTypeTest.php
index 2996a4f4a5..01f83d2aa1 100644
--- a/tests/lib/Limitation/SiteAccessLimitationTypeTest.php
+++ b/tests/lib/Limitation/SiteAccessLimitationTypeTest.php
@@ -9,6 +9,7 @@
use Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException;
use Ibexa\Contracts\Core\Repository\Exceptions\NotImplementedException;
+use Ibexa\Contracts\Core\Repository\Values\Translation\Message;
use Ibexa\Contracts\Core\Repository\Values\User\Limitation;
use Ibexa\Contracts\Core\Repository\Values\User\Limitation\ObjectStateLimitation;
use Ibexa\Contracts\Core\Repository\Values\User\Limitation\SiteAccessLimitation;
@@ -177,6 +178,27 @@ public function testValidateError(SiteAccessLimitation $limitation, $errorCount,
self::assertCount($errorCount, $validationErrors);
}
+ /**
+ * @depends testConstruct
+ */
+ public function testValidateErrorMessage(SiteAccessLimitationType $limitationType): void
+ {
+ $validationErrors = $limitationType->validate(
+ new SiteAccessLimitation(
+ [
+ 'limitationValues' => ['2339567439'],
+ ]
+ )
+ );
+
+ self::assertCount(1, $validationErrors);
+ self::assertInstanceOf(Message::class, $validationErrors[0]->getTranslatableMessage());
+ self::assertSame(
+ "\$limitationValue->limitationValues[0] => Invalid SiteAccess value '2339567439'",
+ (string) $validationErrors[0]->getTranslatableMessage()
+ );
+ }
+
/**
* @depends testConstruct
*
diff --git a/tests/lib/MVC/Symfony/Translation/BaseMessageExtractorPhpFileVisitorTestCase.php b/tests/lib/MVC/Symfony/Translation/BaseMessageExtractorPhpFileVisitorTestCase.php
index f2543a0fc0..0f5ac0b1f4 100644
--- a/tests/lib/MVC/Symfony/Translation/BaseMessageExtractorPhpFileVisitorTestCase.php
+++ b/tests/lib/MVC/Symfony/Translation/BaseMessageExtractorPhpFileVisitorTestCase.php
@@ -9,6 +9,10 @@
namespace Ibexa\Tests\Core\MVC\Symfony\Translation;
use Doctrine\Common\Annotations\DocParser;
+use Ibexa\Core\MVC\Symfony\Translation\Annotation\Domain;
+use JMS\TranslationBundle\Annotation\Desc;
+use JMS\TranslationBundle\Annotation\Ignore;
+use JMS\TranslationBundle\Annotation\Meaning;
use JMS\TranslationBundle\Model\MessageCatalogue;
use JMS\TranslationBundle\Translation\Extractor\FileVisitorInterface;
use JMS\TranslationBundle\Translation\FileSourceFactory;
@@ -38,6 +42,13 @@ abstract protected function buildVisitor(
protected function setUp(): void
{
$docParser = new DocParser();
+ $docParser->setImports([
+ 'desc' => Desc::class,
+ 'domain' => Domain::class,
+ 'ignore' => Ignore::class,
+ 'meaning' => Meaning::class,
+ ]);
+
$fileSourceFactory = new FileSourceFactory(self::FIXTURES_DIR);
$factory = new ParserFactory();
$this->phpParser = $factory->createForHostVersion();
diff --git a/tests/lib/MVC/Symfony/Translation/ValidationErrorFileVisitorTest.php b/tests/lib/MVC/Symfony/Translation/ValidationErrorFileVisitorTest.php
index 9dbc44e9a3..504417c4dd 100644
--- a/tests/lib/MVC/Symfony/Translation/ValidationErrorFileVisitorTest.php
+++ b/tests/lib/MVC/Symfony/Translation/ValidationErrorFileVisitorTest.php
@@ -11,8 +11,10 @@
use Doctrine\Common\Annotations\DocParser;
use Ibexa\Core\MVC\Symfony\Translation\ValidationErrorFileVisitor;
use JMS\TranslationBundle\Model\Message;
+use JMS\TranslationBundle\Model\MessageCatalogue;
use JMS\TranslationBundle\Translation\Extractor\FileVisitorInterface;
use JMS\TranslationBundle\Translation\FileSourceFactory;
+use SplFileInfo;
/**
* @covers \Ibexa\Core\MVC\Symfony\Translation\ValidationErrorFileVisitor
@@ -30,10 +32,50 @@ public static function getDataForTestExtractTranslation(): iterable
new Message('error_1.singular_only', 'ibexa_repository_exceptions'),
new Message('error_2.singular', 'ibexa_repository_exceptions'),
new Message('error_2.plural', 'ibexa_repository_exceptions'),
+ new Message('error_3.with_desc', 'ibexa_repository_exceptions'),
+ new Message('error_4.validators_domain', 'validators'),
],
];
}
+ public function testExtractTranslationKeepsDescriptionForClassConstant(): void
+ {
+ $messageCatalogue = new MessageCatalogue();
+ $file = self::FIXTURES_DIR . 'ValidationErrorUsageStub.php';
+ $fileInfo = new SplFileInfo($file);
+
+ $ast = $this->getASTFromFile($file);
+ $this->visitor->visitPhpFile(
+ $fileInfo,
+ $messageCatalogue,
+ $ast
+ );
+
+ $message = $messageCatalogue->get('error_3.with_desc', 'ibexa_repository_exceptions');
+
+ self::assertNotNull($message);
+ self::assertSame('Validation error extracted from class const', $message->getDesc());
+ }
+
+ public function testExtractTranslationAllowsDomainOverride(): void
+ {
+ $messageCatalogue = new MessageCatalogue();
+ $file = self::FIXTURES_DIR . 'ValidationErrorUsageStub.php';
+ $fileInfo = new SplFileInfo($file);
+
+ $ast = $this->getASTFromFile($file);
+ $this->visitor->visitPhpFile(
+ $fileInfo,
+ $messageCatalogue,
+ $ast
+ );
+
+ $message = $messageCatalogue->get('error_4.validators_domain', 'validators');
+
+ self::assertNotNull($message);
+ self::assertSame('Validation error extracted into validators domain', $message->getDesc());
+ }
+
protected function buildVisitor(DocParser $docParser, FileSourceFactory $fileSourceFactory): FileVisitorInterface
{
return new ValidationErrorFileVisitor(
diff --git a/tests/lib/MVC/Symfony/Translation/fixtures/ValidationErrorUsageStub.php b/tests/lib/MVC/Symfony/Translation/fixtures/ValidationErrorUsageStub.php
index 5802f16f00..cd93c4146f 100644
--- a/tests/lib/MVC/Symfony/Translation/fixtures/ValidationErrorUsageStub.php
+++ b/tests/lib/MVC/Symfony/Translation/fixtures/ValidationErrorUsageStub.php
@@ -9,12 +9,19 @@
namespace Ibexa\Tests\Core\MVC\Symfony\Translation\fixtures;
use Ibexa\Core\FieldType\ValidationError;
+use Ibexa\Core\MVC\Symfony\Translation\Annotation\Domain;
+use JMS\TranslationBundle\Annotation\Desc;
/**
* @see \Ibexa\Tests\Core\MVC\Symfony\Translation\ValidationErrorFileVisitorTest
*/
final class ValidationErrorUsageStub
{
+ private const SINGULAR = 'error_2.singular';
+ private const PLURAL = 'error_2.plural';
+ private const WITH_DESC = 'error_3.with_desc';
+ private const WITH_VALIDATORS_DOMAIN = 'error_4.validators_domain';
+
/**
* @return \Ibexa\Contracts\Core\FieldType\ValidationError[]
*/
@@ -23,5 +30,17 @@ public function getErrors(): iterable
yield new ValidationError('error_1.singular_only');
yield new ValidationError('error_2.singular', 'error_2.plural');
+
+ yield new ValidationError(static::SINGULAR, self::PLURAL);
+
+ yield new ValidationError(
+ /** @Desc("Validation error extracted from class const") */
+ self::WITH_DESC
+ );
+
+ yield new ValidationError(
+ /** @Desc("Validation error extracted into validators domain") @Domain("validators") */
+ self::WITH_VALIDATORS_DOMAIN
+ );
}
}
diff --git a/tests/lib/Repository/ContentTypeServiceValidationErrorTest.php b/tests/lib/Repository/ContentTypeServiceValidationErrorTest.php
new file mode 100644
index 0000000000..5d82f1a9b9
--- /dev/null
+++ b/tests/lib/Repository/ContentTypeServiceValidationErrorTest.php
@@ -0,0 +1,68 @@
+createMock(RepositoryInterface::class),
+ $this->createMock(Handler::class),
+ $this->createMock(UserHandler::class),
+ $this->createMock(ContentDomainMapper::class),
+ $this->createMock(ContentTypeDomainMapper::class),
+ $this->createMock(FieldTypeRegistry::class),
+ $this->createMock(PermissionResolver::class)
+ ) extends ContentTypeService {
+ /**
+ * @return array<\Ibexa\Contracts\Core\FieldType\ValidationError>
+ */
+ public function exposeValidateFieldDefinitionCreateStruct(
+ FieldDefinitionCreateStruct $fieldDefinitionCreateStruct,
+ SPIFieldType $fieldType
+ ): array {
+ return $this->validateFieldDefinitionCreateStruct($fieldDefinitionCreateStruct, $fieldType);
+ }
+ };
+
+ $fieldDefinitionCreateStruct = new FieldDefinitionCreateStruct();
+ $fieldDefinitionCreateStruct->fieldTypeIdentifier = 'ibexa_non_searchable';
+ $fieldDefinitionCreateStruct->isSearchable = true;
+ $fieldDefinitionCreateStruct->validatorConfiguration = [];
+ $fieldDefinitionCreateStruct->fieldSettings = [];
+
+ $fieldType = $this->createMock(SPIFieldType::class);
+ $fieldType->method('isSearchable')->willReturn(false);
+ $fieldType->method('validateValidatorConfiguration')->willReturn([]);
+ $fieldType->method('validateFieldSettings')->willReturn([]);
+
+ $validationErrors = $service->exposeValidateFieldDefinitionCreateStruct($fieldDefinitionCreateStruct, $fieldType);
+
+ self::assertCount(1, $validationErrors);
+ self::assertInstanceOf(Message::class, $validationErrors[0]->getTranslatableMessage());
+ self::assertSame(
+ 'Field type ibexa_non_searchable is not searchable',
+ (string) $validationErrors[0]->getTranslatableMessage()
+ );
+ }
+}
diff --git a/tests/lib/Repository/Mapper/ContentTypeDomainMapperValidationErrorTest.php b/tests/lib/Repository/Mapper/ContentTypeDomainMapperValidationErrorTest.php
new file mode 100644
index 0000000000..6b810160d0
--- /dev/null
+++ b/tests/lib/Repository/Mapper/ContentTypeDomainMapperValidationErrorTest.php
@@ -0,0 +1,80 @@
+createMock(SPIFieldType::class);
+ $fieldType->method('isSearchable')->willReturn(false);
+ $fieldType->method('validateValidatorConfiguration')->willReturn([]);
+ $fieldType->method('validateFieldSettings')->willReturn([]);
+
+ $fieldTypeRegistry = $this->createMock(FieldTypeRegistry::class);
+ $fieldTypeRegistry->method('getFieldType')->with('ibexa_non_searchable')->willReturn($fieldType);
+
+ $mapper = new ContentTypeDomainMapper(
+ $this->createMock(SPITypeHandler::class),
+ $this->createMock(SPILanguageHandler::class),
+ $fieldTypeRegistry
+ );
+
+ $fieldDefinitionUpdateStruct = new FieldDefinitionUpdateStruct();
+ $fieldDefinitionUpdateStruct->isSearchable = true;
+ $fieldDefinitionUpdateStruct->validatorConfiguration = [];
+ $fieldDefinitionUpdateStruct->fieldSettings = [];
+
+ $fieldDefinition = new FieldDefinition([
+ 'id' => 1,
+ 'identifier' => 'test_field',
+ 'fieldTypeIdentifier' => 'ibexa_non_searchable',
+ 'fieldGroup' => 'content',
+ 'position' => 0,
+ 'isTranslatable' => false,
+ 'isRequired' => false,
+ 'isThumbnail' => false,
+ 'isInfoCollector' => false,
+ 'isSearchable' => false,
+ 'mainLanguageCode' => 'eng-GB',
+ 'names' => ['eng-GB' => 'Test field'],
+ 'descriptions' => ['eng-GB' => ''],
+ 'validatorConfiguration' => [],
+ 'fieldSettings' => [],
+ 'defaultValue' => null,
+ 'prioritizedLanguages' => [],
+ ]);
+
+ try {
+ $mapper->buildSPIFieldDefinitionFromUpdateStruct($fieldDefinitionUpdateStruct, $fieldDefinition, 'eng-GB');
+ self::fail('Expected ContentTypeFieldDefinitionValidationException was not thrown.');
+ } catch (ContentTypeFieldDefinitionValidationException $exception) {
+ $errors = $exception->getFieldErrors();
+
+ self::assertArrayHasKey('test_field', $errors);
+ self::assertCount(1, $errors['test_field']);
+ self::assertInstanceOf(Message::class, $errors['test_field'][0]->getTranslatableMessage());
+ self::assertSame(
+ 'Field type ibexa_non_searchable is not searchable',
+ (string) $errors['test_field'][0]->getTranslatableMessage()
+ );
+ }
+ }
+}