From 5e71eb7d7e4c973f4115a6eb54701df3c64422de Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Mon, 13 Apr 2026 11:32:58 +0200 Subject: [PATCH 01/16] IBX-11441: Provide proper index limit for the `StringField` for search engines --- phpstan-baseline.neon | 12 -- src/lib/FieldType/TextBlock/SearchField.php | 14 +-- .../Common/FieldValueMapper/StringMapper.php | 6 +- .../FieldValueMapper/StringMapperTest.php | 105 ++++++++++++++++++ 4 files changed, 111 insertions(+), 26 deletions(-) create mode 100644 tests/lib/Search/Common/FieldValueMapper/StringMapperTest.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 9bee1e5f4b..673304e66b 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -11946,12 +11946,6 @@ parameters: count: 1 path: src/lib/FieldType/StorageGateway.php - - - message: '#^Parameter \#1 \$string of method Ibexa\\Core\\FieldType\\TextBlock\\SearchField\:\:extractShortText\(\) expects string, array\|bool\|float\|int\|string\|null given\.$#' - identifier: argument.type - count: 1 - path: src/lib/FieldType/TextBlock/SearchField.php - - message: '#^Access to an undefined property Ibexa\\Contracts\\Core\\FieldType\\Value\:\:\$text\.$#' identifier: property.notFound @@ -26173,12 +26167,6 @@ parameters: count: 1 path: src/lib/Search/Common/FieldValueMapper/MultipleStringMapper.php - - - message: '#^Method Ibexa\\Core\\Search\\Common\\FieldValueMapper\\StringMapper\:\:convert\(\) should return string but returns string\|null\.$#' - identifier: return.type - count: 1 - path: src/lib/Search/Common/FieldValueMapper/StringMapper.php - - message: '#^Method Ibexa\\Core\\Search\\Common\\IncrementalIndexer\:\:createSearchIndex\(\) has no return type specified\.$#' identifier: missingType.return diff --git a/src/lib/FieldType/TextBlock/SearchField.php b/src/lib/FieldType/TextBlock/SearchField.php index 2bfde7dc89..89fa2dbf4e 100644 --- a/src/lib/FieldType/TextBlock/SearchField.php +++ b/src/lib/FieldType/TextBlock/SearchField.php @@ -21,7 +21,7 @@ public function getIndexData(Field $field, FieldDefinition $fieldDefinition) return [ new Search\Field( 'value', - $this->extractShortText($field->value->data), + $field->value->data, new Search\FieldType\StringField() ), new Search\Field( @@ -32,18 +32,6 @@ public function getIndexData(Field $field, FieldDefinition $fieldDefinition) ]; } - /** - * Extracts short snippet of the given $string. - * - * @param string $string - * - * @return string - */ - private function extractShortText($string) - { - return mb_substr(strtok(trim((string)$string), "\r\n"), 0, 255); - } - public function getIndexDefinition() { return [ diff --git a/src/lib/Search/Common/FieldValueMapper/StringMapper.php b/src/lib/Search/Common/FieldValueMapper/StringMapper.php index 1050b068ed..4a8bc344ce 100644 --- a/src/lib/Search/Common/FieldValueMapper/StringMapper.php +++ b/src/lib/Search/Common/FieldValueMapper/StringMapper.php @@ -17,6 +17,7 @@ class StringMapper extends FieldValueMapper { public const REPLACE_WITH_SPACE_PATTERN = '([\x09\x0B\x0C]+)'; public const REMOVE_PATTERN = '([\x00-\x08\x0E-\x1F]+)'; + public const MAX_TERM_LENGTH = 32766; public function canMap(Field $field): bool { @@ -43,11 +44,14 @@ protected function convert($value): string ); // Remove non-printable characters. - return preg_replace( + $value = preg_replace( self::REMOVE_PATTERN, '', (string)$value ); + + // Enforce Lucene's bytes MAX_TERM_LENGTH to avoid silent indexing failures + return mb_strcut((string)$value, 0, self::MAX_TERM_LENGTH); } } diff --git a/tests/lib/Search/Common/FieldValueMapper/StringMapperTest.php b/tests/lib/Search/Common/FieldValueMapper/StringMapperTest.php new file mode 100644 index 0000000000..eddd165ddc --- /dev/null +++ b/tests/lib/Search/Common/FieldValueMapper/StringMapperTest.php @@ -0,0 +1,105 @@ +mapper = new StringMapper(); + } + + public function testCanMap(): void + { + $field = $this->createFieldWithValue('hello', new StringField()); + + self::assertTrue($this->mapper->canMap($field)); + } + + public function testMapsPlainString(): void + { + $field = $this->createFieldWithValue('hello world', new StringField()); + + self::assertSame('hello world', $this->mapper->map($field)); + } + + public function testStripsNonPrintableCharacters(): void + { + $field = $this->createFieldWithValue("hello\x01\x02world", new StringField()); + + self::assertSame('helloworld', $this->mapper->map($field)); + } + + public function testReplacesTabAndVerticalWhitespaceWithSpace(): void + { + $field = $this->createFieldWithValue("hello\x09world\x0Bfoo", new StringField()); + + self::assertSame('hello world foo', $this->mapper->map($field)); + } + + public function testTruncatesToMaxTermLength(): void + { + $longValue = str_repeat('a', StringMapper::MAX_TERM_LENGTH + 100); + $field = $this->createFieldWithValue($longValue, new StringField()); + $result = $this->mapper->map($field); + + self::assertSame(StringMapper::MAX_TERM_LENGTH, strlen($result)); + } + + public function testTruncatesMultibyteStringAtCharacterBoundary(): void + { + // Each UTF-8 character here is 3 bytes (€ = E2 82 AC). + // Fill up to just past the limit so truncation must happen on a char boundary. + $char = '€'; + $charBytes = strlen($char); + + self::assertSame(3, $charBytes); + + $count = (int) ceil((StringMapper::MAX_TERM_LENGTH + $charBytes) / $charBytes); + $longValue = str_repeat($char, $count); + + $field = $this->createFieldWithValue($longValue, new StringField()); + $result = $this->mapper->map($field); + + self::assertSame(StringMapper::MAX_TERM_LENGTH, strlen($result)); + // Result must be valid UTF-8 (no split mid-character). + self::assertSame(1, preg_match('//u', $result)); + } + + public function testValueWithinLimitIsNotTruncated(): void + { + $value = str_repeat('a', StringMapper::MAX_TERM_LENGTH); + $field = $this->createFieldWithValue($value, new StringField()); + + self::assertSame($value, $this->mapper->map($field)); + } + + private function createFieldWithValue(string $value, StringField $type): Field + { + $field = $this->createMock(Field::class); + $field + ->method('getValue') + ->willReturn($value); + $field + ->method('getType') + ->willReturn($type); + + return $field; + } +} From 0c91a745cadb18d85149488d2d42f8f2bc38cf1a Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Mon, 13 Apr 2026 13:15:37 +0200 Subject: [PATCH 02/16] IBX-11441: Baseline --- phpstan-baseline-7.4.neon | 6 ------ 1 file changed, 6 deletions(-) diff --git a/phpstan-baseline-7.4.neon b/phpstan-baseline-7.4.neon index d91c9ee853..c970170901 100644 --- a/phpstan-baseline-7.4.neon +++ b/phpstan-baseline-7.4.neon @@ -150,12 +150,6 @@ parameters: count: 1 path: src/lib/FieldType/Selection/SearchField.php - - - message: '#^Parameter \#1 \$str of function mb_substr expects string, string\|false given\.$#' - identifier: argument.type - count: 1 - path: src/lib/FieldType/TextBlock/SearchField.php - - message: '#^Parameter \#1 \$str of function mb_substr expects string, string\|false given\.$#' identifier: argument.type From 5cd3fe72b66e028de2bbdb2cba356a12796a4feb Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Mon, 13 Apr 2026 13:18:46 +0200 Subject: [PATCH 03/16] IBX-11441: Baseline --- phpstan-baseline-gte-8.0.neon | 6 ------ 1 file changed, 6 deletions(-) diff --git a/phpstan-baseline-gte-8.0.neon b/phpstan-baseline-gte-8.0.neon index 92badbf76c..874cf8f42c 100644 --- a/phpstan-baseline-gte-8.0.neon +++ b/phpstan-baseline-gte-8.0.neon @@ -162,12 +162,6 @@ parameters: count: 1 path: src/lib/FieldType/Selection/SearchField.php - - - message: '#^Parameter \#1 \$string of function mb_substr expects string, string\|false given\.$#' - identifier: argument.type - count: 1 - path: src/lib/FieldType/TextBlock/SearchField.php - - message: '#^Parameter \#1 \$string of function mb_substr expects string, string\|false given\.$#' identifier: argument.type From d24ad2576aaf991f76bedefb0fb77835e57eefa3 Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Mon, 13 Apr 2026 20:32:26 +0200 Subject: [PATCH 04/16] IBX-11441: Update --- src/lib/FieldType/TextBlock/SearchField.php | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/lib/FieldType/TextBlock/SearchField.php b/src/lib/FieldType/TextBlock/SearchField.php index 89fa2dbf4e..4b2f2b2e43 100644 --- a/src/lib/FieldType/TextBlock/SearchField.php +++ b/src/lib/FieldType/TextBlock/SearchField.php @@ -21,7 +21,7 @@ public function getIndexData(Field $field, FieldDefinition $fieldDefinition) return [ new Search\Field( 'value', - $field->value->data, + $this->extractText($field->value->data), new Search\FieldType\StringField() ), new Search\Field( @@ -32,6 +32,20 @@ public function getIndexData(Field $field, FieldDefinition $fieldDefinition) ]; } + /** + * @param mixed $string + */ + private function extractText($string): string + { + if (!is_string($string)) { + return ''; + } + + $tokenizedString = strtok(trim($string), "\r\n"); + + return $tokenizedString ?: ''; + } + public function getIndexDefinition() { return [ From 7812076516043004635457d1e5da14dcb6d63122 Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Mon, 13 Apr 2026 20:42:16 +0200 Subject: [PATCH 05/16] IBX-11441: PHPStan # Conflicts: # phpstan-baseline.neon --- .../ExceptionMessageTemplateFileVisitor.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/lib/MVC/Symfony/Translation/ExceptionMessageTemplateFileVisitor.php b/src/lib/MVC/Symfony/Translation/ExceptionMessageTemplateFileVisitor.php index c975d605b3..56df97034c 100644 --- a/src/lib/MVC/Symfony/Translation/ExceptionMessageTemplateFileVisitor.php +++ b/src/lib/MVC/Symfony/Translation/ExceptionMessageTemplateFileVisitor.php @@ -65,12 +65,21 @@ public function enterNode(Node $node): void $ignore = $this->isIgnore($node); + if (!$node->args[0] instanceof Node\Arg) { + return; + } + if (!$node->args[0]->value instanceof String_) { if ($ignore) { return; } - $message = sprintf('Can only extract the translation id from a scalar string, but got "%s". Please 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". Please 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() + ); $this->logger->error($message); From 0219ee732afc25066419a8e69d104b240f777d52 Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Tue, 14 Apr 2026 14:15:44 +0200 Subject: [PATCH 06/16] IBX-11441: Update --- src/lib/FieldType/TextBlock/SearchField.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/lib/FieldType/TextBlock/SearchField.php b/src/lib/FieldType/TextBlock/SearchField.php index 4b2f2b2e43..849e77b58b 100644 --- a/src/lib/FieldType/TextBlock/SearchField.php +++ b/src/lib/FieldType/TextBlock/SearchField.php @@ -41,9 +41,7 @@ private function extractText($string): string return ''; } - $tokenizedString = strtok(trim($string), "\r\n"); - - return $tokenizedString ?: ''; + return str_replace(["\r\n", "\r", "\n"], ' ', trim($string)); } public function getIndexDefinition() From da9661fcd4e72ce54f6676f37b5f13193076ddde Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Tue, 14 Apr 2026 14:23:33 +0200 Subject: [PATCH 07/16] IBX-11441: Added logger --- phpstan-baseline.neon | 42 ------------------- .../search_engines/field_value_mappers.yml | 2 + .../Common/FieldValueMapper/StringMapper.php | 25 ++++++++++- .../RemoteIdentifierMapperTest.php | 5 ++- .../FieldValueMapper/StringMapperTest.php | 5 ++- 5 files changed, 34 insertions(+), 45 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 673304e66b..4cdbcded3b 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -15822,12 +15822,6 @@ parameters: count: 1 path: src/lib/MVC/Symfony/Translation/ExceptionMessageTemplateFileVisitor.php - - - message: '#^Access to an undefined property PhpParser\\Node\\Arg\|PhpParser\\Node\\VariadicPlaceholder\:\:\$value\.$#' - identifier: property.notFound - count: 1 - path: src/lib/MVC/Symfony/Translation/ExceptionMessageTemplateFileVisitor.php - - message: '#^Cannot call method error\(\) on Psr\\Log\\LoggerInterface\|null\.$#' identifier: method.nonObject @@ -32029,12 +32023,6 @@ parameters: count: 1 path: tests/integration/Core/Repository/BaseTest.php - - - message: '#^Call to method commit\(\) on an unknown class Ibexa\\Solr\\Handler\.$#' - identifier: class.notFound - count: 1 - path: tests/integration/Core/Repository/BaseTest.php - - message: '#^Method Ibexa\\Tests\\Integration\\Core\\Repository\\BaseTest\:\:assertPropertiesCorrect\(\) has no return type specified\.$#' identifier: missingType.return @@ -32155,12 +32143,6 @@ parameters: count: 1 path: tests/integration/Core/Repository/BaseTest.php - - - message: '#^PHPDoc tag @var for variable \$searchHandler contains unknown class Ibexa\\Solr\\Handler\.$#' - identifier: class.notFound - count: 1 - path: tests/integration/Core/Repository/BaseTest.php - - message: '#^Property Ibexa\\Contracts\\Core\\Repository\\Values\\Content\\ContentCreateStruct\:\:\$remoteId \(string\) does not accept string\|null\.$#' identifier: assign.propertyType @@ -42436,12 +42418,6 @@ parameters: count: 1 path: tests/integration/Core/Repository/PermissionResolverTest.php - - - message: '#^Class Ibexa\\Tests\\Solr\\SetupFactory\\LegacySetupFactory not found\.$#' - identifier: class.notFound - count: 1 - path: tests/integration/Core/Repository/Regression/EZP20018LanguageTest.php - - message: '#^Method Ibexa\\Tests\\Integration\\Core\\Repository\\Regression\\EZP20018LanguageTest\:\:testSearchOnNotExistingLanguageGivesException\(\) has no return type specified\.$#' identifier: missingType.return @@ -44620,12 +44596,6 @@ parameters: count: 1 path: tests/integration/Core/Repository/SearchServiceLocationTest.php - - - message: '#^Class Ibexa\\Tests\\Solr\\SetupFactory\\LegacySetupFactory not found\.$#' - identifier: class.notFound - count: 1 - path: tests/integration/Core/Repository/SearchServiceLocationTest.php - - message: '#^Method Ibexa\\Tests\\Integration\\Core\\Repository\\SearchServiceLocationTest\:\:assertQueryFixture\(\) has no return type specified\.$#' identifier: missingType.return @@ -44836,12 +44806,6 @@ parameters: count: 2 path: tests/integration/Core/Repository/SearchServiceTest.php - - - message: '#^Class Ibexa\\Tests\\Solr\\SetupFactory\\LegacySetupFactory not found\.$#' - identifier: class.notFound - count: 5 - path: tests/integration/Core/Repository/SearchServiceTest.php - - message: '#^Instanceof between Ibexa\\Contracts\\Core\\Repository\\Values\\Content\\Query and Ibexa\\Contracts\\Core\\Repository\\Values\\Content\\Query will always evaluate to true\.$#' identifier: instanceof.alwaysTrue @@ -45814,12 +45778,6 @@ parameters: count: 3 path: tests/integration/Core/Repository/SearchServiceTest.php - - - message: '#^Class Ibexa\\Tests\\Solr\\SetupFactory\\LegacySetupFactory not found\.$#' - identifier: class.notFound - count: 3 - path: tests/integration/Core/Repository/SearchServiceTranslationLanguageFallbackTest.php - - message: '#^Method Ibexa\\Tests\\Integration\\Core\\Repository\\SearchServiceTranslationLanguageFallbackTest\:\:assertIndexName\(\) has parameter \$indexMap with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue diff --git a/src/lib/Resources/settings/search_engines/field_value_mappers.yml b/src/lib/Resources/settings/search_engines/field_value_mappers.yml index eb80c572db..5edebd8194 100644 --- a/src/lib/Resources/settings/search_engines/field_value_mappers.yml +++ b/src/lib/Resources/settings/search_engines/field_value_mappers.yml @@ -62,6 +62,8 @@ services: - { name: ibexa.search.common.field_value.mapper, maps: Ibexa\Contracts\Core\Search\FieldType\PriceField } Ibexa\Core\Search\Common\FieldValueMapper\StringMapper: + arguments: + $logger: '@logger' class: Ibexa\Core\Search\Common\FieldValueMapper\StringMapper tags: - { name: ibexa.search.common.field_value.mapper, maps: Ibexa\Contracts\Core\Search\FieldType\StringField } diff --git a/src/lib/Search/Common/FieldValueMapper/StringMapper.php b/src/lib/Search/Common/FieldValueMapper/StringMapper.php index 4a8bc344ce..25699993b2 100644 --- a/src/lib/Search/Common/FieldValueMapper/StringMapper.php +++ b/src/lib/Search/Common/FieldValueMapper/StringMapper.php @@ -9,12 +9,21 @@ use Ibexa\Contracts\Core\Search\Field; use Ibexa\Contracts\Core\Search\FieldType; use Ibexa\Core\Search\Common\FieldValueMapper; +use Psr\Log\LoggerInterface; /** * Common string field value mapper implementation. */ class StringMapper extends FieldValueMapper { + private LoggerInterface $logger; + + public function __construct( + LoggerInterface $logger + ) { + $this->logger = $logger; + } + public const REPLACE_WITH_SPACE_PATTERN = '([\x09\x0B\x0C]+)'; public const REMOVE_PATTERN = '([\x00-\x08\x0E-\x1F]+)'; public const MAX_TERM_LENGTH = 32766; @@ -51,7 +60,21 @@ protected function convert($value): string ); // Enforce Lucene's bytes MAX_TERM_LENGTH to avoid silent indexing failures - return mb_strcut((string)$value, 0, self::MAX_TERM_LENGTH); + $value = (string)$value; + $truncated = mb_strcut($value, 0, self::MAX_TERM_LENGTH); + + if (strlen($truncated) < strlen($value)) { + $this->logger->warning( + sprintf( + 'String field value was truncated from %d to %d bytes (max term length: %d).', + strlen($value), + strlen($truncated), + self::MAX_TERM_LENGTH + ) + ); + } + + return $truncated; } } diff --git a/tests/lib/Search/Common/FieldValueMapper/RemoteIdentifierMapperTest.php b/tests/lib/Search/Common/FieldValueMapper/RemoteIdentifierMapperTest.php index 2d09349b83..da2a37357e 100644 --- a/tests/lib/Search/Common/FieldValueMapper/RemoteIdentifierMapperTest.php +++ b/tests/lib/Search/Common/FieldValueMapper/RemoteIdentifierMapperTest.php @@ -15,6 +15,7 @@ use Ibexa\Contracts\Core\Search\FieldType\StringField; use Ibexa\Core\Search\Common\FieldValueMapper\RemoteIdentifierMapper; use Ibexa\Tests\Core\Search\TestCase; +use Psr\Log\LoggerInterface; /** * @covers \Ibexa\Core\Search\Common\FieldValueMapper\RemoteIdentifierMapper @@ -26,7 +27,9 @@ final class RemoteIdentifierMapperTest extends TestCase protected function setUp(): void { - $this->mapper = new RemoteIdentifierMapper(); + $this->mapper = new RemoteIdentifierMapper( + $this->createMock(LoggerInterface::class) + ); } /** diff --git a/tests/lib/Search/Common/FieldValueMapper/StringMapperTest.php b/tests/lib/Search/Common/FieldValueMapper/StringMapperTest.php index eddd165ddc..9be3058f2b 100644 --- a/tests/lib/Search/Common/FieldValueMapper/StringMapperTest.php +++ b/tests/lib/Search/Common/FieldValueMapper/StringMapperTest.php @@ -12,6 +12,7 @@ use Ibexa\Contracts\Core\Search\FieldType\StringField; use Ibexa\Core\Search\Common\FieldValueMapper\StringMapper; use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; /** * @covers \Ibexa\Core\Search\Common\FieldValueMapper\StringMapper @@ -22,7 +23,9 @@ final class StringMapperTest extends TestCase protected function setUp(): void { - $this->mapper = new StringMapper(); + $this->mapper = new StringMapper( + $this->createMock(LoggerInterface::class) + ); } public function testCanMap(): void From 5c0427dc06c2bc8e36b31b5a1dac28cac7de0229 Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Tue, 14 Apr 2026 14:44:40 +0200 Subject: [PATCH 08/16] IBX-11441: Rollbacked baseline --- phpstan-baseline.neon | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 4cdbcded3b..9cf0187ef3 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -32023,6 +32023,12 @@ parameters: count: 1 path: tests/integration/Core/Repository/BaseTest.php + - + message: '#^Call to method commit\(\) on an unknown class Ibexa\\Solr\\Handler\.$#' + identifier: class.notFound + count: 1 + path: tests/integration/Core/Repository/BaseTest.php + - message: '#^Method Ibexa\\Tests\\Integration\\Core\\Repository\\BaseTest\:\:assertPropertiesCorrect\(\) has no return type specified\.$#' identifier: missingType.return @@ -32143,6 +32149,12 @@ parameters: count: 1 path: tests/integration/Core/Repository/BaseTest.php + - + message: '#^PHPDoc tag @var for variable \$searchHandler contains unknown class Ibexa\\Solr\\Handler\.$#' + identifier: class.notFound + count: 1 + path: tests/integration/Core/Repository/BaseTest.php + - message: '#^Property Ibexa\\Contracts\\Core\\Repository\\Values\\Content\\ContentCreateStruct\:\:\$remoteId \(string\) does not accept string\|null\.$#' identifier: assign.propertyType @@ -42418,6 +42430,12 @@ parameters: count: 1 path: tests/integration/Core/Repository/PermissionResolverTest.php + - + message: '#^Class Ibexa\\Tests\\Solr\\SetupFactory\\LegacySetupFactory not found\.$#' + identifier: class.notFound + count: 1 + path: tests/integration/Core/Repository/Regression/EZP20018LanguageTest.php + - message: '#^Method Ibexa\\Tests\\Integration\\Core\\Repository\\Regression\\EZP20018LanguageTest\:\:testSearchOnNotExistingLanguageGivesException\(\) has no return type specified\.$#' identifier: missingType.return @@ -44596,6 +44614,12 @@ parameters: count: 1 path: tests/integration/Core/Repository/SearchServiceLocationTest.php + - + message: '#^Class Ibexa\\Tests\\Solr\\SetupFactory\\LegacySetupFactory not found\.$#' + identifier: class.notFound + count: 1 + path: tests/integration/Core/Repository/SearchServiceLocationTest.php + - message: '#^Method Ibexa\\Tests\\Integration\\Core\\Repository\\SearchServiceLocationTest\:\:assertQueryFixture\(\) has no return type specified\.$#' identifier: missingType.return @@ -44806,6 +44830,12 @@ parameters: count: 2 path: tests/integration/Core/Repository/SearchServiceTest.php + - + message: '#^Class Ibexa\\Tests\\Solr\\SetupFactory\\LegacySetupFactory not found\.$#' + identifier: class.notFound + count: 5 + path: tests/integration/Core/Repository/SearchServiceTest.php + - message: '#^Instanceof between Ibexa\\Contracts\\Core\\Repository\\Values\\Content\\Query and Ibexa\\Contracts\\Core\\Repository\\Values\\Content\\Query will always evaluate to true\.$#' identifier: instanceof.alwaysTrue @@ -45778,6 +45808,12 @@ parameters: count: 3 path: tests/integration/Core/Repository/SearchServiceTest.php + - + message: '#^Class Ibexa\\Tests\\Solr\\SetupFactory\\LegacySetupFactory not found\.$#' + identifier: class.notFound + count: 3 + path: tests/integration/Core/Repository/SearchServiceTranslationLanguageFallbackTest.php + - message: '#^Method Ibexa\\Tests\\Integration\\Core\\Repository\\SearchServiceTranslationLanguageFallbackTest\:\:assertIndexName\(\) has parameter \$indexMap with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue From 59cd1ddfcfc7be7e53af067ae01f239d620c3fbb Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Tue, 14 Apr 2026 15:27:54 +0200 Subject: [PATCH 09/16] IBX-11441: Fixed logger --- .../settings/search_engines/field_value_mappers.yml | 4 ++-- .../Search/Common/FieldValueMapper/StringMapper.php | 11 +++++++---- .../Common/FieldValueMapper/StringMapperTest.php | 5 +---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/lib/Resources/settings/search_engines/field_value_mappers.yml b/src/lib/Resources/settings/search_engines/field_value_mappers.yml index 5edebd8194..c564cfd75e 100644 --- a/src/lib/Resources/settings/search_engines/field_value_mappers.yml +++ b/src/lib/Resources/settings/search_engines/field_value_mappers.yml @@ -62,9 +62,9 @@ services: - { name: ibexa.search.common.field_value.mapper, maps: Ibexa\Contracts\Core\Search\FieldType\PriceField } Ibexa\Core\Search\Common\FieldValueMapper\StringMapper: - arguments: - $logger: '@logger' class: Ibexa\Core\Search\Common\FieldValueMapper\StringMapper + calls: + - [setLogger, ['@logger']] tags: - { name: ibexa.search.common.field_value.mapper, maps: Ibexa\Contracts\Core\Search\FieldType\StringField } diff --git a/src/lib/Search/Common/FieldValueMapper/StringMapper.php b/src/lib/Search/Common/FieldValueMapper/StringMapper.php index 25699993b2..94482065b7 100644 --- a/src/lib/Search/Common/FieldValueMapper/StringMapper.php +++ b/src/lib/Search/Common/FieldValueMapper/StringMapper.php @@ -9,19 +9,22 @@ use Ibexa\Contracts\Core\Search\Field; use Ibexa\Contracts\Core\Search\FieldType; use Ibexa\Core\Search\Common\FieldValueMapper; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; /** * Common string field value mapper implementation. */ -class StringMapper extends FieldValueMapper +class StringMapper extends FieldValueMapper implements LoggerAwareInterface { - private LoggerInterface $logger; + use LoggerAwareTrait; public function __construct( - LoggerInterface $logger + ?LoggerInterface $logger = null ) { - $this->logger = $logger; + $this->logger = $logger ?? new NullLogger(); } public const REPLACE_WITH_SPACE_PATTERN = '([\x09\x0B\x0C]+)'; diff --git a/tests/lib/Search/Common/FieldValueMapper/StringMapperTest.php b/tests/lib/Search/Common/FieldValueMapper/StringMapperTest.php index 9be3058f2b..eddd165ddc 100644 --- a/tests/lib/Search/Common/FieldValueMapper/StringMapperTest.php +++ b/tests/lib/Search/Common/FieldValueMapper/StringMapperTest.php @@ -12,7 +12,6 @@ use Ibexa\Contracts\Core\Search\FieldType\StringField; use Ibexa\Core\Search\Common\FieldValueMapper\StringMapper; use PHPUnit\Framework\TestCase; -use Psr\Log\LoggerInterface; /** * @covers \Ibexa\Core\Search\Common\FieldValueMapper\StringMapper @@ -23,9 +22,7 @@ final class StringMapperTest extends TestCase protected function setUp(): void { - $this->mapper = new StringMapper( - $this->createMock(LoggerInterface::class) - ); + $this->mapper = new StringMapper(); } public function testCanMap(): void From fee937cb952107d9cbd6a06a8362920041fb0eee Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Wed, 15 Apr 2026 10:27:39 +0200 Subject: [PATCH 10/16] IBX-11441: Removed new lines from indexed string in TextBlock --- src/lib/FieldType/TextBlock/SearchField.php | 4 +++- .../Core/Repository/FieldType/TextBlockIntegrationTest.php | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/lib/FieldType/TextBlock/SearchField.php b/src/lib/FieldType/TextBlock/SearchField.php index 849e77b58b..501bb3699f 100644 --- a/src/lib/FieldType/TextBlock/SearchField.php +++ b/src/lib/FieldType/TextBlock/SearchField.php @@ -41,7 +41,9 @@ private function extractText($string): string return ''; } - return str_replace(["\r\n", "\r", "\n"], ' ', trim($string)); + $lines = explode("\n", str_replace(["\r\n", "\r"], "\n", $string)); + + return implode(' ', array_map('trim', $lines)); } public function getIndexDefinition() diff --git a/tests/integration/Core/Repository/FieldType/TextBlockIntegrationTest.php b/tests/integration/Core/Repository/FieldType/TextBlockIntegrationTest.php index 5c26890faf..a829444a42 100644 --- a/tests/integration/Core/Repository/FieldType/TextBlockIntegrationTest.php +++ b/tests/integration/Core/Repository/FieldType/TextBlockIntegrationTest.php @@ -293,7 +293,7 @@ protected function getValidSearchValueOne() protected function getSearchTargetValueOne() { // ensure case-insensitivity - return strtoupper('caution is the " path to mediocrity'); + return strtoupper('caution is the " path to mediocrity something completely different'); } protected function getValidSearchValueTwo() @@ -304,7 +304,7 @@ protected function getValidSearchValueTwo() protected function getSearchTargetValueTwo() { // ensure case-insensitivity - return strtoupper("truth suffers from ' too much analysis"); + return strtoupper("truth suffers from ' too much analysis hello and goodbye"); } protected function getFullTextIndexedFieldData() From 0c03a41fd1822550d92feedf64cbf9610d866ee9 Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Wed, 15 Apr 2026 12:10:00 +0200 Subject: [PATCH 11/16] IBX-11441: Altered tests --- .../Core/Repository/FieldType/TextBlockIntegrationTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/Core/Repository/FieldType/TextBlockIntegrationTest.php b/tests/integration/Core/Repository/FieldType/TextBlockIntegrationTest.php index a829444a42..ed6d12892f 100644 --- a/tests/integration/Core/Repository/FieldType/TextBlockIntegrationTest.php +++ b/tests/integration/Core/Repository/FieldType/TextBlockIntegrationTest.php @@ -287,7 +287,7 @@ public function providerForTestIsNotEmptyValue() protected function getValidSearchValueOne() { - return 'caution is the " path to mediocrity' . PHP_EOL . 'something completely different'; + return 'caution is the " path to mediocrity something completely different'; } protected function getSearchTargetValueOne() @@ -298,7 +298,7 @@ protected function getSearchTargetValueOne() protected function getValidSearchValueTwo() { - return "truth suffers from ' too much analysis\n hello and goodbye"; + return "truth suffers from ' too much analysis hello and goodbye"; } protected function getSearchTargetValueTwo() From 8e6267942660f7898c3d84f131806074dc0b31f3 Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Tue, 5 May 2026 14:24:08 +0200 Subject: [PATCH 12/16] Used `autoconfigure` --- .../Resources/settings/search_engines/field_value_mappers.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lib/Resources/settings/search_engines/field_value_mappers.yml b/src/lib/Resources/settings/search_engines/field_value_mappers.yml index c564cfd75e..219bf682a4 100644 --- a/src/lib/Resources/settings/search_engines/field_value_mappers.yml +++ b/src/lib/Resources/settings/search_engines/field_value_mappers.yml @@ -62,9 +62,8 @@ services: - { name: ibexa.search.common.field_value.mapper, maps: Ibexa\Contracts\Core\Search\FieldType\PriceField } Ibexa\Core\Search\Common\FieldValueMapper\StringMapper: + autoconfigure: true class: Ibexa\Core\Search\Common\FieldValueMapper\StringMapper - calls: - - [setLogger, ['@logger']] tags: - { name: ibexa.search.common.field_value.mapper, maps: Ibexa\Contracts\Core\Search\FieldType\StringField } From ff888c8fa9c115c9c4becc82567b9fd5068de9c0 Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Tue, 5 May 2026 15:34:24 +0200 Subject: [PATCH 13/16] PHPStan --- .../FieldValue/Converter/DateAndTimeConverter.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/lib/Persistence/Legacy/Content/FieldValue/Converter/DateAndTimeConverter.php b/src/lib/Persistence/Legacy/Content/FieldValue/Converter/DateAndTimeConverter.php index 42a2efc4f1..872fc0aa2b 100644 --- a/src/lib/Persistence/Legacy/Content/FieldValue/Converter/DateAndTimeConverter.php +++ b/src/lib/Persistence/Legacy/Content/FieldValue/Converter/DateAndTimeConverter.php @@ -98,6 +98,11 @@ public function toFieldDefinition(StorageFieldDefinition $storageDef, FieldDefin { $useSeconds = (bool)$storageDef->dataInt2; $dateInterval = $this->getDateIntervalFromXML($storageDef->dataText5); + if ($dateInterval === false) { + $fieldDef->defaultValue->data = null; + + return; + } $fieldDef->fieldTypeConstraints->fieldSettings = new FieldSettings( [ @@ -204,12 +209,12 @@ protected function generateDateIntervalXML(DateInterval $dateInterval) * * @param string $xmlText * - * @return \DateInterval + * @return \DateInterval|false */ protected function getDateIntervalFromXML($xmlText) { if (empty($xmlText)) { - return; + return false; } $xml = new SimpleXMLElement($xmlText); From 1ff64ff7d55e722b70ab16265e34a033ab8a3497 Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Tue, 5 May 2026 16:14:39 +0200 Subject: [PATCH 14/16] PHPStan --- phpstan-baseline.neon | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 9cf0187ef3..1ab25391fb 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -18066,13 +18066,6 @@ parameters: count: 1 path: src/lib/Persistence/Legacy/Content/FieldValue/Converter/DateAndTimeConverter.php - - - message: '#^Method Ibexa\\Core\\Persistence\\Legacy\\Content\\FieldValue\\Converter\\DateAndTimeConverter\:\:getDateIntervalFromXML\(\) should return DateInterval but empty return statement found\.$#' - identifier: return.empty - count: 1 - path: src/lib/Persistence/Legacy/Content/FieldValue/Converter/DateAndTimeConverter.php - - - message: '#^Method Ibexa\\Core\\Persistence\\Legacy\\Content\\FieldValue\\Converter\\DateAndTimeConverter\:\:toFieldDefinition\(\) has no return type specified\.$#' identifier: missingType.return @@ -32287,7 +32280,6 @@ parameters: count: 1 path: tests/integration/Core/Repository/Common/SlugConverter.php - - message: '#^Cannot call method getValue\(\) on Ibexa\\Contracts\\Core\\Repository\\Values\\Content\\Field\|null\.$#' identifier: method.nonObject @@ -41912,7 +41904,6 @@ parameters: count: 1 path: tests/integration/Core/Repository/ObjectStateServiceTest.php - - message: '#^Method Ibexa\\Tests\\Integration\\Core\\Repository\\ObjectStateServiceTest\:\:assertObjectsLoadedByIdentifiers\(\) has no return type specified\.$#' identifier: missingType.return @@ -41943,7 +41934,6 @@ parameters: count: 1 path: tests/integration/Core/Repository/ObjectStateServiceTest.php - - message: '#^Method Ibexa\\Tests\\Integration\\Core\\Repository\\ObjectStateServiceTest\:\:deleteExistingObjectStateGroups\(\) has no return type specified\.$#' identifier: missingType.return From 0234ccd3029705506c31469413af0da07757f41e Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Tue, 5 May 2026 16:32:05 +0200 Subject: [PATCH 15/16] PHPStan --- phpstan-baseline-7.4.neon | 6 ------ 1 file changed, 6 deletions(-) diff --git a/phpstan-baseline-7.4.neon b/phpstan-baseline-7.4.neon index c970170901..993f164a9e 100644 --- a/phpstan-baseline-7.4.neon +++ b/phpstan-baseline-7.4.neon @@ -180,12 +180,6 @@ parameters: count: 1 path: src/lib/MVC/Symfony/Matcher/ContentBased/UrlAlias.php - - - message: '#^Method Ibexa\\Core\\Persistence\\Legacy\\Content\\FieldValue\\Converter\\DateAndTimeConverter\:\:getDateIntervalFromXML\(\) should return DateInterval but returns DateInterval\|false\.$#' - identifier: return.type - count: 1 - path: src/lib/Persistence/Legacy/Content/FieldValue/Converter/DateAndTimeConverter.php - - message: '#^Cannot access property \$ownerDocument on DOMElement\|false\.$#' identifier: property.nonObject From 5f6717b70228013f2e645a8b29265de8304b381f Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Tue, 5 May 2026 19:48:51 +0200 Subject: [PATCH 16/16] PHPStan --- phpstan-baseline-7.4.neon | 6 ++++++ phpstan-baseline.neon | 6 ++++++ .../FieldValue/Converter/DateAndTimeConverter.php | 9 ++------- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/phpstan-baseline-7.4.neon b/phpstan-baseline-7.4.neon index 993f164a9e..c970170901 100644 --- a/phpstan-baseline-7.4.neon +++ b/phpstan-baseline-7.4.neon @@ -180,6 +180,12 @@ parameters: count: 1 path: src/lib/MVC/Symfony/Matcher/ContentBased/UrlAlias.php + - + message: '#^Method Ibexa\\Core\\Persistence\\Legacy\\Content\\FieldValue\\Converter\\DateAndTimeConverter\:\:getDateIntervalFromXML\(\) should return DateInterval but returns DateInterval\|false\.$#' + identifier: return.type + count: 1 + path: src/lib/Persistence/Legacy/Content/FieldValue/Converter/DateAndTimeConverter.php + - message: '#^Cannot access property \$ownerDocument on DOMElement\|false\.$#' identifier: property.nonObject diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 1ab25391fb..7e90edb8c2 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -18066,6 +18066,12 @@ parameters: count: 1 path: src/lib/Persistence/Legacy/Content/FieldValue/Converter/DateAndTimeConverter.php + - + message: '#^Method Ibexa\\Core\\Persistence\\Legacy\\Content\\FieldValue\\Converter\\DateAndTimeConverter\:\:getDateIntervalFromXML\(\) should return DateInterval but empty return statement found\.$#' + identifier: return.empty + count: 1 + path: src/lib/Persistence/Legacy/Content/FieldValue/Converter/DateAndTimeConverter.php + - message: '#^Method Ibexa\\Core\\Persistence\\Legacy\\Content\\FieldValue\\Converter\\DateAndTimeConverter\:\:toFieldDefinition\(\) has no return type specified\.$#' identifier: missingType.return diff --git a/src/lib/Persistence/Legacy/Content/FieldValue/Converter/DateAndTimeConverter.php b/src/lib/Persistence/Legacy/Content/FieldValue/Converter/DateAndTimeConverter.php index 872fc0aa2b..42a2efc4f1 100644 --- a/src/lib/Persistence/Legacy/Content/FieldValue/Converter/DateAndTimeConverter.php +++ b/src/lib/Persistence/Legacy/Content/FieldValue/Converter/DateAndTimeConverter.php @@ -98,11 +98,6 @@ public function toFieldDefinition(StorageFieldDefinition $storageDef, FieldDefin { $useSeconds = (bool)$storageDef->dataInt2; $dateInterval = $this->getDateIntervalFromXML($storageDef->dataText5); - if ($dateInterval === false) { - $fieldDef->defaultValue->data = null; - - return; - } $fieldDef->fieldTypeConstraints->fieldSettings = new FieldSettings( [ @@ -209,12 +204,12 @@ protected function generateDateIntervalXML(DateInterval $dateInterval) * * @param string $xmlText * - * @return \DateInterval|false + * @return \DateInterval */ protected function getDateIntervalFromXML($xmlText) { if (empty($xmlText)) { - return false; + return; } $xml = new SimpleXMLElement($xmlText);