Update the validation engine of the "Each" rule

These changes will also introduce an abstract rule that validates
non-empty-iterable values. The abstract rule can also be the parent of
the recently created "Min" rule. Therefore, I've changed that class too.

I've introduced many tests for the "Each" rule to make sure what its
expected behavior is. I'm not super happy with its output, but I tried a
couple of options, and it is the best choice.

Note that Each now rejects `stdClass` and empty iterable values. I
thought that would make sense, as it would be useless when the input is
empty.

Signed-off-by: Henrique Moody <henriquemoody@gmail.com>
This commit is contained in:
Henrique Moody 2024-03-03 14:26:43 +01:00
parent 210aa4ac01
commit 433ceb4452
No known key found for this signature in database
GPG key ID: 221E9281655813A6
10 changed files with 431 additions and 215 deletions

View file

@ -20,16 +20,10 @@ You can also validate array keys combining this rule with [Call](Call.md):
v::call('array_keys', v::each(v::stringType()))->validate($releaseDates); // true
```
This rule will not validate values that are not iterable, to have a more detailed
error message, add [IterableVal](IterableVal.md) to your chain, for example.
## Note
If the input is empty this rule will consider the value as valid, you use
[NotEmpty](NotEmpty.md) if convenient:
```php
v::each(v::dateTime())->validate([]); // true
v::notEmpty()->each(v::dateTime())->validate([]); // false
```
This rule uses [IterableType](IterableType.md) and [NotEmpty](NotEmpty.md) internally. If an input is non-iterable or
empty, the validation will fail.
## Categorization
@ -39,10 +33,11 @@ v::notEmpty()->each(v::dateTime())->validate([]); // false
## Changelog
Version | Description
--------|-------------
2.0.0 | Remove support for key validation
0.3.9 | Created
| Version | Description |
|--------:|-------------------------------------------------------------|
| 3.0.0 | Rejected `stdClass`, non-iterable. or empty iterable values |
| 2.0.0 | Remove support for key validation |
| 0.3.9 | Created |
***
See also:
@ -52,5 +47,6 @@ See also:
- [IterableType](IterableType.md)
- [IterableVal](IterableVal.md)
- [Key](Key.md)
- [Min](Min.md)
- [NotEmpty](NotEmpty.md)
- [Unique](Unique.md)

View file

@ -17,11 +17,8 @@ v::min(v::lessThan(3))->validate([4, 8, 12]); // false
## Note
This rule uses PHP's [min][] function to compare the input against the given rule. The PHP manual states that:
> Values of different types will be compared using the [standard comparison rules][]. For instance, a non-numeric
> `string` will be compared to an `int` as though it were `0`, but multiple non-numeric `string` values will be compared
> alphanumerically. The actual value returned will be of the original type with no conversion applied.
This rule uses [IterableType](IterableType.md) and [NotEmpty](NotEmpty.md) internally. If an input is non-iterable or
empty, the validation will fail.
## Categorization
@ -41,10 +38,9 @@ This rule uses PHP's [min][] function to compare the input against the given rul
See also:
- [Between](Between.md)
- [Each](Each.md)
- [GreaterThan](GreaterThan.md)
- [GreaterThanOrEqual](GreaterThanOrEqual.md)
- [LessThan](LessThan.md)
- [LessThanOrEqual](LessThanOrEqual.md)
[min]: https://www.php.net/min
[standard comparison rules]: https://www.php.net/operators.comparison
- [NotEmpty](NotEmpty.md)

View file

@ -49,6 +49,7 @@ Version | Description
See also:
- [Each](Each.md)
- [Min](Min.md)
- [NoWhitespace](NoWhitespace.md)
- [NotBlank](NotBlank.md)
- [NotOptional](NotOptional.md)

View file

@ -0,0 +1,61 @@
<?php
/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-License-Identifier: MIT
*/
declare(strict_types=1);
namespace Respect\Validation\Rules\Core;
use Respect\Validation\Helpers\CanBindEvaluateRule;
use Respect\Validation\Result;
use Respect\Validation\Rules\IterableType;
use Respect\Validation\Rules\NotEmpty;
use Respect\Validation\Rules\Wrapper;
use function is_array;
use function iterator_to_array;
use function lcfirst;
use function strrchr;
use function substr;
abstract class FilteredNonEmptyArray extends Wrapper
{
use CanBindEvaluateRule;
/** @param non-empty-array<mixed> $input */
abstract protected function evaluateNonEmptyArray(array $input): Result;
public function evaluate(mixed $input): Result
{
$id = $this->rule->getName() ?? $this->getName() ?? lcfirst(substr((string) strrchr(static::class, '\\'), 1));
$iterableResult = $this->bindEvaluate(new IterableType(), $this, $input);
if (!$iterableResult->isValid) {
return $iterableResult->withId($id);
}
$array = $this->toArray($input);
$notEmptyResult = $this->bindEvaluate(new NotEmpty(), $this, $array);
if (!$notEmptyResult->isValid) {
return $notEmptyResult->withId($id);
}
// @phpstan-ignore-next-line
return $this->evaluateNonEmptyArray($array);
}
/**
* @param iterable<mixed> $input
* @return array<mixed>
*/
private function toArray(iterable $input): array
{
if (is_array($input)) {
return $input;
}
return iterator_to_array($input);
}
}

View file

@ -9,92 +9,28 @@ declare(strict_types=1);
namespace Respect\Validation\Rules;
use Respect\Validation\Attributes\ExceptionClass;
use Respect\Validation\Exceptions\EachException;
use Respect\Validation\Exceptions\ValidationException;
use Respect\Validation\Helpers\CanValidateIterable;
use Respect\Validation\Message\Template;
use Respect\Validation\Result;
use Respect\Validation\Validatable;
use Respect\Validation\Rules\Core\FilteredNonEmptyArray;
use function array_map;
use function array_reduce;
#[ExceptionClass(EachException::class)]
#[Template(
'Each item in {{name}} must be valid',
'Each item in {{name}} must not validate',
)]
final class Each extends AbstractRule
final class Each extends FilteredNonEmptyArray
{
use CanValidateIterable;
public function __construct(
private readonly Validatable $rule
) {
}
public function evaluate(mixed $input): Result
/** @param non-empty-array<mixed> $input */
protected function evaluateNonEmptyArray(array $input): Result
{
if (!$this->isIterable($input)) {
return Result::failed($input, $this);
}
$children = [];
$isValid = true;
foreach ($input as $inputItem) {
$childResult = $this->rule->evaluate($inputItem);
$isValid = $isValid && $childResult->isValid;
$children[] = $childResult;
}
$children = array_map(fn ($item) => $this->rule->evaluate($item), $input);
$isValid = array_reduce($children, static fn ($carry, $childResult) => $carry && $childResult->isValid, true);
if ($isValid) {
return Result::passed($input, $this)->withChildren(...$children);
}
return Result::failed($input, $this)->withChildren(...$children);
}
public function assert(mixed $input): void
{
if (!$this->isIterable($input)) {
throw $this->reportError($input);
}
$exceptions = [];
foreach ($input as $value) {
try {
$this->rule->assert($value);
} catch (ValidationException $exception) {
$exceptions[] = $exception;
}
}
if (!empty($exceptions)) {
/** @var EachException $eachException */
$eachException = $this->reportError($input);
$eachException->addChildren($exceptions);
throw $eachException;
}
}
public function check(mixed $input): void
{
if (!$this->isIterable($input)) {
throw $this->reportError($input);
}
foreach ($input as $value) {
$this->rule->check($value);
}
}
public function validate(mixed $input): bool
{
try {
$this->check($input);
} catch (ValidationException $exception) {
return false;
}
return true;
}
}

View file

@ -11,54 +11,22 @@ namespace Respect\Validation\Rules;
use Respect\Validation\Message\Template;
use Respect\Validation\Result;
use Respect\Validation\Rules\Core\FilteredNonEmptyArray;
use function count;
use function is_array;
use function is_iterable;
use function iterator_to_array;
use function min;
#[Template('As the minimum from {{name}},', 'As the minimum from {{name}},')]
#[Template('The minimum from', 'The minimum from', self::TEMPLATE_NAMED)]
#[Template('{{name}} must have at least 1 item', '{{name}} must not have at least 1 item', self::TEMPLATE_EMPTY)]
#[Template(
'{{name}} must be an array or iterable to validate its minimum value',
'{{name}} must not be an array or iterable to validate its minimum value',
self::TEMPLATE_TYPE,
)]
final class Min extends Wrapper
final class Min extends FilteredNonEmptyArray
{
public const TEMPLATE_NAMED = '__named__';
public const TEMPLATE_EMPTY = '__empty__';
public const TEMPLATE_TYPE = '__min__';
public function evaluate(mixed $input): Result
/** @param non-empty-array<mixed> $input */
protected function evaluateNonEmptyArray(array $input): Result
{
if (!is_iterable($input)) {
return Result::failed($input, $this);
}
$array = $this->toArray($input);
if (count($array) === 0) {
return Result::failed($input, $this);
}
$result = $this->rule->evaluate(min($array));
$result = $this->rule->evaluate(min($input));
$template = $this->getName() === null ? self::TEMPLATE_STANDARD : self::TEMPLATE_NAMED;
return (new Result($result->isValid, $input, $this, [], $template,))->withNextSibling($result);
}
/**
* @param iterable<mixed> $input
* @return array<mixed>
*/
private function toArray(iterable $input): array
{
if (is_array($input)) {
return $input;
}
return iterator_to_array($input);
return (new Result($result->isValid, $input, $this, [], $template))->withNextSibling($result);
}
}

View file

@ -7,13 +7,245 @@ require 'vendor/autoload.php';
use Respect\Validation\Validator as v;
exceptionMessage(static fn() => v::each(v::dateTime())->check(null));
exceptionMessage(static fn() => v::not(v::each(v::dateTime()))->check(['2018-10-10']));
exceptionFullMessage(static fn() => v::each(v::dateTime())->assert(null));
exceptionFullMessage(static fn() => v::not(v::each(v::dateTime()))->assert(['2018-10-10']));
$empty = [];
$nonIterable = null;
$negative = [1, 2, 3];
$default = ['a', 'b', 'c'];
run([
// Simple
'Non-iterable' => [v::each(v::intType()), $nonIterable],
'Empty' => [v::each(v::intType()), $empty],
'Default' => [v::each(v::intType()), $default],
'Negative' => [v::not(v::each(v::intType())), $negative],
// With name
'With name, non-iterable' => [v::each(v::intType()->setName('Wrapped'))->setName('Wrapper'), $nonIterable],
'With name, empty' => [v::each(v::intType()->setName('Wrapped'))->setName('Wrapper'), $empty],
'With name, default' => [v::each(v::intType()->setName('Wrapped'))->setName('Wrapper'), $default],
'With name, negative' => [
v::not(v::each(v::intType()->setName('Wrapped'))->setName('Wrapper'))->setName('Not'),
$negative,
],
'With wrapper name, default' => [v::each(v::intType())->setName('Wrapper'), $default],
'With wrapper name, negative' => [
v::not(v::each(v::intType())->setName('Wrapper'))->setName('Not'),
$negative,
],
'With Not name, negative' => [
v::not(v::each(v::intType()))->setName('Not'),
$negative,
],
// With template
'With template, non-iterable' => [v::each(v::intType()), $nonIterable, 'You should have passed an iterable'],
'With template, empty' => [v::each(v::intType()), $empty, 'You should have passed an non-empty'],
'With template, default' => [v::each(v::intType()), $default, 'All items should have been integers'],
'with template, negative' => [v::not(v::each(v::intType())), $negative, 'All items should not have been integers'],
// With array template
'With array template, default' => [
v::each(v::intType()),
$default, [
'each' => [
'__self__' => 'Here a sequence of items that did not pass the validation',
'intType.1' => 'First item should have been an integer',
'intType.2' => 'Second item should have been an integer',
'intType.3' => 'Third item should have been an integer',
],
],
],
'With array template and name, default' => [
v::each(v::intType()->setName('Wrapped'))->setName('Wrapper'),
$default, [
'Wrapped' => [
'__self__' => 'Here a sequence of items that did not pass the validation',
'Wrapped.1' => 'First item should have been an integer',
'Wrapped.2' => 'Second item should have been an integer',
'Wrapped.3' => 'Third item should have been an integer',
],
],
],
]);
?>
--EXPECT--
Each item in `null` must be valid
"2018-10-10" must not be a valid date/time
- Each item in `null` must be valid
- "2018-10-10" must not be a valid date/time
Non-iterable
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
`null` must be of type iterable
- `null` must be of type iterable
[
'each' => '`null` must be of type iterable',
]
Empty
⎺⎺⎺⎺⎺
The value must not be empty
- The value must not be empty
[
'each' => 'The value must not be empty',
]
Default
⎺⎺⎺⎺⎺⎺⎺
"a" must be of type integer
- Each item in `["a", "b", "c"]` must be valid
- "a" must be of type integer
- "b" must be of type integer
- "c" must be of type integer
[
'intType.1' => '"a" must be of type integer',
'intType.2' => '"b" must be of type integer',
'intType.3' => '"c" must be of type integer',
]
Negative
⎺⎺⎺⎺⎺⎺⎺⎺
1 must not be of type integer
- Each item in `[1, 2, 3]` must not validate
- 1 must not be of type integer
- 2 must not be of type integer
- 3 must not be of type integer
[
'intType.1' => '1 must not be of type integer',
'intType.2' => '2 must not be of type integer',
'intType.3' => '3 must not be of type integer',
]
With name, non-iterable
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
Wrapped must be of type iterable
- Wrapped must be of type iterable
[
'Wrapped' => 'Wrapped must be of type iterable',
]
With name, empty
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
Wrapped must not be empty
- Wrapped must not be empty
[
'Wrapped' => 'Wrapped must not be empty',
]
With name, default
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
Wrapped must be of type integer
- Each item in Wrapped must be valid
- Wrapped must be of type integer
- Wrapped must be of type integer
- Wrapped must be of type integer
[
'Wrapped.1' => 'Wrapped must be of type integer',
'Wrapped.2' => 'Wrapped must be of type integer',
'Wrapped.3' => 'Wrapped must be of type integer',
]
With name, negative
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
Wrapped must not be of type integer
- Each item in Wrapped must not validate
- Wrapped must not be of type integer
- Wrapped must not be of type integer
- Wrapped must not be of type integer
[
'Wrapped.1' => 'Wrapped must not be of type integer',
'Wrapped.2' => 'Wrapped must not be of type integer',
'Wrapped.3' => 'Wrapped must not be of type integer',
]
With wrapper name, default
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
Wrapper must be of type integer
- Each item in Wrapper must be valid
- Wrapper must be of type integer
- Wrapper must be of type integer
- Wrapper must be of type integer
[
'Wrapper.1' => 'Wrapper must be of type integer',
'Wrapper.2' => 'Wrapper must be of type integer',
'Wrapper.3' => 'Wrapper must be of type integer',
]
With wrapper name, negative
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
Wrapper must not be of type integer
- Each item in Wrapper must not validate
- Wrapper must not be of type integer
- Wrapper must not be of type integer
- Wrapper must not be of type integer
[
'Wrapper.1' => 'Wrapper must not be of type integer',
'Wrapper.2' => 'Wrapper must not be of type integer',
'Wrapper.3' => 'Wrapper must not be of type integer',
]
With Not name, negative
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
Not must not be of type integer
- Each item in Not must not validate
- Not must not be of type integer
- Not must not be of type integer
- Not must not be of type integer
[
'Not.1' => 'Not must not be of type integer',
'Not.2' => 'Not must not be of type integer',
'Not.3' => 'Not must not be of type integer',
]
With template, non-iterable
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
You should have passed an iterable
- You should have passed an iterable
[
'each' => 'You should have passed an iterable',
]
With template, empty
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
You should have passed an non-empty
- You should have passed an non-empty
[
'each' => 'You should have passed an non-empty',
]
With template, default
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
All items should have been integers
- All items should have been integers
[
'each' => 'All items should have been integers',
]
with template, negative
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
All items should not have been integers
- All items should not have been integers
[
'each' => 'All items should not have been integers',
]
With array template, default
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
First item should have been an integer
- Here a sequence of items that did not pass the validation
- First item should have been an integer
- Second item should have been an integer
- Third item should have been an integer
[
'intType.1' => 'First item should have been an integer',
'intType.2' => 'Second item should have been an integer',
'intType.3' => 'Third item should have been an integer',
]
With array template and name, default
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
First item should have been an integer
- Here a sequence of items that did not pass the validation
- First item should have been an integer
- Second item should have been an integer
- Third item should have been an integer
[
'Wrapped.1' => 'First item should have been an integer',
'Wrapped.2' => 'Second item should have been an integer',
'Wrapped.3' => 'Third item should have been an integer',
]

View file

@ -0,0 +1,22 @@
<?php
/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-License-Identifier: MIT
*/
declare(strict_types=1);
namespace Respect\Validation\Test\Rules;
use Respect\Validation\Result;
use Respect\Validation\Rules\Core\FilteredNonEmptyArray;
final class ConcreteFilteredNonEmptyArray extends FilteredNonEmptyArray
{
/** @param non-empty-array<mixed> $input */
protected function evaluateNonEmptyArray(array $input): Result
{
return $this->rule->evaluate($input);
}
}

View file

@ -0,0 +1,73 @@
<?php
/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-License-Identifier: MIT
*/
declare(strict_types=1);
namespace Respect\Validation\Rules\Core;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\Test;
use Respect\Validation\Test\Rules\ConcreteFilteredNonEmptyArray;
use Respect\Validation\Test\Rules\Stub;
use Respect\Validation\Test\TestCase;
#[Group('core')]
#[CoversClass(FilteredNonEmptyArray::class)]
final class NonEmptyArrayFilteredTest extends TestCase
{
#[Test]
#[DataProvider('providerForNonIterableValues')]
public function itShouldInvalidateNonIterableValues(mixed $input): void
{
$sut = new ConcreteFilteredNonEmptyArray(Stub::daze());
self::assertInvalidInput($sut, $input);
}
/** @param iterable<mixed> $input */
#[Test]
#[DataProvider('providerForEmptyIterableValues')]
public function itShouldInvalidateEmptyIterableValues(iterable $input): void
{
$sut = new ConcreteFilteredNonEmptyArray(Stub::daze());
self::assertInvalidInput($sut, $input);
}
#[Test]
public function itShouldEvaluateNonEmptyIterables(): void
{
$rule = Stub::pass(1);
$input = [1, 2, 3];
$sut = new ConcreteFilteredNonEmptyArray($rule);
$sut->evaluate($input);
self::assertSame([$input], $rule->inputs);
}
#[Test]
public function itShouldKeepRuleIdWhenInvalidatingNonIterableValues(): void
{
$sut = new ConcreteFilteredNonEmptyArray(Stub::daze());
$result = $sut->evaluate(null);
self::assertEquals('concreteFilteredNonEmptyArray', $result->id);
}
#[Test]
public function itShouldKeepRuleIdWhenInvalidatingEmptyIterableValues(): void
{
$sut = new ConcreteFilteredNonEmptyArray(Stub::daze());
$result = $sut->evaluate([]);
self::assertEquals('concreteFilteredNonEmptyArray', $result->id);
}
}

View file

@ -9,75 +9,23 @@ declare(strict_types=1);
namespace Respect\Validation\Rules;
use ArrayObject;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\Test;
use Respect\Validation\Exceptions\NestedValidationException;
use Respect\Validation\Test\Rules\Stub;
use Respect\Validation\Test\RuleTestCase;
use SplStack;
use stdClass;
use Traversable;
use function array_chunk;
use function range;
#[Group('rule')]
#[CoversClass(Each::class)]
final class EachTest extends RuleTestCase
{
#[Test]
public function itShouldAssertEachValue(): void
{
$rule = Stub::pass(3);
$inputs = range(1, 3);
$sut = new Each($rule);
$sut->assert($inputs);
self::assertSame($inputs, $rule->inputs);
}
#[Test]
public function itShouldCheckEachValue(): void
{
$rule = Stub::pass(3);
$inputs = range(1, 3);
$sut = new Each($rule);
$sut->check($inputs);
self::assertSame($inputs, $rule->inputs);
}
#[Test]
public function itShouldNotOverrideMessages(): void
{
$rule = new Each(Stub::fail(3));
try {
$rule->assert([1, 2, 3]);
} catch (NestedValidationException $e) {
$this->assertEquals(
$e->getMessages(),
[
'stub.0' => '1 must be a valid stub',
'stub.1' => '2 must be a valid stub',
'stub.2' => '3 must be a valid stub',
]
);
}
}
/** @return iterable<array{Each, mixed}> */
public static function providerForValidInput(): iterable
{
return [
[new Each(Stub::daze()), []],
[new Each(Stub::pass(5)), [1, 2, 3, 4, 5]],
[new Each(Stub::pass(5)), self::createTraversableInput(1, 5)],
[new Each(Stub::pass(5)), self::createStdClassInput(1, 5)],
[new Each(Stub::pass(5)), new ArrayObject([1, 2, 3, 4, 5])],
];
}
@ -85,33 +33,16 @@ final class EachTest extends RuleTestCase
public static function providerForInvalidInput(): iterable
{
return [
[new Each(Stub::daze()), []],
[new Each(Stub::daze()), new stdClass()],
[new Each(Stub::daze()), 123],
[new Each(Stub::daze()), ''],
[new Each(Stub::daze()), null],
[new Each(Stub::daze()), false],
[new Each(Stub::fail(5)), ['', 2, 3, 4, 5]],
[new Each(Stub::fail(5)), ['a', 2, 3, 4, 5]],
[new Each(Stub::fail(5)), self::createTraversableInput(1, 5)],
[new Each(Stub::fail(5)), self::createStdClassInput(1, 5)],
[new Each(Stub::fail(5)), new ArrayObject([1, 2, 3, 4, 5])],
[new Each(Stub::fail(5)), (object) ['foo' => true]],
];
}
/**
* @return Traversable<int>
*/
private static function createTraversableInput(int $firstValue, int $lastValue): Traversable
{
/** @var SplStack<int> */
$input = new SplStack();
foreach (range($firstValue, $lastValue) as $value) {
$input->push($value);
}
return $input;
}
private static function createStdClassInput(int $firstValue, int $lastValue): stdClass
{
return (object) array_chunk(range($firstValue, $lastValue), 1);
}
}