From 36103881581a57b0c37faccc6fa5cbe6891b8739 Mon Sep 17 00:00:00 2001 From: GeorgII Date: Wed, 8 Apr 2026 23:52:45 +0200 Subject: [PATCH 1/3] Add immutable ArrayOfPrimitives collection with primitive validation Introduce the `ArrayOfPrimitives` class, comprehensive unit tests for its behavior, and a `PrimitivesArrayTypeException` to enforce that only `PrimitiveTypeInterface` instances are stored. --- src/ArrayType/ArrayOfPrimitives.php | 187 ++++++++++++++++++ .../PrimitivesArrayTypeException.php | 11 ++ .../Unit/ArrayType/ArrayOfPrimitivesTest.php | 135 +++++++++++++ 3 files changed, 333 insertions(+) create mode 100755 src/ArrayType/ArrayOfPrimitives.php create mode 100755 src/Exception/ArrayType/PrimitivesArrayTypeException.php create mode 100755 tests/Unit/ArrayType/ArrayOfPrimitivesTest.php diff --git a/src/ArrayType/ArrayOfPrimitives.php b/src/ArrayType/ArrayOfPrimitives.php new file mode 100755 index 00000000..a093715d --- /dev/null +++ b/src/ArrayType/ArrayOfPrimitives.php @@ -0,0 +1,187 @@ +isEmpty(); // true + * - $v = ArrayOfPrimitives::fromArray([IntegerPositive::fromInt(1)]); + * $v->isEmpty(); // false + * + * @template TItem of PrimitiveTypeInterface + * + * @template-extends ArrayTypeAbstract + * + * @psalm-immutable + */ +readonly class ArrayOfPrimitives extends ArrayTypeAbstract +{ + /** + * @var list + */ + private array $value; + + /** + * @param list $value + * + * @throws PrimitivesArrayTypeException + */ + public function __construct(array $value) + { + foreach ($value as $item) { + if (!$item instanceof PrimitiveTypeInterface) { + throw new PrimitivesArrayTypeException('Expected array of PrimitiveTypeInterface instances'); + } + } + + $this->value = $value; + } + + /** + * @return non-negative-int + */ + public function count(): int + { + return count($this->value); + } + + /** + * @psalm-pure + * + * @param list $value + * + * @throws PrimitivesArrayTypeException + */ + public static function fromArray(array $value): static + { + /** @var list $value */ + return new static($value); + } + + /** + * @no-named-arguments + * + * @throws PrimitivesArrayTypeException + */ + public static function fromItems(PrimitiveTypeInterface ...$items): static + { + /** @var list $items */ + return new static($items); + } + + /** + * @psalm-return list + */ + public function getDefinedItems(): array + { + $result = []; + + foreach ($this->value as $item) { + if (!$item->isUndefined()) { + $result[] = $item; + } + } + + /** @var list $result */ + return $result; + } + + /** + * @return Traversable + */ + public function getIterator(): Traversable + { + yield from $this->value; + } + + public function hasUndefined(): bool + { + foreach ($this->value as $item) { + if ($item instanceof Undefined) { + return true; + } + } + + return false; + } + + public function isEmpty(): bool + { + return $this->count() === 0; + } + + public function isTypeOf(string ...$classNames): bool + { + foreach ($classNames as $className) { + if ($this instanceof $className) { + return true; + } + } + + return false; + } + + public function isUndefined(): bool + { + $items = $this->value; + + if ($items === []) { + return false; + } + + foreach ($items as $item) { + if (!$item->isUndefined()) { + return false; + } + } + + return true; + } + + /** + * JSON serialization helper. + * + * @throws PrimitivesArrayTypeException + * + * @psalm-mutation-free + */ + public function jsonSerialize(): array + { + return $this->toArray(); + } + + /** + * @psalm-mutation-free + */ + public function toArray(): array + { + $result = []; + foreach ($this->value as $item) { + /** @psalm-suppress ImpureMethodCall */ + $result[] = $item->jsonSerialize(); + } + + return $result; + } + + /** + * @return list + */ + public function value(): array + { + return $this->value; + } +} diff --git a/src/Exception/ArrayType/PrimitivesArrayTypeException.php b/src/Exception/ArrayType/PrimitivesArrayTypeException.php new file mode 100755 index 00000000..8f7f9b07 --- /dev/null +++ b/src/Exception/ArrayType/PrimitivesArrayTypeException.php @@ -0,0 +1,11 @@ +value()) + ->toHaveCount(2) + ->and($c->value()[0])->toBe($i1) + ->and($c->value()[1])->toBe($s1); + }); + + it('throws when any item is not a primitive', function () { + expect(fn() => ArrayOfPrimitives::fromArray([1, new stdClass()])) + ->toThrow(PrimitivesArrayTypeException::class, 'Expected array of PrimitiveTypeInterface instances'); + }); + }); + + describe('fromItems', function () { + it('creates instance from variadic arguments', function () { + $i1 = IntegerType::fromInt(1); + $s1 = StringType::fromString('A'); + + $array = ArrayOfPrimitives::fromItems($i1, $s1); + + expect($array)->toBeInstanceOf(ArrayOfPrimitives::class) + ->and($array->count())->toBe(2) + ->and($array->value())->toBe([$i1, $s1]); + }); + }); + }); + + describe('Collection Methods', function () { + it('isEmpty() returns correct boolean', function (array $input, bool $expected) { + $c = new ArrayOfPrimitives($input); + expect($c->isEmpty())->toBe($expected); + })->with([ + 'empty' => [[], true], + 'not empty' => [[IntegerType::fromInt(1)], false], + ]); + + it('count() returns correct number of items', function (array $input, int $expected) { + $c = new ArrayOfPrimitives($input); + expect($c->count())->toBe($expected); + })->with([ + 'empty' => [[], 0], + 'three items' => [[IntegerType::fromInt(1), IntegerType::fromInt(2), IntegerType::fromInt(3)], 3], + ]); + }); + + describe('Undefined Handling', function () { + it('hasUndefined() detects undefined', function () { + $array = new ArrayOfPrimitives([IntegerType::fromInt(1), Undefined::create()]); + expect($array->hasUndefined())->toBeTrue(); + }); + + it('hasUndefined() returns false for no undefined', function () { + $array = new ArrayOfPrimitives([IntegerType::fromInt(1)]); + expect($array->hasUndefined())->toBeFalse(); + }); + + it('isUndefined() returns true for all undefined items', function () { + $array = new ArrayOfPrimitives([Undefined::create(), Undefined::create()]); + expect($array->isUndefined())->toBeTrue(); + }); + + it('isUndefined() returns false for empty array', function () { + $array = new ArrayOfPrimitives([]); + expect($array->isUndefined())->toBeFalse(); + }); + + it('isUndefined() returns false for mixed undefined and defined', function () { + $array = new ArrayOfPrimitives([IntegerType::fromInt(1), Undefined::create()]); + expect($array->isUndefined())->toBeFalse(); + }); + }); + + describe('Accessors', function () { + it('getDefinedItems() returns only defined items', function () { + $items = [IntegerType::fromInt(1), Undefined::create(), IntegerType::fromInt(2)]; + $array = new ArrayOfPrimitives($items); + + expect($array->getDefinedItems())->toHaveCount(2) + ->and($array->getDefinedItems()[0])->toBe($items[0]) + ->and($array->getDefinedItems()[1])->toBe($items[2]); + }); + + it('getIterator() iterates over all items', function () { + $items = [IntegerType::fromInt(1), IntegerType::fromInt(2)]; + $array = new ArrayOfPrimitives($items); + $iterated = []; + foreach ($array as $item) { + $iterated[] = $item; + } + expect($iterated)->toBe($items); + }); + + it('isTypeOf() returns true for current class', function () { + $array = new ArrayOfPrimitives([]); + expect($array->isTypeOf(ArrayOfPrimitives::class))->toBeTrue(); + }); + + it('isTypeOf() returns false for unknown class', function () { + $array = new ArrayOfPrimitives([]); + expect($array->isTypeOf(stdClass::class))->toBeFalse(); + }); + + it('toArray() and jsonSerialize() return array representation', function () { + $i1 = IntegerType::fromInt(1); + $i2 = IntegerType::fromInt(2); + $array = new ArrayOfPrimitives([$i1, $i2]); + + expect($array->toArray())->toBe([1, 2]) + ->and($array->jsonSerialize())->toBe([1, 2]); + }); + }); +}); From d6bae0f46dc7d867289863576dc81d17712a435d Mon Sep 17 00:00:00 2001 From: GeorgII Date: Wed, 8 Apr 2026 23:53:00 +0200 Subject: [PATCH 2/3] Rename Array tests to ArrayType and update mutation script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Change mutation command to run tests in `tests/Unit/ArrayType` instead of `tests/Unit/Array`. - Move all Array‑related unit test files to the new `ArrayType` directory to reflect the updated namespace. --- composer.json | 2 +- tests/Unit/{Array => ArrayType}/ArrayEmptyTest.php | 0 tests/Unit/{Array => ArrayType}/ArrayNonEmptyTest.php | 0 tests/Unit/{Array => ArrayType}/ArrayOfObjectsTest.php | 0 tests/Unit/{Array => ArrayType}/ArrayUndefinedTest.php | 0 5 files changed, 1 insertion(+), 1 deletion(-) rename tests/Unit/{Array => ArrayType}/ArrayEmptyTest.php (100%) rename tests/Unit/{Array => ArrayType}/ArrayNonEmptyTest.php (100%) rename tests/Unit/{Array => ArrayType}/ArrayOfObjectsTest.php (100%) rename tests/Unit/{Array => ArrayType}/ArrayUndefinedTest.php (100%) diff --git a/composer.json b/composer.json index 69df1352..962266d0 100755 --- a/composer.json +++ b/composer.json @@ -74,7 +74,7 @@ "echo \"Decimal\" && ./vendor/bin/pest tests/Unit/Decimal --mutate --covered-only --parallel --min=100", "echo \"Float\" && ./vendor/bin/pest tests/Unit/Float --mutate --covered-only --parallel --min=100", "echo \"Base\" && ./vendor/bin/pest tests/Unit/Base --mutate --covered-only --parallel --min=100", - "echo \"Array\" && ./vendor/bin/pest tests/Unit/Array --mutate --covered-only --parallel --min=100", + "echo \"Array\" && ./vendor/bin/pest tests/Unit/ArrayType --mutate --covered-only --parallel --min=100", "echo \"Bool\" && ./vendor/bin/pest tests/Unit/Bool --mutate --covered-only --parallel --min=100", "echo \"Undefined\" && ./vendor/bin/pest tests/Unit/Undefined --mutate --covered-only --parallel --min=100" ] diff --git a/tests/Unit/Array/ArrayEmptyTest.php b/tests/Unit/ArrayType/ArrayEmptyTest.php similarity index 100% rename from tests/Unit/Array/ArrayEmptyTest.php rename to tests/Unit/ArrayType/ArrayEmptyTest.php diff --git a/tests/Unit/Array/ArrayNonEmptyTest.php b/tests/Unit/ArrayType/ArrayNonEmptyTest.php similarity index 100% rename from tests/Unit/Array/ArrayNonEmptyTest.php rename to tests/Unit/ArrayType/ArrayNonEmptyTest.php diff --git a/tests/Unit/Array/ArrayOfObjectsTest.php b/tests/Unit/ArrayType/ArrayOfObjectsTest.php similarity index 100% rename from tests/Unit/Array/ArrayOfObjectsTest.php rename to tests/Unit/ArrayType/ArrayOfObjectsTest.php diff --git a/tests/Unit/Array/ArrayUndefinedTest.php b/tests/Unit/ArrayType/ArrayUndefinedTest.php similarity index 100% rename from tests/Unit/Array/ArrayUndefinedTest.php rename to tests/Unit/ArrayType/ArrayUndefinedTest.php From 77cd43e08e78dcae0245d6a8306beae359501948 Mon Sep 17 00:00:00 2001 From: GeorgII Date: Wed, 8 Apr 2026 23:53:10 +0200 Subject: [PATCH 3/3] Add PHPDoc usage examples to array collection classes Include simple example snippets for `ArrayNonEmpty`, `ArrayOfObjects`, `ArrayUndefined`, and `ArrayEmpty` to illustrate creation and basic operations. --- src/ArrayType/ArrayEmpty.php | 4 ++++ src/ArrayType/ArrayNonEmpty.php | 4 ++++ src/ArrayType/ArrayOfObjects.php | 4 ++++ src/ArrayType/ArrayUndefined.php | 4 ++++ 4 files changed, 16 insertions(+) diff --git a/src/ArrayType/ArrayEmpty.php b/src/ArrayType/ArrayEmpty.php index cecb1334..2e4dfd7f 100755 --- a/src/ArrayType/ArrayEmpty.php +++ b/src/ArrayType/ArrayEmpty.php @@ -11,6 +11,10 @@ /** * Immutable empty collection. * + * Example + * - $v = new ArrayEmpty([]); + * $v->toArray(); // [] + * * @extends ArrayTypeAbstract * * @psalm-immutable diff --git a/src/ArrayType/ArrayNonEmpty.php b/src/ArrayType/ArrayNonEmpty.php index fd26dc7c..46985e2b 100755 --- a/src/ArrayType/ArrayNonEmpty.php +++ b/src/ArrayType/ArrayNonEmpty.php @@ -18,6 +18,10 @@ /** * Immutable non-empty array. * + * Example + * - $v = ArrayNonEmpty::fromArray([new \stdClass()]); + * $v->count(); // 1 + * * @template TItem of object * * @template-extends ArrayTypeAbstract diff --git a/src/ArrayType/ArrayOfObjects.php b/src/ArrayType/ArrayOfObjects.php index 3998f6e8..d46f0d37 100755 --- a/src/ArrayType/ArrayOfObjects.php +++ b/src/ArrayType/ArrayOfObjects.php @@ -16,6 +16,10 @@ /** * Immutable collection of objects. * + * Example + * - $v = ArrayOfObjects::fromItems(new \stdClass()); + * $v->count(); // 1 + * * @template TItem of object * * @template-extends ArrayTypeAbstract diff --git a/src/ArrayType/ArrayUndefined.php b/src/ArrayType/ArrayUndefined.php index 412e200a..a91985a6 100755 --- a/src/ArrayType/ArrayUndefined.php +++ b/src/ArrayType/ArrayUndefined.php @@ -10,6 +10,10 @@ /** * Immutable undefined collection. * + * Example + * - $v = ArrayUndefined::create(); + * $v->isEmpty(); // true + * * @extends ArrayTypeAbstract * * @psalm-immutable