diff --git a/.github/workflows/bc.yml b/.github/workflows/bc.yml index 00041a9..85232cc 100644 --- a/.github/workflows/bc.yml +++ b/.github/workflows/bc.yml @@ -30,4 +30,4 @@ jobs: os: >- ['ubuntu-latest'] php: >- - ['8.1'] + ['8.3'] diff --git a/CHANGELOG.md b/CHANGELOG.md index 77503fe..cffddb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 1.6.4 under development +- New #116: Add `ToArrayOfIntegers` parameter attribute (@samdark) - Enh #117: Explicitly import functions and constants in "use" section (@mspirkov) ## 1.6.3 December 16, 2025 diff --git a/docs/guide/en/typecasting.md b/docs/guide/en/typecasting.md index e68187c..8619f85 100644 --- a/docs/guide/en/typecasting.md +++ b/docs/guide/en/typecasting.md @@ -240,3 +240,29 @@ Attribute parameters: - `splitResolvedValue` — split resolved value by separator (boolean, default `true`); - `separator` — the boundary string (default, `\R`), it's a part of regular expression so should be taken into account or properly escaped with `preg_quote()`. + +### `ToArrayOfIntegers` + +Use `ToArrayOfIntegers` attribute to cast a value to an array of integers: + +```php +use Yiisoft\Hydrator\Attribute\Parameter\ToArrayOfIntegers; + +final class Post +{ + #[ToArrayOfIntegers] + public array $ratings = []; +} +``` + +If the resolved value is iterable, each element will be cast to an integer. For example, array `['1', '2', '3']` will be +converted to `[1, 2, 3]`. + +If the resolved value is not iterable, it will be cast to a string, split by separator, and then each element will be +cast to an integer. For example, string `'1,2,3'` will be converted to `[1, 2, 3]`. + +Attribute parameters: + +- `splitResolvedValue` — split non-array resolved value by separator (boolean, default `true`); +- `separator` — the boundary string (default `,`), it's a part of regular expression so should be taken into account + or properly escaped with `preg_quote()`. diff --git a/docs/guide/ru/typecasting.md b/docs/guide/ru/typecasting.md index e7d23d3..f58f611 100644 --- a/docs/guide/ru/typecasting.md +++ b/docs/guide/ru/typecasting.md @@ -253,3 +253,33 @@ final class Post - `separator` — символ перевода строки (по умолчанию, `\R`). Это часть регулярного выражения, поэтому ее следует учитывать или правильно экранировать с помощью `preg_quote()`. + +### `ToArrayOfIntegers` + +Use `ToArrayOfIntegers` attribute to cast a value to an array of integers: + +```php +use Yiisoft\Hydrator\Attribute\Parameter\ToArrayOfIntegers; + +final class Post +{ + #[ToArrayOfIntegers] + public array $ratings = []; +} +``` + +If the resolved value is iterable, each element will be cast to an +integer. For example, array `['1', '2', '3']` will be converted to `[1, 2, +3]`. + +If the resolved value is not iterable, it will be cast to a string, split by +separator, and then each element will be cast to an integer. For example, +string `'1,2,3'` will be converted to `[1, 2, 3]`. + +Параметры атрибута: + +- `splitResolvedValue` — split non-array resolved value by separator + (boolean, default `true`); +- `separator` — the boundary string (default `,`), it's a part of regular + expression so should be taken into account or properly escaped with + `preg_quote()`. diff --git a/docs/po/typecasting.md/ru/typecasting.md.ru.po b/docs/po/typecasting.md/ru/typecasting.md.ru.po index bec757f..5d20f76 100644 --- a/docs/po/typecasting.md/ru/typecasting.md.ru.po +++ b/docs/po/typecasting.md/ru/typecasting.md.ru.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" -"POT-Creation-Date: 2025-06-18 12:27+0000\n" +"POT-Creation-Date: 2026-01-12 22:34+0000\n" "PO-Revision-Date: 2025-03-01 15:15+0500\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -577,3 +577,69 @@ msgstr "`splitResolvedValue` — разделить значения по раз #: guide/en/typecasting.md msgid "`separator` — the boundary string (default, `\\R`), it's a part of regular expression so should be taken into account or properly escaped with `preg_quote()`." msgstr "`separator` — символ перевода строки (по умолчанию, `\\R`). Это часть регулярного выражения, поэтому ее следует учитывать или правильно экранировать с помощью `preg_quote()`." + +#. type: Title ### +#: guide/en/typecasting.md +#, fuzzy, no-wrap +#| msgid "`ToArrayOfStrings`" +msgid "`ToArrayOfIntegers`" +msgstr "`ToArrayOfStrings`" + +#. type: Plain text +#: guide/en/typecasting.md +#, fuzzy +#| msgid "Use `ToArrayOfStrings` attribute to cast a value to an array of strings:" +msgid "Use `ToArrayOfIntegers` attribute to cast a value to an array of integers:" +msgstr "Используйте атрибут `ToArrayOfStrings` для приведения значения к массиву строк:" + +#. type: Fenced code block (php) +#: guide/en/typecasting.md +#, fuzzy, no-wrap +#| msgid "" +#| "use Yiisoft\\Hydrator\\Attribute\\Parameter\\ToArrayOfStrings;\n" +#| "\n" +#| "final class Post\n" +#| "{\n" +#| " #[ToArrayOfStrings(separator: ',')]\n" +#| " public array $tags = []; \n" +#| "}\n" +msgid "" +"use Yiisoft\\Hydrator\\Attribute\\Parameter\\ToArrayOfIntegers;\n" +"\n" +"final class Post\n" +"{\n" +" #[ToArrayOfIntegers]\n" +" public array $ratings = []; \n" +"}\n" +msgstr "" +"use Yiisoft\\Hydrator\\Attribute\\Parameter\\ToArrayOfStrings;\n" +"\n" +"final class Post\n" +"{\n" +" #[ToArrayOfStrings(separator: ',')]\n" +" public array $tags = []; \n" +"}\n" + +#. type: Plain text +#: guide/en/typecasting.md +msgid "If the resolved value is iterable, each element will be cast to an integer. For example, array `['1', '2', '3']` will be converted to `[1, 2, 3]`." +msgstr "" + +#. type: Plain text +#: guide/en/typecasting.md +msgid "If the resolved value is not iterable, it will be cast to a string, split by separator, and then each element will be cast to an integer. For example, string `'1,2,3'` will be converted to `[1, 2, 3]`." +msgstr "" + +#. type: Bullet: '- ' +#: guide/en/typecasting.md +#, fuzzy +#| msgid "`splitResolvedValue` — split resolved value by separator (boolean, default `true`);" +msgid "`splitResolvedValue` — split non-array resolved value by separator (boolean, default `true`);" +msgstr "`splitResolvedValue` — разделить значения по разделителю (логическое значение, по умолчанию `true`);" + +#. type: Bullet: '- ' +#: guide/en/typecasting.md +#, fuzzy +#| msgid "`separator` — the boundary string (default, `\\R`), it's a part of regular expression so should be taken into account or properly escaped with `preg_quote()`." +msgid "`separator` — the boundary string (default `,`), it's a part of regular expression so should be taken into account or properly escaped with `preg_quote()`." +msgstr "`separator` — символ перевода строки (по умолчанию, `\\R`). Это часть регулярного выражения, поэтому ее следует учитывать или правильно экранировать с помощью `preg_quote()`." diff --git a/docs/po/typecasting.md/typecasting.md.pot b/docs/po/typecasting.md/typecasting.md.pot index 621d82b..d69f282 100644 --- a/docs/po/typecasting.md/typecasting.md.pot +++ b/docs/po/typecasting.md/typecasting.md.pot @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2025-06-18 12:27+0000\n" +"POT-Creation-Date: 2026-01-12 22:34+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -409,3 +409,58 @@ msgid "" "expression so should be taken into account or properly escaped with " "`preg_quote()`." msgstr "" + +#. type: Title ### +#: guide/en/typecasting.md +#, no-wrap +msgid "`ToArrayOfIntegers`" +msgstr "" + +#. type: Plain text +#: guide/en/typecasting.md +msgid "" +"Use `ToArrayOfIntegers` attribute to cast a value to an array of integers:" +msgstr "" + +#. type: Fenced code block (php) +#: guide/en/typecasting.md +#, no-wrap +msgid "" +"use Yiisoft\\Hydrator\\Attribute\\Parameter\\ToArrayOfIntegers;\n" +"\n" +"final class Post\n" +"{\n" +" #[ToArrayOfIntegers]\n" +" public array $ratings = []; \n" +"}\n" +msgstr "" + +#. type: Plain text +#: guide/en/typecasting.md +msgid "" +"If the resolved value is iterable, each element will be cast to an integer. " +"For example, array `['1', '2', '3']` will be converted to `[1, 2, 3]`." +msgstr "" + +#. type: Plain text +#: guide/en/typecasting.md +msgid "" +"If the resolved value is not iterable, it will be cast to a string, split by " +"separator, and then each element will be cast to an integer. For example, " +"string `'1,2,3'` will be converted to `[1, 2, 3]`." +msgstr "" + +#. type: Bullet: '- ' +#: guide/en/typecasting.md +msgid "" +"`splitResolvedValue` — split non-array resolved value by separator (boolean, " +"default `true`);" +msgstr "" + +#. type: Bullet: '- ' +#: guide/en/typecasting.md +msgid "" +"`separator` — the boundary string (default `,`), it's a part of regular " +"expression so should be taken into account or properly escaped with " +"`preg_quote()`." +msgstr "" diff --git a/rector.php b/rector.php index 6874c65..0d53413 100644 --- a/rector.php +++ b/rector.php @@ -28,5 +28,6 @@ NewInInitializerRector::class, ReadOnlyPropertyRector::class, __DIR__ . '/tests/Support/Classes/SimpleClass.php', + __DIR__ . '/tests/TestEnvironments/Php84/Hydrator/PublicPrivateSetProperty/Figure.php' ]); }; diff --git a/src/Attribute/Parameter/ToArrayOfIntegers.php b/src/Attribute/Parameter/ToArrayOfIntegers.php new file mode 100644 index 0000000..417c526 --- /dev/null +++ b/src/Attribute/Parameter/ToArrayOfIntegers.php @@ -0,0 +1,30 @@ +isResolved()) { + return Result::fail(); + } + + $resolvedValue = $context->getResolvedValue(); + if (is_iterable($resolvedValue)) { + $array = []; + foreach ($resolvedValue as $value) { + $array[] = (int) $value; + } + } else { + $value = $this->castValueToString($resolvedValue); + if ($attribute->splitResolvedValue) { + $array = []; + if (trim($value) !== '') { + /** + * @var string[] $stringArray We assume valid regular expression is used here, so `preg_split()` always returns + * an array of strings. + */ + $stringArray = preg_split('~' . $attribute->separator . '~u', $value); + + foreach ($stringArray as $item) { + $array[] = (int) $item; + } + } + } elseif (trim($value) === '') { + $array = []; + } else { + $array = [(int) $value]; + } + } + + return Result::success($array); + } + + private function castValueToString(mixed $value): string + { + return is_scalar($value) || $value instanceof Stringable ? (string) $value : ''; + } +} diff --git a/tests/Attribute/Parameter/ToArrayOfIntegersTest.php b/tests/Attribute/Parameter/ToArrayOfIntegersTest.php new file mode 100644 index 0000000..bf024a9 --- /dev/null +++ b/tests/Attribute/Parameter/ToArrayOfIntegersTest.php @@ -0,0 +1,215 @@ + [ + 'expectedValue' => [], + 'value' => [], + 'object' => new class { + #[ToArrayOfIntegers] + public ?array $value = null; + }, + ]; + yield 'empty string' => [ + 'expectedValue' => [], + 'value' => '', + 'object' => new class { + #[ToArrayOfIntegers] + public ?array $value = null; + }, + ]; + yield 'integer' => [ + 'expectedValue' => [42], + 'value' => 42, + 'object' => new class { + #[ToArrayOfIntegers] + public ?array $value = null; + }, + ]; + yield 'numeric string' => [ + 'expectedValue' => [42], + 'value' => '42', + 'object' => new class { + #[ToArrayOfIntegers] + public ?array $value = null; + }, + ]; + yield 'array of integers' => [ + 'expectedValue' => [1, 2, 3], + 'value' => [1, 2, 3], + 'object' => new class { + #[ToArrayOfIntegers] + public ?array $value = null; + }, + ]; + yield 'array of numeric strings' => [ + 'expectedValue' => [1, 2, 3], + 'value' => ['1', '2', '3'], + 'object' => new class { + #[ToArrayOfIntegers] + public ?array $value = null; + }, + ]; + yield 'array of mixed types' => [ + 'expectedValue' => [1, 42, 1, 2], + 'value' => ['1', 42, true, 2.4], + 'object' => new class { + #[ToArrayOfIntegers] + public ?array $value = null; + }, + ]; + yield 'array with empty strings' => [ + 'expectedValue' => [1, 0], + 'value' => ['1', ''], + 'object' => new class { + #[ToArrayOfIntegers] + public ?array $value = null; + }, + ]; + yield 'SPL array object' => [ + 'expectedValue' => [1, 2, 3], + 'value' => new ArrayObject([1, 2, 3]), + 'object' => new class { + #[ToArrayOfIntegers] + public ?array $value = null; + }, + ]; + yield 'array of mixed types with false' => [ + 'expectedValue' => [1, 0, 2], + 'value' => [1, false, 2], + 'object' => new class { + #[ToArrayOfIntegers] + public ?array $value = null; + }, + ]; + yield 'array of mixed types with null' => [ + 'expectedValue' => [1, 0, 2], + 'value' => [1, null, 2], + 'object' => new class { + #[ToArrayOfIntegers] + public ?array $value = null; + }, + ]; + yield 'array of mixed types with float' => [ + 'expectedValue' => [10, 20, 30], + 'value' => ['10.5', '20.9', '30.1'], + 'object' => new class { + #[ToArrayOfIntegers] + public ?array $value = null; + }, + ]; + yield 'splitting with default separator (comma)' => [ + 'expectedValue' => [1, 2, 3], + 'value' => '1,2,3', + 'object' => new class { + #[ToArrayOfIntegers] + public ?array $value = null; + }, + ]; + yield 'splitting with empty values' => [ + 'expectedValue' => [1, 0, 2], + 'value' => '1, ,2', + 'object' => new class { + #[ToArrayOfIntegers] + public ?array $value = null; + }, + ]; + yield 'splitting with spaces' => [ + 'expectedValue' => [1, 2, 3], + 'value' => '1, 2, 3', + 'object' => new class { + #[ToArrayOfIntegers] + public ?array $value = null; + }, + ]; + yield 'custom separator' => [ + 'expectedValue' => [1, 2, 3], + 'value' => '1;2;3', + 'object' => new class { + #[ToArrayOfIntegers(separator: ';')] + public ?array $value = null; + }, + ]; + yield 'splitResolvedValue = false' => [ + 'expectedValue' => [1], + 'value' => '1,2,3', + 'object' => new class { + #[ToArrayOfIntegers(splitResolvedValue: false)] + public ?array $value = null; + }, + ]; + yield 'splitResolvedValue = false with empty value' => [ + 'expectedValue' => [], + 'value' => '', + 'object' => new class { + #[ToArrayOfIntegers(splitResolvedValue: false)] + public ?array $value = null; + }, + ]; + yield 'split with mixed types' => [ + 'expectedValue' => [1, 2, 3, 4], + 'value' => '1,2.5,3,4.9', + 'object' => new class { + #[ToArrayOfIntegers] + public ?array $value = null; + }, + ]; + } + + #[DataProvider('dataBase')] + public function testBase(mixed $expectedValue, mixed $value, object $object) + { + (new Hydrator())->hydrate($object, ['value' => $value]); + $this->assertSame($expectedValue, $object->value); + } + + public function testNotResolved(): void + { + $object = new class { + #[ToArrayOfIntegers] + public ?array $value = null; + }; + + (new Hydrator())->hydrate($object); + + $this->assertNull($object->value); + } + + public function testUnexpectedAttributeException(): void + { + $hydrator = new Hydrator( + attributeResolverFactory: new ContainerAttributeResolverFactory( + new SimpleContainer([ + CounterResolver::class => new ToArrayOfIntegersResolver(), + ]), + ), + ); + + $object = new CounterClass(); + + $this->expectException(UnexpectedAttributeException::class); + $this->expectExceptionMessage( + 'Expected "' . ToArrayOfIntegers::class . '", but "' . Counter::class . '" given.', + ); + $hydrator->hydrate($object); + } +}