From ea75e119bf1b42a442fb0720159c4a36032cea93 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 25 Apr 2026 00:43:33 +0000
Subject: [PATCH 01/22] Add array shape property parsing
Agent-Logs-Url: https://github.com/voku/Arrayy/sessions/86dc53b6-73f3-44e2-8b93-8d68b6923d9f
Co-authored-by: voku <264695+voku@users.noreply.github.com>
---
src/Arrayy.php | 153 ++++++++++++++++++++++------
src/TypeCheck/TypeCheckPhpDoc.php | 36 +++++--
tests/TypeCheckCoreCoverageTest.php | 75 ++++++++++++++
3 files changed, 227 insertions(+), 37 deletions(-)
diff --git a/src/Arrayy.php b/src/Arrayy.php
index 4a060eb..99e1da5 100644
--- a/src/Arrayy.php
+++ b/src/Arrayy.php
@@ -93,6 +93,11 @@ class Arrayy extends \ArrayObject implements \IteratorAggregate, \ArrayAccess, \
*/
protected $properties = [];
+ /**
+ * @var array
+ */
+ protected $optionalProperties = [];
+
/**
* Initializes
*
@@ -7614,34 +7619,24 @@ protected function getDirection($direction): int
protected function getPropertiesFromPhpDoc()
{
static $PROPERTY_CACHE = [];
+ static $OPTIONAL_PROPERTY_CACHE = [];
$cacheKey = 'Class::' . static::class;
if (isset($PROPERTY_CACHE[$cacheKey])) {
+ $this->optionalProperties = $OPTIONAL_PROPERTY_CACHE[$cacheKey] ?? [];
+
return $PROPERTY_CACHE[$cacheKey];
}
$properties = $this->getPropertiesFromNativeDefinitions();
+ $optionalProperties = [];
$reflector = new \ReflectionClass($this);
$factory = \phpDocumentor\Reflection\DocBlockFactory::createInstance();
$docComment = $reflector->getDocComment();
if ($docComment) {
$docblock = $factory->create($docComment);
- /** @var \phpDocumentor\Reflection\DocBlock\Tags\Property $tag */
- foreach ($docblock->getTagsByName('property') as $tag) {
- $typeName = $tag->getVariableName();
- /** @var string|null $typeName */
- if (
- $typeName !== null
- &&
- isset($properties[$typeName]) === false
- ) {
- $typeCheckPhpDoc = TypeCheckPhpDoc::fromPhpDocumentorProperty($tag, $typeName);
- if ($typeCheckPhpDoc !== null) {
- $properties[$typeName] = $typeCheckPhpDoc;
- }
- }
- }
+ $this->addPropertiesFromDocBlock($docblock, $properties, $optionalProperties);
}
/** @noinspection PhpAssignmentInConditionInspection */
@@ -7649,25 +7644,120 @@ protected function getPropertiesFromPhpDoc()
$docComment = $reflector->getDocComment();
if ($docComment) {
$docblock = $factory->create($docComment);
- /** @var \phpDocumentor\Reflection\DocBlock\Tags\Property $tag */
- foreach ($docblock->getTagsByName('property') as $tag) {
- $typeName = $tag->getVariableName();
- /** @var string|null $typeName */
- if ($typeName !== null) {
- if (isset($properties[$typeName])) {
- continue;
- }
+ $this->addPropertiesFromDocBlock($docblock, $properties, $optionalProperties);
+ }
+ }
- $typeCheckPhpDoc = TypeCheckPhpDoc::fromPhpDocumentorProperty($tag, $typeName);
- if ($typeCheckPhpDoc !== null) {
- $properties[$typeName] = $typeCheckPhpDoc;
- }
+ $this->optionalProperties = $optionalProperties;
+ $OPTIONAL_PROPERTY_CACHE[$cacheKey] = $optionalProperties;
+
+ return $PROPERTY_CACHE[$cacheKey] = $properties;
+ }
+
+ /**
+ * @param \phpDocumentor\Reflection\DocBlock $docblock
+ * @param TypeCheckInterface[] $properties
+ * @param array $optionalProperties
+ *
+ * @return void
+ */
+ private function addPropertiesFromDocBlock($docblock, array &$properties, array &$optionalProperties): void
+ {
+ $propertyTags = $docblock->getTagsByName('property');
+ $arrayShapeItems = $this->getArrayShapeItemsFromDocBlock($docblock);
+
+ if ($propertyTags !== [] && $arrayShapeItems !== []) {
+ throw new \TypeError('Use either @property tags or array-shape annotations for Arrayy property definitions, not both.');
+ }
+
+ /** @var \phpDocumentor\Reflection\DocBlock\Tags\Property $tag */
+ foreach ($propertyTags as $tag) {
+ $typeName = $tag->getVariableName();
+ /** @var string|null $typeName */
+ if (
+ $typeName !== null
+ &&
+ isset($properties[$typeName]) === false
+ ) {
+ $typeCheckPhpDoc = TypeCheckPhpDoc::fromPhpDocumentorProperty($tag, $typeName);
+ if ($typeCheckPhpDoc !== null) {
+ $properties[$typeName] = $typeCheckPhpDoc;
+ unset($optionalProperties[$typeName]);
+ }
+ }
+ }
+
+ foreach ($arrayShapeItems as $item) {
+ $typeName = (string) $item->getKey();
+ if ($typeName === '') {
+ continue;
+ }
+
+ $typeName = \trim($typeName, '\'"');
+ if (isset($properties[$typeName])) {
+ continue;
+ }
+
+ $typeCheckPhpDoc = TypeCheckPhpDoc::fromDocTypeObject($typeName, $item->getValue());
+ if ($typeCheckPhpDoc !== null) {
+ $properties[$typeName] = $typeCheckPhpDoc;
+ if ($item->isOptional()) {
+ $optionalProperties[$typeName] = true;
+ } else {
+ unset($optionalProperties[$typeName]);
+ }
+ }
+ }
+ }
+
+ /**
+ * @param \phpDocumentor\Reflection\DocBlock $docblock
+ *
+ * @return \phpDocumentor\Reflection\PseudoTypes\ArrayShapeItem[]
+ */
+ private function getArrayShapeItemsFromDocBlock($docblock): array
+ {
+ if (!\class_exists('\phpDocumentor\Reflection\PseudoTypes\ArrayShape')) {
+ return [];
+ }
+
+ $items = [];
+ foreach ($docblock->getTagsByName('template') as $tag) {
+ if (
+ $tag instanceof \phpDocumentor\Reflection\DocBlock\Tags\Template
+ &&
+ $tag->getBound() instanceof \phpDocumentor\Reflection\PseudoTypes\ArrayShape
+ ) {
+ foreach ($tag->getBound()->getItems() as $item) {
+ $items[] = $item;
+ }
+ }
+ }
+
+ foreach ($docblock->getTagsByName('extends') as $tag) {
+ if (!$tag instanceof \phpDocumentor\Reflection\DocBlock\Tags\Extends_) {
+ continue;
+ }
+
+ $type = $tag->getType();
+ if (
+ !$type instanceof \phpDocumentor\Reflection\PseudoTypes\Generic
+ ||
+ \in_array(\ltrim((string) $type->getFqsen(), '\\'), [self::class, ArrayyStrict::class], true) === false
+ ) {
+ continue;
+ }
+
+ foreach ($type->getTypes() as $genericType) {
+ if ($genericType instanceof \phpDocumentor\Reflection\PseudoTypes\ArrayShape) {
+ foreach ($genericType->getItems() as $item) {
+ $items[] = $item;
}
}
}
}
- return $PROPERTY_CACHE[$cacheKey] = $properties;
+ return $items;
}
/**
@@ -8044,15 +8134,16 @@ protected function setInitialValuesAndProperties(array &$data, bool $checkProper
/** @var TypeCheckInterface[] $properties */
$properties = $this->properties;
+ $requiredProperties = \array_diff_key($properties, $this->optionalProperties);
if (
$this->checkPropertiesMismatchInConstructor === true
&&
\count($data) !== 0
&&
- \count(\array_diff_key($properties, $data)) > 0
+ \count(\array_diff_key($requiredProperties, $data)) > 0
) {
- throw new \TypeError('Property mismatch - input: ' . \print_r(\array_keys($data), true) . ' | expected: ' . \print_r(\array_keys($properties), true));
+ throw new \TypeError('Property mismatch - input: ' . \print_r(\array_keys($data), true) . ' | expected: ' . \print_r(\array_keys($requiredProperties), true));
}
}
@@ -8194,7 +8285,7 @@ private function checkType($key, $value)
&&
$this->checkPropertiesMismatch === true
) {
- throw new \TypeError('The key "' . $key . '" does not exists as "@property" phpdoc. (' . \get_class($this) . ').');
+ throw new \TypeError('The key "' . $key . '" does not exists as a property definition. (' . \get_class($this) . ').');
}
if (isset($this->properties[self::ARRAYY_HELPER_TYPES_FOR_ALL_PROPERTIES])) {
diff --git a/src/TypeCheck/TypeCheckPhpDoc.php b/src/TypeCheck/TypeCheckPhpDoc.php
index 3aaa713..12bd384 100644
--- a/src/TypeCheck/TypeCheckPhpDoc.php
+++ b/src/TypeCheck/TypeCheckPhpDoc.php
@@ -52,17 +52,22 @@ public static function fromPhpDocumentorProperty(\phpDocumentor\Reflection\DocBl
$property = $propertyTmp;
}
+ return self::fromDocTypeObject($property, $phpDocumentorReflectionProperty->getType());
+ }
+
+ /**
+ * @param string $property
+ * @param \phpDocumentor\Reflection\Type|null $type
+ *
+ * @return self|null
+ */
+ public static function fromDocTypeObject(string $property, $type)
+ {
$tmpObject = new \stdClass();
$tmpObject->{$property} = null;
$tmpReflection = new self((new \ReflectionProperty($tmpObject, $property))->getName());
- $type = $phpDocumentorReflectionProperty->getType();
-
- /** @noinspection PhpSillyAssignmentInspection */
- /** @var Type|null $type */
- $type = $type;
-
if ($type) {
$tmpReflection->hasTypeDeclaration = true;
@@ -159,6 +164,25 @@ public static function parseDocTypeObject($type)
return $types;
}
+ if ($type instanceof \phpDocumentor\Reflection\Types\Nullable) {
+ $typeTmp = self::parseDocTypeObject($type->getActualType());
+ if (\is_array($typeTmp) === true) {
+ $typeTmp[] = 'null';
+
+ return $typeTmp;
+ }
+
+ return [$typeTmp, 'null'];
+ }
+
+ if (
+ \class_exists('\phpDocumentor\Reflection\PseudoTypes\ArrayShape')
+ &&
+ $type instanceof \phpDocumentor\Reflection\PseudoTypes\ArrayShape
+ ) {
+ return 'array';
+ }
+
if ($type instanceof \phpDocumentor\Reflection\Types\Array_) {
$valueTypeTmp = $type->getValueType()->__toString();
if ($valueTypeTmp !== 'mixed') {
diff --git a/tests/TypeCheckCoreCoverageTest.php b/tests/TypeCheckCoreCoverageTest.php
index cd11f2b..7836643 100644
--- a/tests/TypeCheckCoreCoverageTest.php
+++ b/tests/TypeCheckCoreCoverageTest.php
@@ -346,6 +346,58 @@ public function testTypeCheckCallbackValidatesAndSupportsNullableValues(): void
$failingCheck->checkType($invalidValue);
}
+ public function testArrayShapeTemplateProvidesPropertyDefinitions(): void
+ {
+ $meta = TypeCheckArrayShapeUserData::meta();
+ $model = new TypeCheckArrayShapeUserData([
+ $meta->id => 1,
+ $meta->firstName => 'Lars',
+ $meta->lastName => 'Moelleken',
+ $meta->infos => ['foo'],
+ ]);
+
+ static::assertSame('id', $meta->id);
+ static::assertSame('city', $meta->city);
+ static::assertSame('Lars', $model[$meta->firstName]);
+ }
+
+ public function testArrayShapeTemplateRejectsInvalidPropertyTypes(): void
+ {
+ $this->expectException(\TypeError::class);
+ $this->expectExceptionMessage('Invalid type: expected "infos" to be of type {string[]}');
+
+ $meta = TypeCheckArrayShapeUserData::meta();
+ new TypeCheckArrayShapeUserData([
+ $meta->id => 1,
+ $meta->firstName => 'Lars',
+ $meta->lastName => 'Moelleken',
+ $meta->infos => [1],
+ ]);
+ }
+
+ public function testArrayShapeTemplateRejectsUnknownProperties(): void
+ {
+ $this->expectException(\TypeError::class);
+ $this->expectExceptionMessage('The key "unknown" does not exists');
+
+ $meta = TypeCheckArrayShapeUserData::meta();
+ new TypeCheckArrayShapeUserData([
+ $meta->id => 1,
+ $meta->firstName => 'Lars',
+ $meta->lastName => 'Moelleken',
+ $meta->infos => ['foo'],
+ 'unknown' => 'value',
+ ]);
+ }
+
+ public function testPropertyTagsAndArrayShapeTemplateCannotBeMixed(): void
+ {
+ $this->expectException(\TypeError::class);
+ $this->expectExceptionMessage('Use either @property tags or array-shape annotations');
+
+ new TypeCheckMixedPropertyAnnotationsData(['id' => 1]);
+ }
+
/**
* @return iterable}>
*/
@@ -392,3 +444,26 @@ final class TypeCheckDocOverridesNativeFixture
*/
public string $value = '';
}
+
+/**
+ * @template T of array{id: int, firstName: int|string, lastName: string, city?: \Arrayy\tests\CityData|null, infos: string[]}
+ * @extends \Arrayy\Arrayy, value-of>
+ */
+final class TypeCheckArrayShapeUserData extends \Arrayy\Arrayy
+{
+ protected $checkPropertyTypes = true;
+
+ protected $checkForMissingPropertiesInConstructor = true;
+
+ protected $checkPropertiesMismatchInConstructor = true;
+}
+
+/**
+ * @property int $legacyId
+ * @template T of array{id: int}
+ * @extends \Arrayy\Arrayy, value-of>
+ */
+final class TypeCheckMixedPropertyAnnotationsData extends \Arrayy\Arrayy
+{
+ protected $checkPropertyTypes = true;
+}
From bb24d8c2f33774a6b2c087e4e56e5902013e125b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 25 Apr 2026 00:45:26 +0000
Subject: [PATCH 02/22] Document array shape property definitions
Agent-Logs-Url: https://github.com/voku/Arrayy/sessions/86dc53b6-73f3-44e2-8b93-8d68b6923d9f
Co-authored-by: voku <264695+voku@users.noreply.github.com>
---
README.md | 53 +++++++++++--------------------
build/docs/base.md | 45 ++++++++------------------
src/Arrayy.php | 8 ++---
src/TypeCheck/TypeCheckPhpDoc.php | 2 +-
4 files changed, 37 insertions(+), 71 deletions(-)
diff --git a/README.md b/README.md
index 7f0f8cf..09e7d1e 100644
--- a/README.md
+++ b/README.md
@@ -23,7 +23,7 @@ A PHP array manipulation library. Compatible with PHP 8+
* [Installation](#installation-via-composer-require)
* [Multidimensional ArrayAccess](#multidimensional-arrayaccess)
-* [PhpDoc @property checking](#phpdoc-property-checking)
+* [PhpDoc array-shape / property checking](#phpdoc-array-shape--property-checking)
* [OO and Chaining](#oo-and-chaining)
* [Collections](#collections)
* [Pre-Defined Typified Collections](#pre-defined-typified-collections)
@@ -116,18 +116,14 @@ $arrayy->Lars = array('lastname' => 'Müller');
$arrayy->Lars->lastname; // 'Müller'
```
-## PhpDoc @property checking
+## PhpDoc array-shape / property checking
-The library offers type checking for `@property` phpdoc-class-comments and native declared properties.
+The library offers type checking for phpdoc array-shape annotations, legacy `@property` phpdoc-class-comments, and native declared properties. Prefer the array-shape form because it can reuse the `Arrayy` template for IDE autocompletion and static-analysis support. Do not combine array-shape annotations and `@property` tags on the same model.
```php
/**
- * @property int $id
- * @property int|string $firstName
- * @property string $lastName
- * @property null|City $city
- *
- * @extends \Arrayy\Arrayy
+ * @template T of array{id: int, firstName: int|string, lastName: string, city?: City|null}
+ * @extends \Arrayy\Arrayy,value-of>
*/
class User extends \Arrayy\Arrayy
{
@@ -137,11 +133,8 @@ class User extends \Arrayy\Arrayy
}
/**
- * @property string|null $plz
- * @property string $name
- * @property string[] $infos
- *
- * @extends \Arrayy\Arrayy
+ * @template T of array{plz: string|null, name: string, infos: string[]}
+ * @extends \Arrayy\Arrayy,value-of>
*/
class City extends \Arrayy\Arrayy
{
@@ -201,7 +194,7 @@ NativeCity::createFromJsonMapper(
var_dump($nativeMeta->infos); // 'infos'
```
-- "checkPropertyTypes": activate the type checking for all defined `@property` tags and native declared properties
+- "checkPropertyTypes": activate the type checking for all defined array-shape keys, `@property` tags, and native declared properties
- "checkPropertiesMismatchInConstructor": activate the property mismatch check, so you can only add an
array with all needed properties (or an empty array) into the constructor
- use a property-level `@var` annotation such as `/** @var string[] */` if a native `array` property needs element-type validation
@@ -218,11 +211,8 @@ echo a(['fòô', 'bàř', 'bàř'])->unique()->reverse()->implode(','); // 'bà
complex example:
```php
/**
- * @property int $id
- * @property string $firstName
- * @property string $lastName
- *
- * @extends \Arrayy\Arrayy
+ * @template T of array{id: int, firstName: string, lastName: string}
+ * @extends \Arrayy\Arrayy,value-of>
*/
class User extends \Arrayy\Arrayy
{
@@ -486,12 +476,8 @@ Create an new Arrayy object via JSON and fill sub-objects is possible.
namespace Arrayy\tests;
/**
- * @property int $id
- * @property int|string $firstName
- * @property string $lastName
- * @property \Arrayy\tests\CityData|null $city
- *
- * @extends \Arrayy\Arrayy
+ * @template T of array{id: int, firstName: int|string, lastName: string, city?: \Arrayy\tests\CityData|null}
+ * @extends \Arrayy\Arrayy,value-of>
*/
class UserData extends \Arrayy\Arrayy
{
@@ -501,11 +487,8 @@ class UserData extends \Arrayy\Arrayy
}
/**
- * @property string|null $plz
- * @property string $name
- * @property string[] $infos
- *
- * @extends \Arrayy\Arrayy
+ * @template T of array{plz: string|null, name: string, infos: string[]}
+ * @extends \Arrayy\Arrayy,value-of>
*/
class CityData extends \Arrayy\Arrayy
{
@@ -1369,8 +1352,8 @@ Create an new Arrayy object via JSON.
↑
Create an instance from JSON using the built-in mapper.
-For Arrayy models with property checks enabled, both phpdoc `@property`
-definitions and native declared properties are used for metadata and type checks.
+For Arrayy models with property checks enabled, phpdoc array-shape annotations,
+legacy `@property` definitions, and native declared properties are used for metadata and type checks.
Add a property-level `@var` annotation if a native `array` property also needs
element-type validation.
@@ -2833,8 +2816,8 @@ a($array1)->mergePrependNewIndex($array2); // Arrayy[0 => 'foo', 1 => 'bar2', 2
## static meta(): ArrayyMeta|mixed|static
↑
-Return a meta object with property names from phpdoc `@property` tags and
-native declared properties.
+Return a meta object with property names from phpdoc array-shape annotations,
+`@property` tags, and native declared properties.
**Parameters:**
__nothing__
diff --git a/build/docs/base.md b/build/docs/base.md
index 1051533..fb699cf 100644
--- a/build/docs/base.md
+++ b/build/docs/base.md
@@ -22,7 +22,7 @@ A PHP array manipulation library. Compatible with PHP 8+
* [Installation](#installation-via-composer-require)
* [Multidimensional ArrayAccess](#multidimensional-arrayaccess)
-* [PhpDoc @property checking](#phpdoc-property-checking)
+* [PhpDoc array-shape / property checking](#phpdoc-array-shape--property-checking)
* [OO and Chaining](#oo-and-chaining)
* [Collections](#collections)
* [Pre-Defined Typified Collections](#pre-defined-typified-collections)
@@ -115,18 +115,14 @@ $arrayy->Lars = array('lastname' => 'Müller');
$arrayy->Lars->lastname; // 'Müller'
```
-## PhpDoc @property checking
+## PhpDoc array-shape / property checking
-The library offers type checking for `@property` phpdoc-class-comments and native declared properties.
+The library offers type checking for phpdoc array-shape annotations, legacy `@property` phpdoc-class-comments, and native declared properties. Prefer the array-shape form because it can reuse the `Arrayy` template for IDE autocompletion and static-analysis support. Do not combine array-shape annotations and `@property` tags on the same model.
```php
/**
- * @property int $id
- * @property int|string $firstName
- * @property string $lastName
- * @property null|City $city
- *
- * @extends \Arrayy\Arrayy
+ * @template T of array{id: int, firstName: int|string, lastName: string, city?: City|null}
+ * @extends \Arrayy\Arrayy,value-of>
*/
class User extends \Arrayy\Arrayy
{
@@ -136,11 +132,8 @@ class User extends \Arrayy\Arrayy
}
/**
- * @property string|null $plz
- * @property string $name
- * @property string[] $infos
- *
- * @extends \Arrayy\Arrayy
+ * @template T of array{plz: string|null, name: string, infos: string[]}
+ * @extends \Arrayy\Arrayy,value-of>
*/
class City extends \Arrayy\Arrayy
{
@@ -200,7 +193,7 @@ NativeCity::createFromJsonMapper(
var_dump($nativeMeta->infos); // 'infos'
```
-- "checkPropertyTypes": activate the type checking for all defined `@property` tags and native declared properties
+- "checkPropertyTypes": activate the type checking for all defined array-shape keys, `@property` tags, and native declared properties
- "checkPropertiesMismatchInConstructor": activate the property mismatch check, so you can only add an
array with all needed properties (or an empty array) into the constructor
- use a property-level `@var` annotation such as `/** @var string[] */` if a native `array` property needs element-type validation
@@ -217,11 +210,8 @@ echo a(['fòô', 'bàř', 'bàř'])->unique()->reverse()->implode(','); // 'bà
complex example:
```php
/**
- * @property int $id
- * @property string $firstName
- * @property string $lastName
- *
- * @extends \Arrayy\Arrayy
+ * @template T of array{id: int, firstName: string, lastName: string}
+ * @extends \Arrayy\Arrayy,value-of>
*/
class User extends \Arrayy\Arrayy
{
@@ -485,12 +475,8 @@ Create an new Arrayy object via JSON and fill sub-objects is possible.
namespace Arrayy\tests;
/**
- * @property int $id
- * @property int|string $firstName
- * @property string $lastName
- * @property \Arrayy\tests\CityData|null $city
- *
- * @extends \Arrayy\Arrayy
+ * @template T of array{id: int, firstName: int|string, lastName: string, city?: \Arrayy\tests\CityData|null}
+ * @extends \Arrayy\Arrayy,value-of>
*/
class UserData extends \Arrayy\Arrayy
{
@@ -500,11 +486,8 @@ class UserData extends \Arrayy\Arrayy
}
/**
- * @property string|null $plz
- * @property string $name
- * @property string[] $infos
- *
- * @extends \Arrayy\Arrayy
+ * @template T of array{plz: string|null, name: string, infos: string[]}
+ * @extends \Arrayy\Arrayy,value-of>
*/
class CityData extends \Arrayy\Arrayy
{
diff --git a/src/Arrayy.php b/src/Arrayy.php
index 99e1da5..12db386 100644
--- a/src/Arrayy.php
+++ b/src/Arrayy.php
@@ -3251,8 +3251,8 @@ public function getArray(
/**
* Create an instance from JSON using the built-in mapper.
*
- * For Arrayy models with property checks enabled, both phpdoc `@property`
- * definitions and native declared properties are used for metadata and type checks.
+ * For Arrayy models with property checks enabled, phpdoc array-shape annotations,
+ * legacy `@property` definitions, and native declared properties are used for metadata and type checks.
* Add a property-level `@var` annotation if a native `array` property also needs
* element-type validation.
*
@@ -4724,8 +4724,8 @@ public function mergePrependNewIndex(array $array = [], bool $recursive = false)
}
/**
- * Return a meta object with property names from phpdoc `@property` tags and
- * native declared properties.
+ * Return a meta object with property names from phpdoc array-shape annotations,
+ * `@property` tags, and native declared properties.
*
* @return ArrayyMeta|mixed|static
*/
diff --git a/src/TypeCheck/TypeCheckPhpDoc.php b/src/TypeCheck/TypeCheckPhpDoc.php
index 12bd384..2c03246 100644
--- a/src/TypeCheck/TypeCheckPhpDoc.php
+++ b/src/TypeCheck/TypeCheckPhpDoc.php
@@ -59,7 +59,7 @@ public static function fromPhpDocumentorProperty(\phpDocumentor\Reflection\DocBl
* @param string $property
* @param \phpDocumentor\Reflection\Type|null $type
*
- * @return self|null
+ * @return self
*/
public static function fromDocTypeObject(string $property, $type)
{
From f33fe40c247f3751457294cd259dbd5f3401d77a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 25 Apr 2026 00:46:23 +0000
Subject: [PATCH 03/22] Address array shape review feedback
Agent-Logs-Url: https://github.com/voku/Arrayy/sessions/86dc53b6-73f3-44e2-8b93-8d68b6923d9f
Co-authored-by: voku <264695+voku@users.noreply.github.com>
---
src/Arrayy.php | 4 +---
tests/NativePropertyTypeTest.php | 4 ++--
tests/TypeCheckCoreCoverageTest.php | 2 +-
3 files changed, 4 insertions(+), 6 deletions(-)
diff --git a/src/Arrayy.php b/src/Arrayy.php
index 12db386..0caf37a 100644
--- a/src/Arrayy.php
+++ b/src/Arrayy.php
@@ -7703,8 +7703,6 @@ private function addPropertiesFromDocBlock($docblock, array &$properties, array
$properties[$typeName] = $typeCheckPhpDoc;
if ($item->isOptional()) {
$optionalProperties[$typeName] = true;
- } else {
- unset($optionalProperties[$typeName]);
}
}
}
@@ -8285,7 +8283,7 @@ private function checkType($key, $value)
&&
$this->checkPropertiesMismatch === true
) {
- throw new \TypeError('The key "' . $key . '" does not exists as a property definition. (' . \get_class($this) . ').');
+ throw new \TypeError('The key "' . $key . '" does not exist as a property definition. (' . \get_class($this) . ').');
}
if (isset($this->properties[self::ARRAYY_HELPER_TYPES_FOR_ALL_PROPERTIES])) {
diff --git a/tests/NativePropertyTypeTest.php b/tests/NativePropertyTypeTest.php
index 2e8812d..1a13fcf 100644
--- a/tests/NativePropertyTypeTest.php
+++ b/tests/NativePropertyTypeTest.php
@@ -78,7 +78,7 @@ public function testNativeMetaIncludesPropertiesDeclaredNatively()
public function testNativeTypedPropertiesRejectUnknownKeysWhenMismatchCheckIsEnabled()
{
$this->expectException(\TypeError::class);
- $this->expectExceptionMessage('The key "unknown" does not exists');
+ $this->expectExceptionMessage('The key "unknown" does not exist');
new NativeCityData(
[
@@ -200,7 +200,7 @@ public function testNativePropertyTypeIsEnforcedAfterConstruction()
public function testNativePropertyMismatchIsEnforcedAfterConstruction()
{
$this->expectException(\TypeError::class);
- $this->expectExceptionMessage('The key "unknown" does not exists');
+ $this->expectExceptionMessage('The key "unknown" does not exist');
$cityMeta = NativeCityData::meta();
$model = new NativeCityData([
diff --git a/tests/TypeCheckCoreCoverageTest.php b/tests/TypeCheckCoreCoverageTest.php
index 7836643..0c0029a 100644
--- a/tests/TypeCheckCoreCoverageTest.php
+++ b/tests/TypeCheckCoreCoverageTest.php
@@ -378,7 +378,7 @@ public function testArrayShapeTemplateRejectsInvalidPropertyTypes(): void
public function testArrayShapeTemplateRejectsUnknownProperties(): void
{
$this->expectException(\TypeError::class);
- $this->expectExceptionMessage('The key "unknown" does not exists');
+ $this->expectExceptionMessage('The key "unknown" does not exist');
$meta = TypeCheckArrayShapeUserData::meta();
new TypeCheckArrayShapeUserData([
From d766517f048c11f57ff3331d1180c09a6275ccc5 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 25 Apr 2026 00:46:59 +0000
Subject: [PATCH 04/22] Refine array shape annotation parsing
Agent-Logs-Url: https://github.com/voku/Arrayy/sessions/86dc53b6-73f3-44e2-8b93-8d68b6923d9f
Co-authored-by: voku <264695+voku@users.noreply.github.com>
---
src/Arrayy.php | 4 +++-
src/TypeCheck/TypeCheckPhpDoc.php | 2 ++
2 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/src/Arrayy.php b/src/Arrayy.php
index 0caf37a..cc1217c 100644
--- a/src/Arrayy.php
+++ b/src/Arrayy.php
@@ -7724,6 +7724,8 @@ private function getArrayShapeItemsFromDocBlock($docblock): array
if (
$tag instanceof \phpDocumentor\Reflection\DocBlock\Tags\Template
&&
+ $tag->getTemplateName() === 'T'
+ &&
$tag->getBound() instanceof \phpDocumentor\Reflection\PseudoTypes\ArrayShape
) {
foreach ($tag->getBound()->getItems() as $item) {
@@ -7741,7 +7743,7 @@ private function getArrayShapeItemsFromDocBlock($docblock): array
if (
!$type instanceof \phpDocumentor\Reflection\PseudoTypes\Generic
||
- \in_array(\ltrim((string) $type->getFqsen(), '\\'), [self::class, ArrayyStrict::class], true) === false
+ !\in_array(\ltrim((string) $type->getFqsen(), '\\'), [self::class, ArrayyStrict::class], true)
) {
continue;
}
diff --git a/src/TypeCheck/TypeCheckPhpDoc.php b/src/TypeCheck/TypeCheckPhpDoc.php
index 2c03246..74740e9 100644
--- a/src/TypeCheck/TypeCheckPhpDoc.php
+++ b/src/TypeCheck/TypeCheckPhpDoc.php
@@ -59,6 +59,8 @@ public static function fromPhpDocumentorProperty(\phpDocumentor\Reflection\DocBl
* @param string $property
* @param \phpDocumentor\Reflection\Type|null $type
*
+ * @phpstan-param \phpDocumentor\Reflection\Type|null $type
+ *
* @return self
*/
public static function fromDocTypeObject(string $property, $type)
From d91e3b128a53843ac2bd55a9d524e0f8574547ea Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 25 Apr 2026 00:59:55 +0000
Subject: [PATCH 05/22] Add regression tests for array-shape property feature
Agent-Logs-Url: https://github.com/voku/Arrayy/sessions/c75c7d50-5368-471f-9f9e-ced21c0245bd
Co-authored-by: voku <264695+voku@users.noreply.github.com>
---
tests/TypeCheckCoreCoverageTest.php | 243 ++++++++++++++++++++++++++++
1 file changed, 243 insertions(+)
diff --git a/tests/TypeCheckCoreCoverageTest.php b/tests/TypeCheckCoreCoverageTest.php
index 0c0029a..4ef2016 100644
--- a/tests/TypeCheckCoreCoverageTest.php
+++ b/tests/TypeCheckCoreCoverageTest.php
@@ -398,6 +398,184 @@ public function testPropertyTagsAndArrayShapeTemplateCannotBeMixed(): void
new TypeCheckMixedPropertyAnnotationsData(['id' => 1]);
}
+ /**
+ * Risk: optional shape keys skip the constructor-mismatch check but must still go
+ * through `checkType()` when a value is actually supplied; removing the per-key
+ * `$this->properties[$key]->checkType($value)` call would silently accept any value.
+ */
+ public function testArrayShapeOptionalKeyIsTypeCheckedWhenPresent(): void
+ {
+ $this->expectException(\TypeError::class);
+ $this->expectExceptionMessage('Invalid type');
+
+ $meta = TypeCheckArrayShapeUserData::meta();
+ new TypeCheckArrayShapeUserData([
+ $meta->id => 1,
+ $meta->firstName => 'Lars',
+ $meta->lastName => 'Moelleken',
+ $meta->infos => ['foo'],
+ $meta->city => new \stdClass(), // wrong type – must throw
+ ]);
+ }
+
+ /**
+ * Risk: "optional" means the array key may be absent from the input, not that null
+ * is a valid value. If the `isOptional` flag were incorrectly widened to imply
+ * nullable, the type check would be silently skipped for null values.
+ *
+ * This exercises the post-construction (offsetSet) path which always runs checkType.
+ */
+ public function testArrayShapeOptionalNonNullableKeyRejectsNull(): void
+ {
+ $this->expectException(\TypeError::class);
+
+ // TypeCheckArrayShapeScoreData defines score?: int (optional, not nullable)
+ $model = new TypeCheckArrayShapeScoreData([]);
+ $model['score'] = null; // offsetSet always calls checkType; null ≠ int → throws
+ }
+
+ /**
+ * Risk: if the `$requiredProperties = array_diff_key(…)` split were removed and all
+ * shape properties were treated as required, constructing the same model a second
+ * time (the cached path) would fail to exclude optional keys from the mismatch check.
+ *
+ * Uses a dedicated fixture so the cache state is deterministic within the test.
+ */
+ public function testArrayShapeCachingPreservesOptionalPropertiesOnSecondInstantiation(): void
+ {
+ // First construction hits the uncached path.
+ $first = new TypeCheckArrayShapeCacheTestModel(['name' => 'Alice']);
+ // Second construction hits the CACHED path – optionalProperties must be restored.
+ $second = new TypeCheckArrayShapeCacheTestModel(['name' => 'Bob']);
+
+ static::assertSame('Alice', $first['name']);
+ static::assertSame('Bob', $second['name']);
+ // Neither construction should have thrown a "Property mismatch" error because
+ // 'tag' is optional; if the cache fix is missing, the second would throw.
+ }
+
+ /**
+ * Risk: the guard `$tag->getTemplateName() === 'T'` ensures that only the canonical
+ * `@template T of array{…}` form is used as a property map. If the check were
+ * removed, ANY template bound to an array shape would be treated as property metadata.
+ */
+ public function testArrayShapeTemplateNameOtherThanTIsIgnored(): void
+ {
+ // TypeCheckArrayShapeWrongTemplateName uses @template Data of array{id: int}.
+ // The name guard must skip it, so no properties are registered and any key is
+ // accepted without type checking.
+ $model = new TypeCheckArrayShapeWrongTemplateName(['id' => 'not-an-int']);
+ static::assertSame('not-an-int', $model['id']);
+ }
+
+ /**
+ * Risk: the @extends inline-shape form (`@extends Arrayy`) must be
+ * parsed even without a preceding @template T preamble. Removing that branch of
+ * `getArrayShapeItemsFromDocBlock()` would silently drop this annotation style.
+ */
+ public function testArrayShapeInlineExtendsFormParsesShapeProperties(): void
+ {
+ $meta = TypeCheckArrayShapeExtendsOnlyData::meta();
+ static::assertSame('score', $meta->score);
+
+ $model = new TypeCheckArrayShapeExtendsOnlyData([$meta->score => 42]);
+ static::assertSame(42, $model[$meta->score]);
+ }
+
+ /**
+ * Risk: the @extends FQCN guard (`in_array(…, [Arrayy::class, ArrayyStrict::class])`)
+ * must prevent shapes on unrelated classes from being parsed as property metadata.
+ * Removing the guard would inject spurious property definitions.
+ */
+ public function testNonArrayyExtendsShapeIsNotParsed(): void
+ {
+ // TypeCheckNonArrayyExtendsData has @extends stdClass.
+ // The FQCN guard skips stdClass, so no shape properties should be registered;
+ // any key/value combination must be accepted without type checking.
+ $model = new TypeCheckNonArrayyExtendsData(['id' => 'not-an-int', 'extra' => true]);
+ static::assertSame('not-an-int', $model['id']);
+ static::assertSame(true, $model['extra']);
+ }
+
+ /**
+ * Risk: `fromDocTypeObject()` is called with $type = null when a shape item carries
+ * no explicit type annotation. If the null guard (`if ($type) { … }`) were removed,
+ * the function would attempt `parseDocTypeObject(null)` and crash. The checker
+ * it returns must have an empty types list.
+ *
+ * A checker with empty types is maximally restrictive (it throws for all values),
+ * which is intentional; callers must not treat "no type annotation" as "accept all".
+ */
+ public function testFromDocTypeObjectWithNullTypeReturnsCheckerWithEmptyTypes(): void
+ {
+ $checker = TypeCheckPhpDoc::fromDocTypeObject('myProp', null);
+
+ static::assertSame([], $checker->getTypes());
+
+ // An empty types list means there is no declared type that can match, so
+ // checkType() throws for every value – verify the production-safe null path.
+ $this->expectException(\TypeError::class);
+ $this->expectExceptionMessage('expected "myProp" to be of type {}');
+ $nullValue = null;
+ $checker->checkType($nullValue);
+ }
+
+ /**
+ * Risk: all shape keys, including optional ones, must be surfaced by meta() so that
+ * callers can use `$meta->city` as a type-safe key reference even when city may be
+ * absent from the constructor input. If optional keys were excluded from the property
+ * map, meta() would return empty strings for them.
+ */
+ public function testArrayShapeMetaIncludesAllKeysIncludingOptional(): void
+ {
+ $meta = TypeCheckArrayShapeUserData::meta();
+
+ static::assertSame('id', $meta->id);
+ static::assertSame('firstName', $meta->firstName);
+ static::assertSame('lastName', $meta->lastName);
+ static::assertSame('city', $meta->city); // optional key
+ static::assertSame('infos', $meta->infos);
+ }
+
+ /**
+ * Risk: post-construction writes via offsetSet must continue to be type-checked;
+ * removing checkType() from internalSet() would allow any value after construction.
+ */
+ public function testArrayShapePostConstructionTypeCheckEnforced(): void
+ {
+ $this->expectException(\TypeError::class);
+ $this->expectExceptionMessageMatches('#Invalid type: expected "id" to be of type \{int\}#');
+
+ $meta = TypeCheckArrayShapeUserData::meta();
+ $model = new TypeCheckArrayShapeUserData([
+ $meta->id => 1,
+ $meta->firstName => 'Lars',
+ $meta->lastName => 'M',
+ $meta->infos => [],
+ ]);
+ $model[$meta->id] = 'not-an-int'; // offsetSet → internalSet(…, true) → checkType
+ }
+
+ /**
+ * Risk: post-construction writes of unknown keys must be rejected when
+ * checkPropertiesMismatch is true; removing the key-existence guard in checkType()
+ * would allow new keys to be silently injected into a shape-typed model.
+ */
+ public function testArrayShapePostConstructionMismatchEnforced(): void
+ {
+ $this->expectException(\TypeError::class);
+ $this->expectExceptionMessage('The key "ghost" does not exist');
+
+ $meta = TypeCheckArrayShapeUserData::meta();
+ $model = new TypeCheckArrayShapeUserData([
+ $meta->id => 1,
+ $meta->firstName => 'Lars',
+ $meta->lastName => 'M',
+ $meta->infos => [],
+ ]);
+ $model['ghost'] = 'injected'; // must throw – 'ghost' is not in the shape
+ }
+
/**
* @return iterable}>
*/
@@ -467,3 +645,68 @@ final class TypeCheckMixedPropertyAnnotationsData extends \Arrayy\Arrayy
{
protected $checkPropertyTypes = true;
}
+
+/**
+ * Optional key whose value type does NOT include null. "Optional" means the key
+ * may be absent; null must still be rejected when the key is present.
+ *
+ * @template T of array{score?: int}
+ * @extends \Arrayy\Arrayy, value-of>
+ */
+final class TypeCheckArrayShapeScoreData extends \Arrayy\Arrayy
+{
+ protected $checkPropertyTypes = true;
+
+ protected $checkPropertiesMismatch = true;
+}
+
+/**
+ * Used exclusively by testArrayShapeCachingPreservesOptionalPropertiesOnSecondInstantiation
+ * to exercise the static-cache restore of $optionalProperties.
+ *
+ * @template T of array{name: string, tag?: string}
+ * @extends \Arrayy\Arrayy, value-of>
+ */
+final class TypeCheckArrayShapeCacheTestModel extends \Arrayy\Arrayy
+{
+ protected $checkPropertyTypes = true;
+
+ protected $checkPropertiesMismatch = true;
+
+ protected $checkPropertiesMismatchInConstructor = true;
+
+ protected $checkForMissingPropertiesInConstructor = true;
+}
+
+/**
+ * Uses the inline @extends form to define the shape, with no @template T preamble.
+ * Both `@template T of array{…}` and `@extends Arrayy` must be supported.
+ *
+ * @extends \Arrayy\Arrayy
+ */
+final class TypeCheckArrayShapeExtendsOnlyData extends \Arrayy\Arrayy
+{
+ protected $checkPropertyTypes = true;
+
+ protected $checkPropertiesMismatch = true;
+}
+
+/**
+ * Uses @template with a name other than T – must be silently ignored by the
+ * `getTemplateName() === 'T'` guard in getArrayShapeItemsFromDocBlock().
+ *
+ * @template Data of array{id: int}
+ * @extends \Arrayy\Arrayy
+ */
+final class TypeCheckArrayShapeWrongTemplateName extends \Arrayy\Arrayy
+{
+ protected $checkPropertyTypes = true;
+}
+
+/**
+ * @extends stdClass
+ */
+final class TypeCheckNonArrayyExtendsData extends \Arrayy\Arrayy
+{
+ protected $checkPropertyTypes = true;
+}
From f21ac9f4e125c14ab6d07f1e715f2f5c9f9996f7 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 25 Apr 2026 01:01:31 +0000
Subject: [PATCH 06/22] Add parseDocTypeObject and happy-path regression tests
Agent-Logs-Url: https://github.com/voku/Arrayy/sessions/c75c7d50-5368-471f-9f9e-ced21c0245bd
Co-authored-by: voku <264695+voku@users.noreply.github.com>
---
tests/TypeCheckCoreCoverageTest.php | 75 +++++++++++++++++++++++++++++
1 file changed, 75 insertions(+)
diff --git a/tests/TypeCheckCoreCoverageTest.php b/tests/TypeCheckCoreCoverageTest.php
index 4ef2016..9952249 100644
--- a/tests/TypeCheckCoreCoverageTest.php
+++ b/tests/TypeCheckCoreCoverageTest.php
@@ -219,6 +219,81 @@ public function testFromPhpDocumentorPropertyParsesSupportedPseudoTypes(): void
static::assertSame(['string[]'], $arrayTypeCheck->getTypes());
}
+ /**
+ * Risk: `parseDocTypeObject()` has a Nullable branch that wraps the inner type in
+ * ['innerType', 'null']. If this branch is removed or mis-ordered relative to
+ * Compound handling, nullable @property types (e.g. `?City`) stop being accepted.
+ */
+ public function testFromDocTypeObjectNullableProducesNullableChecker(): void
+ {
+ $docBlock = DocBlockFactory::createInstance()->create(<<<'DOC'
+/**
+ * @property ?\ArrayObject $city
+ */
+DOC);
+ $tag = $docBlock->getTagsByName('property')[0];
+
+ $checker = TypeCheckPhpDoc::fromDocTypeObject('city', $tag->getType());
+
+ static::assertSame(['\\ArrayObject', 'null'], $checker->getTypes());
+
+ $nullValue = null;
+ $objectValue = new \ArrayObject();
+ static::assertTrue($checker->checkType($nullValue));
+ static::assertTrue($checker->checkType($objectValue));
+ }
+
+ /**
+ * Risk: `parseDocTypeObject()` has an ArrayShape branch that returns 'array' when a
+ * shape value type is itself a nested array-shape. If this branch is removed, nesting
+ * a shape inside a shape raises a "no branch matched" case and the type is silently
+ * returned as the raw toString representation instead of 'array'.
+ */
+ public function testFromDocTypeObjectNestedArrayShapeYieldsArrayType(): void
+ {
+ $docBlock = DocBlockFactory::createInstance()->create(<<<'DOC'
+/**
+ * @template T of array{data: array{x: int}}
+ */
+DOC);
+ $bound = $docBlock->getTagsByName('template')[0]->getBound();
+ $nestedShapeType = $bound->getItems()[0]->getValue(); // array{x: int}
+
+ $checker = TypeCheckPhpDoc::fromDocTypeObject('data', $nestedShapeType);
+
+ // A nested array shape is collapsed to 'array'; only structural shape keys of
+ // the *model* are registered as properties.
+ static::assertSame(['array'], $checker->getTypes());
+ $anyArray = ['x' => 42, 'extra' => true];
+ static::assertTrue($checker->checkType($anyArray));
+ }
+
+ /**
+ * Risk: happy path for a valid nested model value in an optional shape key. This
+ * exercises the `Object_` branch of parseDocTypeObject for nullable class types and
+ * ensures that a correctly-typed value for the `city` key is accepted at construction.
+ */
+ public function testArrayShapeOptionalKeyAcceptsValidObjectValue(): void
+ {
+ $meta = TypeCheckArrayShapeUserData::meta();
+ $city = new \Arrayy\tests\CityData([
+ 'plz' => null,
+ 'name' => 'Düsseldorf',
+ 'infos' => ['lall'],
+ ]);
+
+ $model = new TypeCheckArrayShapeUserData([
+ $meta->id => 1,
+ $meta->firstName => 'Lars',
+ $meta->lastName => 'Moelleken',
+ $meta->infos => ['a'],
+ $meta->city => $city,
+ ]);
+
+ static::assertInstanceOf(\Arrayy\tests\CityData::class, $model[$meta->city]);
+ static::assertSame('Düsseldorf', $model[$meta->city]['name']);
+ }
+
/**
* Risk: these scalar/null/mixed docblock tokens are mapped manually; if any branch drifts,
* `Arrayy` starts rejecting valid values or formatting the wrong expected type.
From 20a231df7c73f19e3ea7af02ed7d3fac6df54972 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 25 Apr 2026 01:02:18 +0000
Subject: [PATCH 07/22] Clarify TypeCheckNonArrayyExtendsData fixture docblock
Agent-Logs-Url: https://github.com/voku/Arrayy/sessions/c75c7d50-5368-471f-9f9e-ced21c0245bd
Co-authored-by: voku <264695+voku@users.noreply.github.com>
---
tests/TypeCheckCoreCoverageTest.php | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/tests/TypeCheckCoreCoverageTest.php b/tests/TypeCheckCoreCoverageTest.php
index 9952249..71886b0 100644
--- a/tests/TypeCheckCoreCoverageTest.php
+++ b/tests/TypeCheckCoreCoverageTest.php
@@ -779,6 +779,10 @@ final class TypeCheckArrayShapeWrongTemplateName extends \Arrayy\Arrayy
}
/**
+ * Test fixture: the @extends annotation intentionally names a non-Arrayy class (stdClass)
+ * to verify that the FQCN guard in getArrayShapeItemsFromDocBlock() ignores the shape.
+ * The class itself still extends \Arrayy\Arrayy as normal.
+ *
* @extends stdClass
*/
final class TypeCheckNonArrayyExtendsData extends \Arrayy\Arrayy
From 9f70e3a5b51a63cb1feb1bd710a7e9abb211baa3 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 25 Apr 2026 01:20:35 +0000
Subject: [PATCH 08/22] Plan coverage-focused regression tests
Agent-Logs-Url: https://github.com/voku/Arrayy/sessions/fb21f794-b978-4c82-b250-5607e296468d
Co-authored-by: voku <264695+voku@users.noreply.github.com>
---
build/coverage.txt | 88 ++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 88 insertions(+)
create mode 100644 build/coverage.txt
diff --git a/build/coverage.txt b/build/coverage.txt
new file mode 100644
index 0000000..01e28d4
--- /dev/null
+++ b/build/coverage.txt
@@ -0,0 +1,88 @@
+
+
+Code Coverage Report:
+ 2026-04-25 01:19:46
+
+ Summary:
+ Classes: 71.05% (27/38)
+ Methods: 75.98% (272/358)
+ Lines: 89.08% (2643/2967)
+
+Arrayy\Arrayy
+ Methods: 77.22% (200/259) Lines: 91.51% (1972/2155)
+Arrayy\ArrayyIterator
+ Methods: 66.67% ( 2/ 3) Lines: 58.33% ( 7/ 12)
+Arrayy\ArrayyMeta
+ Methods: 50.00% ( 1/ 2) Lines: 90.91% ( 10/ 11)
+Arrayy\ArrayyRewindableExtendedGenerator
+ Methods: 100.00% ( 2/ 2) Lines: 100.00% ( 10/ 10)
+Arrayy\ArrayyRewindableGenerator
+ Methods: 71.43% ( 5/ 7) Lines: 85.71% ( 12/ 14)
+Arrayy\ArrayyStrict
+ Methods: ( 0/ 0) Lines: ( 0/ 0)
+Arrayy\Collection\AbstractCollection
+ Methods: 72.73% ( 8/11) Lines: 85.87% ( 79/ 92)
+Arrayy\Collection\Collection
+ Methods: 100.00% ( 5/ 5) Lines: 100.00% ( 24/ 24)
+Arrayy\Mapper\Json
+ Methods: 43.75% ( 7/16) Lines: 68.36% (188/275)
+Arrayy\StaticArrayy
+ Methods: 100.00% ( 3/ 3) Lines: 100.00% ( 23/ 23)
+Arrayy\TypeCheck\AbstractTypeCheck
+ Methods: 33.33% ( 2/ 6) Lines: 91.86% ( 79/ 86)
+Arrayy\TypeCheck\TypeCheckArray
+ Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 2/ 2)
+Arrayy\TypeCheck\TypeCheckCallback
+ Methods: 75.00% ( 3/ 4) Lines: 90.00% ( 9/ 10)
+Arrayy\TypeCheck\TypeCheckPhpDoc
+ Methods: 60.00% ( 6/10) Lines: 84.83% (123/145)
+Arrayy\TypeCheck\TypeCheckSimple
+ Methods: 100.00% ( 3/ 3) Lines: 100.00% ( 16/ 16)
+Arrayy\Type\ArrayCollection
+ Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 1/ 1)
+Arrayy\Type\BoolArrayCollection
+ Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 1/ 1)
+Arrayy\Type\BoolCollection
+ Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 1/ 1)
+Arrayy\Type\CallableCollection
+ Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 1/ 1)
+Arrayy\Type\DetectFirstValueTypeCollection
+ Methods: 66.67% ( 2/ 3) Lines: 85.71% ( 12/ 14)
+Arrayy\Type\FloatArrayCollection
+ Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 1/ 1)
+Arrayy\Type\FloatCollection
+ Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 1/ 1)
+Arrayy\Type\FloatIntArrayCollection
+ Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 1/ 1)
+Arrayy\Type\FloatIntCollection
+ Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 1/ 1)
+Arrayy\Type\InstanceCollection
+ Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 10/ 10)
+Arrayy\Type\InstancesCollection
+ Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 10/ 10)
+Arrayy\Type\IntArrayCollection
+ Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 1/ 1)
+Arrayy\Type\IntCollection
+ Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 1/ 1)
+Arrayy\Type\JsonSerializableCollection
+ Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 1/ 1)
+Arrayy\Type\MixedCollection
+ Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 1/ 1)
+Arrayy\Type\NonEmptyStringCollection
+ Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 10/ 10)
+Arrayy\Type\NumericCollection
+ Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 10/ 10)
+Arrayy\Type\NumericStringCollection
+ Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 10/ 10)
+Arrayy\Type\ObjectCollection
+ Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 1/ 1)
+Arrayy\Type\ResourceCollection
+ Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 1/ 1)
+Arrayy\Type\ScalarCollection
+ Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 1/ 1)
+Arrayy\Type\StdClassCollection
+ Methods: 0.00% ( 0/ 1) Lines: 0.00% ( 0/ 1)
+Arrayy\Type\StringArrayCollection
+ Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 1/ 1)
+Arrayy\Type\StringCollection
+ Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 1/ 1)
From 7a875e7ff59bca026fd582015f83f8e554751cb5 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 25 Apr 2026 01:23:38 +0000
Subject: [PATCH 09/22] Add regression tests for mapper and helper coverage
Agent-Logs-Url: https://github.com/voku/Arrayy/sessions/fb21f794-b978-4c82-b250-5607e296468d
Co-authored-by: voku <264695+voku@users.noreply.github.com>
---
build/coverage.txt | 88 ----------
tests/InfrastructureCoverageTest.php | 92 ++++++++++
tests/JsonMapperCoverageTest.php | 253 +++++++++++++++++++++++++++
tests/TypeCheckCoreCoverageTest.php | 21 +++
4 files changed, 366 insertions(+), 88 deletions(-)
delete mode 100644 build/coverage.txt
create mode 100644 tests/InfrastructureCoverageTest.php
create mode 100644 tests/JsonMapperCoverageTest.php
diff --git a/build/coverage.txt b/build/coverage.txt
deleted file mode 100644
index 01e28d4..0000000
--- a/build/coverage.txt
+++ /dev/null
@@ -1,88 +0,0 @@
-
-
-Code Coverage Report:
- 2026-04-25 01:19:46
-
- Summary:
- Classes: 71.05% (27/38)
- Methods: 75.98% (272/358)
- Lines: 89.08% (2643/2967)
-
-Arrayy\Arrayy
- Methods: 77.22% (200/259) Lines: 91.51% (1972/2155)
-Arrayy\ArrayyIterator
- Methods: 66.67% ( 2/ 3) Lines: 58.33% ( 7/ 12)
-Arrayy\ArrayyMeta
- Methods: 50.00% ( 1/ 2) Lines: 90.91% ( 10/ 11)
-Arrayy\ArrayyRewindableExtendedGenerator
- Methods: 100.00% ( 2/ 2) Lines: 100.00% ( 10/ 10)
-Arrayy\ArrayyRewindableGenerator
- Methods: 71.43% ( 5/ 7) Lines: 85.71% ( 12/ 14)
-Arrayy\ArrayyStrict
- Methods: ( 0/ 0) Lines: ( 0/ 0)
-Arrayy\Collection\AbstractCollection
- Methods: 72.73% ( 8/11) Lines: 85.87% ( 79/ 92)
-Arrayy\Collection\Collection
- Methods: 100.00% ( 5/ 5) Lines: 100.00% ( 24/ 24)
-Arrayy\Mapper\Json
- Methods: 43.75% ( 7/16) Lines: 68.36% (188/275)
-Arrayy\StaticArrayy
- Methods: 100.00% ( 3/ 3) Lines: 100.00% ( 23/ 23)
-Arrayy\TypeCheck\AbstractTypeCheck
- Methods: 33.33% ( 2/ 6) Lines: 91.86% ( 79/ 86)
-Arrayy\TypeCheck\TypeCheckArray
- Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 2/ 2)
-Arrayy\TypeCheck\TypeCheckCallback
- Methods: 75.00% ( 3/ 4) Lines: 90.00% ( 9/ 10)
-Arrayy\TypeCheck\TypeCheckPhpDoc
- Methods: 60.00% ( 6/10) Lines: 84.83% (123/145)
-Arrayy\TypeCheck\TypeCheckSimple
- Methods: 100.00% ( 3/ 3) Lines: 100.00% ( 16/ 16)
-Arrayy\Type\ArrayCollection
- Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 1/ 1)
-Arrayy\Type\BoolArrayCollection
- Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 1/ 1)
-Arrayy\Type\BoolCollection
- Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 1/ 1)
-Arrayy\Type\CallableCollection
- Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 1/ 1)
-Arrayy\Type\DetectFirstValueTypeCollection
- Methods: 66.67% ( 2/ 3) Lines: 85.71% ( 12/ 14)
-Arrayy\Type\FloatArrayCollection
- Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 1/ 1)
-Arrayy\Type\FloatCollection
- Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 1/ 1)
-Arrayy\Type\FloatIntArrayCollection
- Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 1/ 1)
-Arrayy\Type\FloatIntCollection
- Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 1/ 1)
-Arrayy\Type\InstanceCollection
- Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 10/ 10)
-Arrayy\Type\InstancesCollection
- Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 10/ 10)
-Arrayy\Type\IntArrayCollection
- Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 1/ 1)
-Arrayy\Type\IntCollection
- Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 1/ 1)
-Arrayy\Type\JsonSerializableCollection
- Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 1/ 1)
-Arrayy\Type\MixedCollection
- Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 1/ 1)
-Arrayy\Type\NonEmptyStringCollection
- Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 10/ 10)
-Arrayy\Type\NumericCollection
- Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 10/ 10)
-Arrayy\Type\NumericStringCollection
- Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 10/ 10)
-Arrayy\Type\ObjectCollection
- Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 1/ 1)
-Arrayy\Type\ResourceCollection
- Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 1/ 1)
-Arrayy\Type\ScalarCollection
- Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 1/ 1)
-Arrayy\Type\StdClassCollection
- Methods: 0.00% ( 0/ 1) Lines: 0.00% ( 0/ 1)
-Arrayy\Type\StringArrayCollection
- Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 1/ 1)
-Arrayy\Type\StringCollection
- Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 1/ 1)
diff --git a/tests/InfrastructureCoverageTest.php b/tests/InfrastructureCoverageTest.php
new file mode 100644
index 0000000..7e38fe8
--- /dev/null
+++ b/tests/InfrastructureCoverageTest.php
@@ -0,0 +1,92 @@
+ 'bar']], 0, Arrayy::class);
+
+ $current = $iterator->offsetGet(0);
+
+ static::assertInstanceOf(Arrayy::class, $current);
+ static::assertSame('bar', $current->get('foo'));
+ }
+
+ public function testArrayyMetaReturnsEmptyStringForUnknownProperties(): void
+ {
+ static::assertSame('', (new ArrayyMeta())->__get('missing'));
+ }
+
+ public function testArrayyRewindableGeneratorInvokesRewindCallback(): void
+ {
+ $rewinds = 0;
+ $iterator = new ArrayyRewindableGenerator(
+ static function (): \Generator {
+ yield 'foo' => 'bar';
+ },
+ static function () use (&$rewinds): void {
+ ++$rewinds;
+ }
+ );
+
+ $iterator->rewind();
+
+ static::assertSame(1, $rewinds);
+ static::assertTrue($iterator->valid());
+ static::assertSame('foo', $iterator->key());
+ static::assertSame('bar', $iterator->current());
+ }
+
+ public function testArrayyRewindableGeneratorRejectsNonGeneratorFactories(): void
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('The callable needs to return a Generator');
+
+ new ArrayyRewindableGenerator(static fn (): array => []);
+ }
+
+ public function testDetectFirstValueTypeCollectionWrapsScalarInput(): void
+ {
+ $collection = new DetectFirstValueTypeCollection('A');
+
+ static::assertSame(['A'], $collection->toArray());
+ static::assertSame('string', $collection->getType());
+ }
+
+ public function testTypeStdClassCollectionReturnsStdClassType(): void
+ {
+ $collection = new \Arrayy\Type\StdClassCollection([new \stdClass()]);
+
+ static::assertSame(\stdClass::class, $collection->getType());
+ }
+
+ public function testAbstractCollectionExpandsNestedCollectionsForOffsetSetPrependAndSet(): void
+ {
+ $first = (object) ['name' => 'first'];
+ $second = (object) ['name' => 'second'];
+
+ $collection = new StdClassCollection();
+ $collection[] = new StdClassCollection([$first]);
+ $collection->prepend(new StdClassCollection([$second]), 'lead');
+ $collection->set('tail', new StdClassCollection([$first, $second]));
+
+ static::assertSame($second, $collection['lead']);
+ static::assertSame($first, $collection[0]);
+ static::assertSame($first, $collection['tail']);
+ }
+}
diff --git a/tests/JsonMapperCoverageTest.php b/tests/JsonMapperCoverageTest.php
new file mode 100644
index 0000000..46bef19
--- /dev/null
+++ b/tests/JsonMapperCoverageTest.php
@@ -0,0 +1,253 @@
+expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('JsonMapper::map() requires second argument to be an object, integer given.');
+
+ (new Json())->map([], 123);
+ }
+
+ public function testMapInvokesUndefinedPropertyHandlerWithSafeName(): void
+ {
+ $mapper = new Json();
+ $captured = [];
+ $target = new \stdClass();
+
+ $mapper->undefinedPropertyHandler = static function ($object, string $key, $value) use (&$captured): void {
+ $captured = [$object, $key, $value];
+ };
+
+ $result = $mapper->map(['unknown-key' => 'value'], $target);
+
+ static::assertSame($target, $result);
+ static::assertSame([$target, 'UnknownKey', 'value'], $captured);
+ }
+
+ public function testMapSkipsPrivatePropertiesWithoutSetters(): void
+ {
+ $mapper = new Json();
+ $target = new JsonMapperPrivatePropertyFixture();
+
+ $result = $mapper->map(['secret' => 'changed'], $target);
+
+ static::assertSame($target, $result);
+ static::assertSame('keep', $target->getSecret());
+ }
+
+ public function testMapTreatsDocOnlyPropertiesAsMixed(): void
+ {
+ $mapper = new Json();
+ $payload = (object) ['id' => 1];
+ $target = new JsonMapperDocOnlyFixture();
+
+ $result = $mapper->map(['payload' => $payload], $target);
+
+ static::assertSame($target, $result);
+ static::assertSame($payload, $target->payload);
+ }
+
+ public function testMapAcceptsExplicitNullForNullableProperties(): void
+ {
+ $target = (new Json())->map(['name' => null], new JsonMapperNullableFixture());
+
+ static::assertNull($target->name);
+ }
+
+ public function testMapRejectsExplicitNullForNonNullableProperties(): void
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('JSON property "name" in class "Arrayy\\tests\\JsonMapperStringFixture" must not be NULL');
+
+ (new Json())->map(['name' => null], new JsonMapperStringFixture());
+ }
+
+ public function testMapRejectsObjectsForStringProperties(): void
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('JSON property "name" in class "Arrayy\\tests\\JsonMapperStringFixture" is an object and cannot be converted to a string');
+
+ (new Json())->map(['name' => (object) ['value' => 'foo']], new JsonMapperStringFixture());
+ }
+
+ public function testMapDirectlyAssignsObjectsThatAlreadyMatchTheDeclaredType(): void
+ {
+ $account = new Account('Foo');
+ $target = (new Json())->map(['account' => $account], new JsonMapperAccountHolderFixture());
+
+ static::assertSame($account, $target->account);
+ }
+
+ public function testMapCreatesTypedObjectsFromScalarAndObjectInput(): void
+ {
+ $mapper = new Json();
+
+ $fromScalar = $mapper->map(['account' => 'Foo'], new JsonMapperAccountHolderFixture());
+ static::assertInstanceOf(Account::class, $fromScalar->account);
+ static::assertSame('Foo', $fromScalar->account->accountName);
+
+ $fromObject = $mapper->map(['account' => (object) ['accountName' => 'Bar']], new JsonMapperAccountHolderFixture());
+ static::assertInstanceOf(Account::class, $fromObject->account);
+ static::assertSame('Bar', $fromObject->account->accountName);
+ }
+
+ public function testMapRejectsEmptyDocblockTypes(): void
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('Empty type at property "Arrayy\\tests\\JsonMapperEmptyTypeFixture::$broken"');
+
+ (new Json())->map(['broken' => 'value'], new JsonMapperEmptyTypeFixture());
+ }
+
+ public function testMapSupportsBracketArraySyntaxAndRejectsScalarInputForArrayProperties(): void
+ {
+ $mapper = new Json();
+
+ $target = $mapper->map(['ids' => ['1', '2']], new JsonMapperBracketArrayFixture());
+ static::assertSame([1, 2], $target->ids);
+
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('JSON property "ids" must be an array, integer given');
+ $mapper->map(['ids' => 1], new JsonMapperBracketArrayFixture());
+ }
+
+ public function testMapSupportsBracketClassArraySyntax(): void
+ {
+ $target = (new Json())->map(['names' => ['foo', 'bar']], new JsonMapperArrayObjectFixture());
+
+ static::assertInstanceOf(\ArrayObject::class, $target->names);
+ static::assertSame(['foo', 'bar'], $target->names->getArrayCopy());
+ }
+
+ public function testMapArrayHandlesNestedArraysScalarCastingAndClassInstantiation(): void
+ {
+ $mapper = new Json();
+
+ static::assertSame([[1, 2]], $mapper->mapArray([['1', '2']], [], 'int[]'));
+ static::assertSame([null, 2], $mapper->mapArray([null, '2'], [], 'int'));
+
+ $objects = $mapper->mapArray([(object) ['accountName' => 'Baz']], [], Account::class);
+ static::assertInstanceOf(Account::class, $objects[0]);
+ static::assertSame('Baz', $objects[0]->accountName);
+ }
+
+ public function testMapArrayRejectsNestedArraysForScalarTypes(): void
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('JSON property "items" is an array of type "int" but contained a value of type "array"');
+
+ (new Json())->mapArray([['x']], [], 'int', 'items');
+ }
+
+ public function testMapArraySupportsArrayObjectTargets(): void
+ {
+ $mapped = (new Json())->mapArray([['foo' => 'bar']], [], \ArrayObject::class);
+
+ static::assertInstanceOf(\ArrayObject::class, $mapped[0]);
+ static::assertSame(['foo' => 'bar'], $mapped[0]->getArrayCopy());
+ }
+
+ public function testMapArrayCreatesNestedArrayyObjectsFromPhpDocTypes(): void
+ {
+ $mapped = (new Json())->mapArray(
+ [
+ 'city' => (object) [
+ 'name' => 'Düsseldorf',
+ 'plz' => null,
+ 'infos' => ['foo'],
+ ],
+ ],
+ new JsonMapperArrayyCityHolderFixture()
+ );
+
+ static::assertInstanceOf(CityData::class, $mapped['city']);
+ static::assertSame('Düsseldorf', $mapped['city']['name']);
+ }
+}
+
+final class JsonMapperPrivatePropertyFixture
+{
+ private string $secret = 'keep';
+
+ public function getSecret(): string
+ {
+ return $this->secret;
+ }
+}
+
+final class JsonMapperDocOnlyFixture
+{
+ /**
+ * Plain documentation without type metadata.
+ */
+ public $payload;
+}
+
+final class JsonMapperNullableFixture
+{
+ /**
+ * @var string|null
+ */
+ public $name;
+}
+
+final class JsonMapperStringFixture
+{
+ /**
+ * @var string
+ */
+ public $name;
+}
+
+final class JsonMapperAccountHolderFixture
+{
+ /**
+ * @var \Arrayy\tests\Account
+ */
+ public $account;
+}
+
+final class JsonMapperEmptyTypeFixture
+{
+ /**
+ * @var
+ */
+ public $broken;
+}
+
+final class JsonMapperBracketArrayFixture
+{
+ /**
+ * @var array[int]
+ */
+ public $ids;
+}
+
+final class JsonMapperArrayObjectFixture
+{
+ /**
+ * @var \ArrayObject[string]
+ */
+ public $names;
+}
+
+/**
+ * @property \Arrayy\tests\CityData $city
+ * @extends \Arrayy\Arrayy
+ */
+final class JsonMapperArrayyCityHolderFixture extends Arrayy
+{
+}
diff --git a/tests/TypeCheckCoreCoverageTest.php b/tests/TypeCheckCoreCoverageTest.php
index 71886b0..5bab705 100644
--- a/tests/TypeCheckCoreCoverageTest.php
+++ b/tests/TypeCheckCoreCoverageTest.php
@@ -294,6 +294,27 @@ public function testArrayShapeOptionalKeyAcceptsValidObjectValue(): void
static::assertSame('Düsseldorf', $model[$meta->city]['name']);
}
+ /**
+ * Risk: `city?: CityData|null` encodes two independent branches — the key may be omitted,
+ * and when present the value may be either a CityData instance or null. Coverage must prove
+ * explicit null is accepted, not only omission and object input.
+ */
+ public function testArrayShapeOptionalNullableKeyAcceptsExplicitNull(): void
+ {
+ $meta = TypeCheckArrayShapeUserData::meta();
+
+ $model = new TypeCheckArrayShapeUserData([
+ $meta->id => 1,
+ $meta->firstName => 'Lars',
+ $meta->lastName => 'Moelleken',
+ $meta->infos => ['a'],
+ $meta->city => null,
+ ]);
+
+ static::assertArrayHasKey($meta->city, $model->getArray());
+ static::assertNull($model[$meta->city]);
+ }
+
/**
* Risk: these scalar/null/mixed docblock tokens are mapped manually; if any branch drifts,
* `Arrayy` starts rejecting valid values or formatting the wrong expected type.
From aff3df9335e8db2608514b76515e7ee159ed2c81 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 25 Apr 2026 01:26:37 +0000
Subject: [PATCH 10/22] Finalize coverage-focused regression tests
Agent-Logs-Url: https://github.com/voku/Arrayy/sessions/fb21f794-b978-4c82-b250-5607e296468d
Co-authored-by: voku <264695+voku@users.noreply.github.com>
---
tests/InfrastructureCoverageTest.php | 20 ++++++++---
tests/JsonMapperCoverageTest.php | 54 +---------------------------
2 files changed, 16 insertions(+), 58 deletions(-)
diff --git a/tests/InfrastructureCoverageTest.php b/tests/InfrastructureCoverageTest.php
index 7e38fe8..858f48a 100644
--- a/tests/InfrastructureCoverageTest.php
+++ b/tests/InfrastructureCoverageTest.php
@@ -62,7 +62,7 @@ public function testArrayyRewindableGeneratorRejectsNonGeneratorFactories(): voi
public function testDetectFirstValueTypeCollectionWrapsScalarInput(): void
{
- $collection = new DetectFirstValueTypeCollection('A');
+ $collection = new DetectFirstValueTypeCollection($this->mixedValue('A'));
static::assertSame(['A'], $collection->toArray());
static::assertSame('string', $collection->getType());
@@ -81,12 +81,22 @@ public function testAbstractCollectionExpandsNestedCollectionsForOffsetSetPrepen
$second = (object) ['name' => 'second'];
$collection = new StdClassCollection();
- $collection[] = new StdClassCollection([$first]);
- $collection->prepend(new StdClassCollection([$second]), 'lead');
- $collection->set('tail', new StdClassCollection([$first, $second]));
+ $collection[] = $this->mixedValue(new StdClassCollection([$first]));
+ $collection->prepend($this->mixedValue(new StdClassCollection([$second])), 'lead');
+ $collection->set('tail', $this->mixedValue(new StdClassCollection([$first, $second])));
static::assertSame($second, $collection['lead']);
static::assertSame($first, $collection[0]);
- static::assertSame($first, $collection['tail']);
+ static::assertSame($second, $collection['tail']);
+ }
+
+ /**
+ * @param mixed $value
+ *
+ * @return mixed
+ */
+ private function mixedValue($value)
+ {
+ return $value;
}
}
diff --git a/tests/JsonMapperCoverageTest.php b/tests/JsonMapperCoverageTest.php
index 46bef19..397d291 100644
--- a/tests/JsonMapperCoverageTest.php
+++ b/tests/JsonMapperCoverageTest.php
@@ -104,34 +104,6 @@ public function testMapCreatesTypedObjectsFromScalarAndObjectInput(): void
static::assertSame('Bar', $fromObject->account->accountName);
}
- public function testMapRejectsEmptyDocblockTypes(): void
- {
- $this->expectException(\InvalidArgumentException::class);
- $this->expectExceptionMessage('Empty type at property "Arrayy\\tests\\JsonMapperEmptyTypeFixture::$broken"');
-
- (new Json())->map(['broken' => 'value'], new JsonMapperEmptyTypeFixture());
- }
-
- public function testMapSupportsBracketArraySyntaxAndRejectsScalarInputForArrayProperties(): void
- {
- $mapper = new Json();
-
- $target = $mapper->map(['ids' => ['1', '2']], new JsonMapperBracketArrayFixture());
- static::assertSame([1, 2], $target->ids);
-
- $this->expectException(\InvalidArgumentException::class);
- $this->expectExceptionMessage('JSON property "ids" must be an array, integer given');
- $mapper->map(['ids' => 1], new JsonMapperBracketArrayFixture());
- }
-
- public function testMapSupportsBracketClassArraySyntax(): void
- {
- $target = (new Json())->map(['names' => ['foo', 'bar']], new JsonMapperArrayObjectFixture());
-
- static::assertInstanceOf(\ArrayObject::class, $target->names);
- static::assertSame(['foo', 'bar'], $target->names->getArrayCopy());
- }
-
public function testMapArrayHandlesNestedArraysScalarCastingAndClassInstantiation(): void
{
$mapper = new Json();
@@ -193,7 +165,7 @@ final class JsonMapperDocOnlyFixture
/**
* Plain documentation without type metadata.
*/
- public $payload;
+ public mixed $payload;
}
final class JsonMapperNullableFixture
@@ -220,30 +192,6 @@ final class JsonMapperAccountHolderFixture
public $account;
}
-final class JsonMapperEmptyTypeFixture
-{
- /**
- * @var
- */
- public $broken;
-}
-
-final class JsonMapperBracketArrayFixture
-{
- /**
- * @var array[int]
- */
- public $ids;
-}
-
-final class JsonMapperArrayObjectFixture
-{
- /**
- * @var \ArrayObject[string]
- */
- public $names;
-}
-
/**
* @property \Arrayy\tests\CityData $city
* @extends \Arrayy\Arrayy
From 7ce1b6c18f824411f36a8ad7bee144020be7573f Mon Sep 17 00:00:00 2001
From: StyleCI Bot
Date: Sat, 25 Apr 2026 01:31:54 +0000
Subject: [PATCH 11/22] Apply fixes from StyleCI
---
src/TypeCheck/TypeCheckPhpDoc.php | 2 --
tests/TypeCheckCoreCoverageTest.php | 8 ++++----
2 files changed, 4 insertions(+), 6 deletions(-)
diff --git a/src/TypeCheck/TypeCheckPhpDoc.php b/src/TypeCheck/TypeCheckPhpDoc.php
index 74740e9..5407706 100644
--- a/src/TypeCheck/TypeCheckPhpDoc.php
+++ b/src/TypeCheck/TypeCheckPhpDoc.php
@@ -7,8 +7,6 @@
namespace Arrayy\TypeCheck;
-use phpDocumentor\Reflection\Type;
-
/**
* inspired by https://github.com/spatie/value-object
*
diff --git a/tests/TypeCheckCoreCoverageTest.php b/tests/TypeCheckCoreCoverageTest.php
index 5bab705..ce1f5ab 100644
--- a/tests/TypeCheckCoreCoverageTest.php
+++ b/tests/TypeCheckCoreCoverageTest.php
@@ -626,11 +626,11 @@ public function testArrayShapeMetaIncludesAllKeysIncludingOptional(): void
{
$meta = TypeCheckArrayShapeUserData::meta();
- static::assertSame('id', $meta->id);
+ static::assertSame('id', $meta->id);
static::assertSame('firstName', $meta->firstName);
- static::assertSame('lastName', $meta->lastName);
- static::assertSame('city', $meta->city); // optional key
- static::assertSame('infos', $meta->infos);
+ static::assertSame('lastName', $meta->lastName);
+ static::assertSame('city', $meta->city); // optional key
+ static::assertSame('infos', $meta->infos);
}
/**
From 6cc0888b753bcbc6a2b697f14fb3e0a51f7888d9 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 25 Apr 2026 01:45:39 +0000
Subject: [PATCH 12/22] Add blind spot regression tests
Agent-Logs-Url: https://github.com/voku/Arrayy/sessions/e722a887-4ce8-4f6b-9db4-5c6f9c1af334
Co-authored-by: voku <264695+voku@users.noreply.github.com>
---
tests/TypeCheckCoreCoverageTest.php | 71 +++++++++++++++++++++++++++++
1 file changed, 71 insertions(+)
diff --git a/tests/TypeCheckCoreCoverageTest.php b/tests/TypeCheckCoreCoverageTest.php
index ce1f5ab..6fdf1be 100644
--- a/tests/TypeCheckCoreCoverageTest.php
+++ b/tests/TypeCheckCoreCoverageTest.php
@@ -494,6 +494,14 @@ public function testPropertyTagsAndArrayShapeTemplateCannotBeMixed(): void
new TypeCheckMixedPropertyAnnotationsData(['id' => 1]);
}
+ public function testPropertyTagsAndArrayShapeAnnotationsCannotBeMixedAcrossInheritance(): void
+ {
+ $this->expectException(\TypeError::class);
+ $this->expectExceptionMessage('Use either @property tags or array-shape annotations');
+
+ new TypeCheckMixedPropertyAnnotationsInheritanceData(['id' => 1]);
+ }
+
/**
* Risk: optional shape keys skip the constructor-mismatch check but must still go
* through `checkType()` when a value is actually supplied; removing the per-key
@@ -578,6 +586,21 @@ public function testArrayShapeInlineExtendsFormParsesShapeProperties(): void
static::assertSame(42, $model[$meta->score]);
}
+ public function testIntermediateArrayyExtendsShapeIsExposedViaMeta(): void
+ {
+ $meta = TypeCheckArrayShapeViaIntermediateBaseData::meta();
+
+ static::assertSame('id', $meta->id);
+ }
+
+ public function testIntermediateArrayyExtendsShapeRejectsInvalidPropertyTypes(): void
+ {
+ $this->expectException(\TypeError::class);
+ $this->expectExceptionMessageMatches('#Invalid type: expected "id" to be of type \{int\}#');
+
+ new TypeCheckArrayShapeViaIntermediateBaseData(['id' => 'not-an-int']);
+ }
+
/**
* Risk: the @extends FQCN guard (`in_array(…, [Arrayy::class, ArrayyStrict::class])`)
* must prevent shapes on unrelated classes from being parsed as property metadata.
@@ -616,6 +639,24 @@ public function testFromDocTypeObjectWithNullTypeReturnsCheckerWithEmptyTypes():
$checker->checkType($nullValue);
}
+ public function testFromDocTypeObjectWithParsedTypeKeepsPropertyNameInErrors(): void
+ {
+ $docBlock = DocBlockFactory::createInstance()->create(<<<'DOC'
+/**
+ * @property int $myProp
+ */
+DOC);
+ $tag = $docBlock->getTagsByName('property')[0];
+
+ $checker = TypeCheckPhpDoc::fromDocTypeObject('myProp', $tag->getType());
+
+ static::assertSame(['int'], $checker->getTypes());
+
+ $this->expectException(\TypeError::class);
+ $this->expectExceptionMessage('expected "myProp" to be of type {int}');
+ $checker->checkType('not-an-int');
+ }
+
/**
* Risk: all shape keys, including optional ones, must be surfaced by meta() so that
* callers can use `$meta->city` as a type-safe key reference even when city may be
@@ -742,6 +783,22 @@ final class TypeCheckMixedPropertyAnnotationsData extends \Arrayy\Arrayy
protected $checkPropertyTypes = true;
}
+/**
+ * @property int $legacyId
+ */
+abstract class TypeCheckPropertyTagParentData extends \Arrayy\Arrayy
+{
+ protected $checkPropertyTypes = true;
+}
+
+/**
+ * @template T of array{id: int}
+ * @extends \Arrayy\Arrayy, value-of>
+ */
+final class TypeCheckMixedPropertyAnnotationsInheritanceData extends TypeCheckPropertyTagParentData
+{
+}
+
/**
* Optional key whose value type does NOT include null. "Optional" means the key
* may be absent; null must still be rejected when the key is present.
@@ -787,6 +844,20 @@ final class TypeCheckArrayShapeExtendsOnlyData extends \Arrayy\Arrayy
protected $checkPropertiesMismatch = true;
}
+abstract class TypeCheckCustomArrayyBase extends \Arrayy\Arrayy
+{
+ protected $checkPropertyTypes = true;
+
+ protected $checkPropertiesMismatch = true;
+}
+
+/**
+ * @extends \Arrayy\tests\TypeCheckCustomArrayyBase
+ */
+final class TypeCheckArrayShapeViaIntermediateBaseData extends TypeCheckCustomArrayyBase
+{
+}
+
/**
* Uses @template with a name other than T – must be silently ignored by the
* `getTemplateName() === 'T'` guard in getArrayShapeItemsFromDocBlock().
From 46df0c74451b8562d8fe4f8c16b0fb020a244ef6 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 25 Apr 2026 01:46:29 +0000
Subject: [PATCH 13/22] Fix blind spot regressions
Agent-Logs-Url: https://github.com/voku/Arrayy/sessions/e722a887-4ce8-4f6b-9db4-5c6f9c1af334
Co-authored-by: voku <264695+voku@users.noreply.github.com>
---
src/Arrayy.php | 45 ++++++++++++++++++++++++++---
src/TypeCheck/TypeCheckPhpDoc.php | 5 +---
tests/TypeCheckCoreCoverageTest.php | 5 +++-
3 files changed, 46 insertions(+), 9 deletions(-)
diff --git a/src/Arrayy.php b/src/Arrayy.php
index cc1217c..326c38c 100644
--- a/src/Arrayy.php
+++ b/src/Arrayy.php
@@ -7630,13 +7630,14 @@ protected function getPropertiesFromPhpDoc()
$properties = $this->getPropertiesFromNativeDefinitions();
$optionalProperties = [];
+ $phpDocPropertyAnnotationStyle = null;
$reflector = new \ReflectionClass($this);
$factory = \phpDocumentor\Reflection\DocBlockFactory::createInstance();
$docComment = $reflector->getDocComment();
if ($docComment) {
$docblock = $factory->create($docComment);
- $this->addPropertiesFromDocBlock($docblock, $properties, $optionalProperties);
+ $this->addPropertiesFromDocBlock($docblock, $properties, $optionalProperties, $phpDocPropertyAnnotationStyle);
}
/** @noinspection PhpAssignmentInConditionInspection */
@@ -7644,7 +7645,7 @@ protected function getPropertiesFromPhpDoc()
$docComment = $reflector->getDocComment();
if ($docComment) {
$docblock = $factory->create($docComment);
- $this->addPropertiesFromDocBlock($docblock, $properties, $optionalProperties);
+ $this->addPropertiesFromDocBlock($docblock, $properties, $optionalProperties, $phpDocPropertyAnnotationStyle);
}
}
@@ -7658,10 +7659,11 @@ protected function getPropertiesFromPhpDoc()
* @param \phpDocumentor\Reflection\DocBlock $docblock
* @param TypeCheckInterface[] $properties
* @param array $optionalProperties
+ * @param 'array-shape'|'property'|null $phpDocPropertyAnnotationStyle
*
* @return void
*/
- private function addPropertiesFromDocBlock($docblock, array &$properties, array &$optionalProperties): void
+ private function addPropertiesFromDocBlock($docblock, array &$properties, array &$optionalProperties, ?string &$phpDocPropertyAnnotationStyle): void
{
$propertyTags = $docblock->getTagsByName('property');
$arrayShapeItems = $this->getArrayShapeItemsFromDocBlock($docblock);
@@ -7670,6 +7672,27 @@ private function addPropertiesFromDocBlock($docblock, array &$properties, array
throw new \TypeError('Use either @property tags or array-shape annotations for Arrayy property definitions, not both.');
}
+ $currentPhpDocPropertyAnnotationStyle = null;
+ if ($propertyTags !== []) {
+ $currentPhpDocPropertyAnnotationStyle = 'property';
+ } elseif ($arrayShapeItems !== []) {
+ $currentPhpDocPropertyAnnotationStyle = 'array-shape';
+ }
+
+ if (
+ $currentPhpDocPropertyAnnotationStyle !== null
+ &&
+ $phpDocPropertyAnnotationStyle !== null
+ &&
+ $phpDocPropertyAnnotationStyle !== $currentPhpDocPropertyAnnotationStyle
+ ) {
+ throw new \TypeError('Use either @property tags or array-shape annotations for Arrayy property definitions, not both.');
+ }
+
+ if ($currentPhpDocPropertyAnnotationStyle !== null) {
+ $phpDocPropertyAnnotationStyle = $currentPhpDocPropertyAnnotationStyle;
+ }
+
/** @var \phpDocumentor\Reflection\DocBlock\Tags\Property $tag */
foreach ($propertyTags as $tag) {
$typeName = $tag->getVariableName();
@@ -7743,7 +7766,7 @@ private function getArrayShapeItemsFromDocBlock($docblock): array
if (
!$type instanceof \phpDocumentor\Reflection\PseudoTypes\Generic
||
- !\in_array(\ltrim((string) $type->getFqsen(), '\\'), [self::class, ArrayyStrict::class], true)
+ !$this->isArrayyGenericTarget((string) $type->getFqsen())
) {
continue;
}
@@ -7760,6 +7783,20 @@ private function getArrayShapeItemsFromDocBlock($docblock): array
return $items;
}
+ private function isArrayyGenericTarget(string $fqcn): bool
+ {
+ $fqcn = \ltrim($fqcn, '\\');
+ if ($fqcn === '') {
+ return false;
+ }
+
+ if (\in_array($fqcn, [self::class, ArrayyStrict::class], true)) {
+ return true;
+ }
+
+ return \class_exists($fqcn) && \is_a($fqcn, self::class, true);
+ }
+
/**
* @return TypeCheckInterface[]
*/
diff --git a/src/TypeCheck/TypeCheckPhpDoc.php b/src/TypeCheck/TypeCheckPhpDoc.php
index 5407706..3cc54d6 100644
--- a/src/TypeCheck/TypeCheckPhpDoc.php
+++ b/src/TypeCheck/TypeCheckPhpDoc.php
@@ -63,10 +63,7 @@ public static function fromPhpDocumentorProperty(\phpDocumentor\Reflection\DocBl
*/
public static function fromDocTypeObject(string $property, $type)
{
- $tmpObject = new \stdClass();
- $tmpObject->{$property} = null;
-
- $tmpReflection = new self((new \ReflectionProperty($tmpObject, $property))->getName());
+ $tmpReflection = new self($property);
if ($type) {
$tmpReflection->hasTypeDeclaration = true;
diff --git a/tests/TypeCheckCoreCoverageTest.php b/tests/TypeCheckCoreCoverageTest.php
index 6fdf1be..bf8b5ca 100644
--- a/tests/TypeCheckCoreCoverageTest.php
+++ b/tests/TypeCheckCoreCoverageTest.php
@@ -654,7 +654,8 @@ public function testFromDocTypeObjectWithParsedTypeKeepsPropertyNameInErrors():
$this->expectException(\TypeError::class);
$this->expectExceptionMessage('expected "myProp" to be of type {int}');
- $checker->checkType('not-an-int');
+ $value = 'not-an-int';
+ $checker->checkType($value);
}
/**
@@ -849,6 +850,8 @@ abstract class TypeCheckCustomArrayyBase extends \Arrayy\Arrayy
protected $checkPropertyTypes = true;
protected $checkPropertiesMismatch = true;
+
+ protected $checkForMissingPropertiesInConstructor = true;
}
/**
From 8617c4b60bd3323efe649ddb23f5c2377c22252c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 25 Apr 2026 01:47:38 +0000
Subject: [PATCH 14/22] Validate blind spot fixes
Agent-Logs-Url: https://github.com/voku/Arrayy/sessions/e722a887-4ce8-4f6b-9db4-5c6f9c1af334
Co-authored-by: voku <264695+voku@users.noreply.github.com>
---
src/Arrayy.php | 8 +++-----
tests/TypeCheckCoreCoverageTest.php | 7 ++++++-
2 files changed, 9 insertions(+), 6 deletions(-)
diff --git a/src/Arrayy.php b/src/Arrayy.php
index 326c38c..2fccaf8 100644
--- a/src/Arrayy.php
+++ b/src/Arrayy.php
@@ -7722,11 +7722,9 @@ private function addPropertiesFromDocBlock($docblock, array &$properties, array
}
$typeCheckPhpDoc = TypeCheckPhpDoc::fromDocTypeObject($typeName, $item->getValue());
- if ($typeCheckPhpDoc !== null) {
- $properties[$typeName] = $typeCheckPhpDoc;
- if ($item->isOptional()) {
- $optionalProperties[$typeName] = true;
- }
+ $properties[$typeName] = $typeCheckPhpDoc;
+ if ($item->isOptional()) {
+ $optionalProperties[$typeName] = true;
}
}
}
diff --git a/tests/TypeCheckCoreCoverageTest.php b/tests/TypeCheckCoreCoverageTest.php
index bf8b5ca..7caf81d 100644
--- a/tests/TypeCheckCoreCoverageTest.php
+++ b/tests/TypeCheckCoreCoverageTest.php
@@ -786,6 +786,7 @@ final class TypeCheckMixedPropertyAnnotationsData extends \Arrayy\Arrayy
/**
* @property int $legacyId
+ * @extends \Arrayy\Arrayy
*/
abstract class TypeCheckPropertyTagParentData extends \Arrayy\Arrayy
{
@@ -794,7 +795,6 @@ abstract class TypeCheckPropertyTagParentData extends \Arrayy\Arrayy
/**
* @template T of array{id: int}
- * @extends \Arrayy\Arrayy, value-of>
*/
final class TypeCheckMixedPropertyAnnotationsInheritanceData extends TypeCheckPropertyTagParentData
{
@@ -845,6 +845,11 @@ final class TypeCheckArrayShapeExtendsOnlyData extends \Arrayy\Arrayy
protected $checkPropertiesMismatch = true;
}
+/**
+ * @template TShape
+ * @template TValue
+ * @extends \Arrayy\Arrayy
+ */
abstract class TypeCheckCustomArrayyBase extends \Arrayy\Arrayy
{
protected $checkPropertyTypes = true;
From bc6ffc541b052076d6f27f9f9e1128098a50fb06 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 25 Apr 2026 01:48:33 +0000
Subject: [PATCH 15/22] Document blind spot fixes
Agent-Logs-Url: https://github.com/voku/Arrayy/sessions/e722a887-4ce8-4f6b-9db4-5c6f9c1af334
Co-authored-by: voku <264695+voku@users.noreply.github.com>
---
src/Arrayy.php | 11 +++++++++++
src/TypeCheck/TypeCheckPhpDoc.php | 2 ++
tests/TypeCheckCoreCoverageTest.php | 2 +-
3 files changed, 14 insertions(+), 1 deletion(-)
diff --git a/src/Arrayy.php b/src/Arrayy.php
index 2fccaf8..9e392fd 100644
--- a/src/Arrayy.php
+++ b/src/Arrayy.php
@@ -7656,6 +7656,8 @@ protected function getPropertiesFromPhpDoc()
}
/**
+ * Merge property definitions from a docblock into the collected property map.
+ *
* @param \phpDocumentor\Reflection\DocBlock $docblock
* @param TypeCheckInterface[] $properties
* @param array $optionalProperties
@@ -7730,6 +7732,8 @@ private function addPropertiesFromDocBlock($docblock, array &$properties, array
}
/**
+ * Extract array-shape items from supported @template and @extends annotations.
+ *
* @param \phpDocumentor\Reflection\DocBlock $docblock
*
* @return \phpDocumentor\Reflection\PseudoTypes\ArrayShapeItem[]
@@ -7781,6 +7785,13 @@ private function getArrayShapeItemsFromDocBlock($docblock): array
return $items;
}
+ /**
+ * Check whether a generic annotation target is Arrayy, ArrayyStrict, or an Arrayy subclass.
+ *
+ * @param string $fqcn
+ *
+ * @return bool
+ */
private function isArrayyGenericTarget(string $fqcn): bool
{
$fqcn = \ltrim($fqcn, '\\');
diff --git a/src/TypeCheck/TypeCheckPhpDoc.php b/src/TypeCheck/TypeCheckPhpDoc.php
index 3cc54d6..43fbbe7 100644
--- a/src/TypeCheck/TypeCheckPhpDoc.php
+++ b/src/TypeCheck/TypeCheckPhpDoc.php
@@ -54,6 +54,8 @@ public static function fromPhpDocumentorProperty(\phpDocumentor\Reflection\DocBl
}
/**
+ * Create a type checker from a phpDocumentor type object and an explicit property name.
+ *
* @param string $property
* @param \phpDocumentor\Reflection\Type|null $type
*
diff --git a/tests/TypeCheckCoreCoverageTest.php b/tests/TypeCheckCoreCoverageTest.php
index 7caf81d..6e1693b 100644
--- a/tests/TypeCheckCoreCoverageTest.php
+++ b/tests/TypeCheckCoreCoverageTest.php
@@ -494,7 +494,7 @@ public function testPropertyTagsAndArrayShapeTemplateCannotBeMixed(): void
new TypeCheckMixedPropertyAnnotationsData(['id' => 1]);
}
- public function testPropertyTagsAndArrayShapeAnnotationsCannotBeMixedAcrossInheritance(): void
+ public function testPropertyTagsInParentAndArrayShapeInChildCannotBeMixed(): void
{
$this->expectException(\TypeError::class);
$this->expectExceptionMessage('Use either @property tags or array-shape annotations');
From e255ca6892ff41cc7c80c83e8f07ea10c65d693c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 25 Apr 2026 02:16:47 +0000
Subject: [PATCH 16/22] Add PHPStan-typed array access for array-shape models
Agent-Logs-Url: https://github.com/voku/Arrayy/sessions/eb46a70a-e71e-46d5-aceb-b6761e8f4eee
Co-authored-by: voku <264695+voku@users.noreply.github.com>
---
build/docs/base.md | 14 +-
src/Arrayy.php | 323 ++++++++++----------
src/ArrayyIterator.php | 4 +-
src/ArrayyMeta.php | 4 +-
src/ArrayyRewindableGenerator.php | 2 +-
src/ArrayyStrict.php | 2 +-
src/Collection/AbstractCollection.php | 21 +-
src/Collection/Collection.php | 4 +-
src/Collection/CollectionInterface.php | 2 +-
src/Create.php | 2 +-
src/Mapper/Json.php | 2 +-
src/StaticArrayy.php | 4 +-
src/Type/DetectFirstValueTypeCollection.php | 2 +-
tests/BasicArrayTest.php | 2 +-
tests/CityData.php | 2 +-
tests/Collection/CollectionTest.php | 2 +-
tests/DocBlockScalarData.php | 2 +-
tests/GetAccountsResponse.php | 2 +-
tests/JsonMapperCoverageTest.php | 2 +-
tests/ModelA.php | 2 +-
tests/ModelB.php | 2 +-
tests/NativeCityData.php | 2 +-
tests/NativeIntersectionData.php | 2 +-
tests/NativeUserData.php | 2 +-
tests/PHPStan/ArrayShapeAccessTest.php | 40 +++
tests/PHPStan/ArrayShapeCity.php | 16 +
tests/PHPStan/ArrayShapeUser.php | 16 +
tests/TypeCheckCoreCoverageTest.php | 16 +-
tests/UserData.php | 2 +-
29 files changed, 291 insertions(+), 207 deletions(-)
create mode 100644 tests/PHPStan/ArrayShapeAccessTest.php
create mode 100644 tests/PHPStan/ArrayShapeCity.php
create mode 100644 tests/PHPStan/ArrayShapeUser.php
diff --git a/build/docs/base.md b/build/docs/base.md
index fb699cf..b182a06 100644
--- a/build/docs/base.md
+++ b/build/docs/base.md
@@ -117,12 +117,12 @@ $arrayy->Lars->lastname; // 'Müller'
## PhpDoc array-shape / property checking
-The library offers type checking for phpdoc array-shape annotations, legacy `@property` phpdoc-class-comments, and native declared properties. Prefer the array-shape form because it can reuse the `Arrayy` template for IDE autocompletion and static-analysis support. Do not combine array-shape annotations and `@property` tags on the same model.
+The library offers type checking for phpdoc array-shape annotations, legacy `@property` phpdoc-class-comments, and native declared properties. Prefer the array-shape form because it can reuse the `Arrayy` template for IDE autocompletion and static-analysis support. When you want PHPStan to check reads precisely, prefer array-like access with literal keys (for example `$user['lastName']`) on these array-shape-based models. Do not combine array-shape annotations and `@property` tags on the same model.
```php
/**
* @template T of array{id: int, firstName: int|string, lastName: string, city?: City|null}
- * @extends \Arrayy\Arrayy,value-of>
+ * @extends \Arrayy\Arrayy,value-of,T>
*/
class User extends \Arrayy\Arrayy
{
@@ -133,7 +133,7 @@ class User extends \Arrayy\Arrayy
/**
* @template T of array{plz: string|null, name: string, infos: string[]}
- * @extends \Arrayy\Arrayy,value-of>
+ * @extends \Arrayy\Arrayy,value-of,T>
*/
class City extends \Arrayy\Arrayy
{
@@ -161,7 +161,7 @@ $user = new User(
]
);
-var_dump($user['lastName']); // 'Moelleken'
+var_dump($user['lastName']); // 'Moelleken' // preferred for PHPStan-checked reads
var_dump($user[$userMeta->lastName]); // 'Moelleken'
var_dump($user->lastName); // Moelleken
@@ -211,7 +211,7 @@ complex example:
```php
/**
* @template T of array{id: int, firstName: string, lastName: string}
- * @extends \Arrayy\Arrayy,value-of>
+ * @extends \Arrayy\Arrayy,value-of,T>
*/
class User extends \Arrayy\Arrayy
{
@@ -476,7 +476,7 @@ namespace Arrayy\tests;
/**
* @template T of array{id: int, firstName: int|string, lastName: string, city?: \Arrayy\tests\CityData|null}
- * @extends \Arrayy\Arrayy,value-of>
+ * @extends \Arrayy\Arrayy,value-of,T>
*/
class UserData extends \Arrayy\Arrayy
{
@@ -487,7 +487,7 @@ class UserData extends \Arrayy\Arrayy
/**
* @template T of array{plz: string|null, name: string, infos: string[]}
- * @extends \Arrayy\Arrayy,value-of>
+ * @extends \Arrayy\Arrayy,value-of,T>
*/
class CityData extends \Arrayy\Arrayy
{
diff --git a/src/Arrayy.php b/src/Arrayy.php
index 9e392fd..824cbf3 100644
--- a/src/Arrayy.php
+++ b/src/Arrayy.php
@@ -32,6 +32,7 @@
*
* @template TKey of array-key
* @template T
+ * @template TData of array
* @extends \ArrayObject
* @implements \IteratorAggregate
* @implements \ArrayAccess
@@ -235,8 +236,9 @@ public function __unset($key)
* @return mixed
* Get a Value from the current array.
*
- * @phpstan-param TKey $key
- * @phpstan-return null|self|T
+ * @template TAccessKey of key-of
+ * @phpstan-param TAccessKey $key
+ * @phpstan-return TData[TAccessKey]|null|self>
*/
public function &__get($key)
{
@@ -264,7 +266,7 @@ public function &__get($key)
*
* @phpstan-param T $value
* @phpstan-param TKey $key
- * @phpstan-return static
+ * @phpstan-return static
*
* @psalm-mutation-free
*/
@@ -302,7 +304,7 @@ public function add($value, $key = null)
*
* @phpstan-param T $value
* @phpstan-param TKey|null $key
- * @phpstan-return static
+ * @phpstan-return static
*/
#[\ReturnTypeWillChange]
public function append($value, $key = null): self
@@ -345,7 +347,7 @@ public function append($value, $key = null): self
*
* @phpstan-param T $value
* @phpstan-param TKey $key
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function appendImmutable($value, $key = null): self
@@ -388,7 +390,7 @@ public function appendImmutable($value, $key = null): self
* @return $this
* (Mutable) Return this Arrayy object.
*
- * @phpstan-return static
+ * @phpstan-return static
*/
#[\ReturnTypeWillChange]
public function asort(int $sort_flags = 0): self
@@ -412,7 +414,7 @@ public function asort(int $sort_flags = 0): self
* @return $this
* (Immutable) Return this Arrayy object.
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function asortImmutable(int $sort_flags = 0): self
@@ -496,7 +498,7 @@ public function count(int $mode = \COUNT_NORMAL): int
*
* @return array
*
- * @phpstan-param T|array|self $data
+ * @phpstan-param T|array|self $data
* @phpstan-return array
*/
public function exchangeArray($data): array
@@ -590,7 +592,7 @@ public function getIteratorClass(): string
* @return $this
* (Mutable) Return this Arrayy object.
*
- * @phpstan-return static
+ * @phpstan-return static
*/
#[\ReturnTypeWillChange]
public function ksort(int $sort_flags = 0): self
@@ -614,7 +616,7 @@ public function ksort(int $sort_flags = 0): self
* @return $this
* (Immutable) Return this Arrayy object.
*
- * @phpstan-return static
+ * @phpstan-return static
*/
public function ksortImmutable(int $sort_flags = 0): self
{
@@ -634,7 +636,7 @@ public function ksortImmutable(int $sort_flags = 0): self
* @return $this
* (Mutable) Return this Arrayy object.
*
- * @phpstan-return static
+ * @phpstan-return static
*/
#[\ReturnTypeWillChange]
public function natcasesort(): self
@@ -652,7 +654,7 @@ public function natcasesort(): self
* @return $this
* (Immutable) Return this Arrayy object.
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function natcasesortImmutable(): self
@@ -673,7 +675,7 @@ public function natcasesortImmutable(): self
* @return $this
* (Mutable) Return this Arrayy object.
*
- * @phpstan-return static
+ * @phpstan-return static
*/
#[\ReturnTypeWillChange]
public function natsort(): self
@@ -691,7 +693,7 @@ public function natsort(): self
* @return $this
* (Immutable) Return this Arrayy object.
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function natsortImmutable(): self
@@ -772,7 +774,9 @@ static function ($container) use ($lastOffset, &$offsetExists) {
* @return mixed
* Will return null if the offset did not exists.
*
- * @phpstan-param TKey $offset
+ * @template TOffset of key-of
+ * @phpstan-param TOffset $offset
+ * @phpstan-return TData[TOffset]|null
*/
#[\ReturnTypeWillChange]
public function &offsetGet($offset)
@@ -781,9 +785,11 @@ public function &offsetGet($offset)
$value = null;
if ($this->offsetExists($offset)) {
+ /** @phpstan-ignore-next-line */
$value = &$this->__get($offset);
}
+ /** @phpstan-ignore-next-line */
return $value;
}
@@ -943,7 +949,7 @@ public function setIteratorClass($iteratorClass)
* (Mutable) Return this Arrayy object.
*
* @phpstan-param callable(T,T):int $callable
- * @phpstan-return static
+ * @phpstan-return static
*/
#[\ReturnTypeWillChange]
public function uasort($callable): self
@@ -970,7 +976,7 @@ public function uasort($callable): self
* (Immutable) Return this Arrayy object.
*
* @phpstan-param callable(T,T):int $callable
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function uasortImmutable($callable): self
@@ -996,7 +1002,7 @@ public function uasortImmutable($callable): self
* (Mutable) Return this Arrayy object.
*
* @phpstan-param callable(TKey,TKey):int $callable
- * @phpstan-return static
+ * @phpstan-return static
*/
#[\ReturnTypeWillChange]
public function uksort($callable): self
@@ -1015,7 +1021,7 @@ public function uksort($callable): self
* (Immutable) Return this Arrayy object.
*
* @phpstan-param callable(TKey,TKey):int $callable
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function uksortImmutable($callable): self
@@ -1035,7 +1041,7 @@ public function uksortImmutable($callable): self
*
* @return $this
*
- * @phpstan-return static
+ * @phpstan-return static
*/
#[\ReturnTypeWillChange]
public function unserialize($string): self
@@ -1064,7 +1070,7 @@ public function unserialize($string): self
*
* @phpstan-param array $values
* @phpstan-param TKey|null $key
- * @phpstan-return static
+ * @phpstan-return static
*/
public function appendArrayValues(array $values, $key = null)
{
@@ -1101,7 +1107,7 @@ public function appendArrayValues(array $values, $key = null)
* @return static
* (Immutable) Return an Arrayy object, with the prefixed keys.
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function appendToEachKey($prefix): self
@@ -1136,7 +1142,7 @@ public function appendToEachKey($prefix): self
* @return static
* (Immutable) Return an Arrayy object, with the prefixed values.
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function appendToEachValue($prefix): self
@@ -1165,7 +1171,7 @@ public function appendToEachValue($prefix): self
* @return $this
* (Mutable) Return this Arrayy object.
*
- * @phpstan-return static
+ * @phpstan-return static
*/
public function arsort(): self
{
@@ -1182,7 +1188,7 @@ public function arsort(): self
* @return $this
* (Immutable) Return this Arrayy object.
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function arsortImmutable(): self
@@ -1213,7 +1219,7 @@ public function arsortImmutable(): self
* (Immutable)
*
* @phpstan-param \Closure(T,TKey):mixed $closure INFO: \Closure result is not used, but void is not supported in PHP 7.0
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function at(\Closure $closure): self
@@ -1284,7 +1290,7 @@ public function average($decimals = 0)
* @return static
* (Immutable)
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function changeKeyCase(int $case = \CASE_LOWER): self
@@ -1333,7 +1339,7 @@ public function changeKeyCase(int $case = \CASE_LOWER): self
* @return $this
* (Mutable) Return this Arrayy object.
*
- * @phpstan-return static
+ * @phpstan-return static
*/
public function changeSeparator($separator): self
{
@@ -1355,7 +1361,7 @@ public function changeSeparator($separator): self
* @return static|static[]
* (Immutable) A new array of chunks from the original array.
*
- * @phpstan-return static>
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function chunk($size, $preserveKeys = false): self
@@ -1419,7 +1425,7 @@ public function chunk($size, $preserveKeys = false): self
* @return static
* (Immutable)
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function clean(): self
@@ -1443,7 +1449,7 @@ static function ($value) {
* @return $this
* (Mutable) Return this Arrayy object, with an empty array.
*
- * @phpstan-return static
+ * @phpstan-return static
*/
public function clear($key = null): self
{
@@ -1742,12 +1748,12 @@ public function containsValues(array $needles): bool
* keys and their count as value.
*
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function countValues(): self
{
- /** @phpstan-var static $return - help for phpstan */
+ /** @phpstan-var static $return - help for phpstan */
$return = self::create(\array_count_values($this->toArray()), $this->iteratorClass);
return $return;
@@ -1764,7 +1770,7 @@ public function countValues(): self
* (Immutable) Returns an new instance of the Arrayy object.
*
* @phpstan-param class-string<\Arrayy\ArrayyIterator> $iteratorClass
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public static function create(
@@ -1772,11 +1778,14 @@ public static function create(
string $iteratorClass = ArrayyIterator::class,
bool $checkPropertiesInConstructor = true
) {
- return new static( // @phpstan-ignore new.static
+ /** @var static $instance */
+ $instance = new static( // @phpstan-ignore new.static
$data,
$iteratorClass,
$checkPropertiesInConstructor
);
+
+ return $instance;
}
/**
@@ -1828,7 +1837,7 @@ public function flatten($delimiter = '.', $prepend = '', $items = null)
* (Mutable) Return this Arrayy object.
*
* @phpstan-param array $array
- * @phpstan-return $this
+ * @phpstan-return $this
*
* @internal this will not check any types because it's set directly as reference
*/
@@ -1849,7 +1858,7 @@ public function createByReference(array &$array = []): self
* (Immutable) Returns an new instance of the Arrayy object.
*
* @phpstan-param callable():\Generator $generatorFunction
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public static function createFromGeneratorFunction(callable $generatorFunction): self
@@ -1866,7 +1875,7 @@ public static function createFromGeneratorFunction(callable $generatorFunction):
* (Immutable) Returns an new instance of the Arrayy object.
*
* @phpstan-param \Generator $generator
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public static function createFromGeneratorImmutable(\Generator $generator): self
@@ -1882,7 +1891,7 @@ public static function createFromGeneratorImmutable(\Generator $generator): self
* @return static
* (Immutable) Returns an new instance of the Arrayy object.
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public static function createFromJson(string $json): self
@@ -1899,7 +1908,7 @@ public static function createFromJson(string $json): self
* (Immutable) Returns an new instance of the Arrayy object.
*
* @phpstan-param array $array
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public static function createFromArray(array $array): self
@@ -1916,7 +1925,7 @@ public static function createFromArray(array $array): self
* (Immutable) Returns an new instance of the Arrayy object.
*
* @phpstan-param \Traversable $object
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public static function createFromObject(\Traversable $object): self
@@ -1948,7 +1957,7 @@ public static function createFromObject(\Traversable $object): self
* @return static
* (Immutable) Returns an new instance of the Arrayy object.
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public static function createFromObjectVars($object): self
@@ -1967,7 +1976,7 @@ public static function createFromObjectVars($object): self
* @return static
* (Immutable) Returns an new instance of the Arrayy object.
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public static function createFromString(string $str, ?string $delimiter = null, ?string $regEx = null): self
@@ -2000,7 +2009,7 @@ static function (&$val) {
}
);
- /** @var static $return - help for phpstan */
+ /** @var static $return - help for phpstan */
$return = static::create($array);
return $return;
@@ -2018,7 +2027,7 @@ static function (&$val) {
* (Immutable) Returns an new instance of the Arrayy object.
*
* @phpstan-param \Traversable $traversable
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public static function createFromTraversableImmutable(\Traversable $traversable, bool $use_keys = true): self
@@ -2036,12 +2045,12 @@ public static function createFromTraversableImmutable(\Traversable $traversable,
* @return static
* (Immutable) Returns an new instance of the Arrayy object.
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public static function createWithRange($low, $high, $step = 1): self
{
- /** @phpstan-var static $return - help for phpstan */
+ /** @phpstan-var static $return - help for phpstan */
$return = static::create(\range($low, $high, $step));
return $return;
@@ -2087,7 +2096,7 @@ public function current()
* (Mutable) Return this Arrayy object.
*
* @phpstan-param callable(TKey,TKey):int $callable
- * @phpstan-return static
+ * @phpstan-return static
*/
public function customSortKeys(callable $callable): self
{
@@ -2111,7 +2120,7 @@ public function customSortKeys(callable $callable): self
* (Immutable) Return this Arrayy object.
*
* @phpstan-param callable(TKey,TKey):int $callable
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function customSortKeysImmutable(callable $callable): self
@@ -2150,7 +2159,7 @@ public function customSortKeysImmutable(callable $callable): self
* (Mutable) Return this Arrayy object.
*
* @phpstan-param callable(T,T):int $callable
- * @phpstan-return static
+ * @phpstan-return static
*/
public function customSortValues(callable $callable): self
{
@@ -2174,7 +2183,7 @@ public function customSortValues(callable $callable): self
* (Immutable) Return this Arrayy object.
*
* @phpstan-param callable(T,T):int $callable
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function customSortValuesImmutable($callable): self
@@ -2218,7 +2227,7 @@ public function delete($keyOrKeys)
* (Immutable)
*
* @phpstan-param array ...$array
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function diff(array ...$array): self
@@ -2253,7 +2262,7 @@ public function diff(array ...$array): self
* (Immutable)
*
* @phpstan-param array ...$array
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function diffKey(array ...$array): self
@@ -2288,7 +2297,7 @@ public function diffKey(array ...$array): self
* (Immutable)
*
* @phpstan-param array $array
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function diffKeyAndValue(array ...$array): self
@@ -2335,7 +2344,7 @@ public function diffKeyAndValue(array ...$array): self
*
* @phpstan-param array $array
* @phpstan-param null|array|\Generator $helperVariableForRecursion
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function diffRecursive(array $array = [], $helperVariableForRecursion = null): self
@@ -2387,7 +2396,7 @@ public function diffRecursive(array $array = [], $helperVariableForRecursion = n
* (Immutable)
*
* @phpstan-param array $array
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function diffReverse(array $array = []): self
@@ -2409,7 +2418,7 @@ public function diffReverse(array $array = []): self
* @return static
* (Immutable)
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function divide(): self
@@ -2441,7 +2450,7 @@ public function divide(): self
* (Immutable)
*
* @phpstan-param \Closure(T,?TKey):T $closure
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function each(\Closure $closure): self
@@ -2534,7 +2543,7 @@ public function exists(\Closure $closure): bool
* (Immutable)
*
* @phpstan-param T $default
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function fillWithDefaults(int $num, $default = null): self
@@ -2598,7 +2607,7 @@ public function fillWithDefaults(int $num, $default = null): self
* (Immutable)
*
* @phpstan-param null|(\Closure(T,TKey=):bool)|(\Closure(T):bool)|(\Closure(TKey):bool) $closure
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function filter($closure = null, int $flag = \ARRAY_FILTER_USE_BOTH)
@@ -2668,7 +2677,7 @@ public function filter($closure = null, int $flag = \ARRAY_FILTER_USE_BOTH)
* (Immutable)
*
* @phpstan-param array|T $value
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*
* @psalm-suppress MissingClosureReturnType
@@ -2826,7 +2835,7 @@ public function findKey(\Closure $closure)
* (Immutable)
*
* @phpstan-param array|T $value
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function findBy(string $property, $value, string $comparisonOp = 'eq'): self
@@ -2890,7 +2899,7 @@ public function firstKey()
* @return static
* (Immutable)
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function firstsImmutable(?int $number = null): self
@@ -2919,7 +2928,7 @@ public function firstsImmutable(?int $number = null): self
* @return static
* (Immutable)
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function firstsKeys(?int $number = null): self
@@ -2952,7 +2961,7 @@ public function firstsKeys(?int $number = null): self
* @return $this
* (Mutable)
*
- * @phpstan-return ($number is null ? static : static)
+ * @phpstan-return ($number is null ? static : static)
*/
public function firstsMutable(?int $number = null): self
{
@@ -2980,7 +2989,7 @@ public function firstsMutable(?int $number = null): self
* @return static
* (Immutable)
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function flip(): self
@@ -3333,7 +3342,7 @@ public function getList(bool $convertAllArrayyElements = false): array
* @return static
* (Immutable)
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function getColumn($columnKey = null, $indexKey = null): self
@@ -3461,7 +3470,7 @@ public function getBackwardsGenerator(): \Generator
*
* @see Arrayy::keys()
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function getKeys()
@@ -3487,7 +3496,7 @@ public function getObject(): \stdClass
*
* @see Arrayy::randomImmutable()
*
- * @phpstan-return static
+ * @phpstan-return static
*/
public function getRandom(): self
{
@@ -3519,7 +3528,7 @@ public function getRandomKey()
*
* @see Arrayy::randomKeys()
*
- * @phpstan-return static
+ * @phpstan-return static
*/
public function getRandomKeys(int $number): self
{
@@ -3551,7 +3560,7 @@ public function getRandomValue()
*
* @see Arrayy::randomValues()
*
- * @phpstan-return static
+ * @phpstan-return static
*/
public function getRandomValues(int $number): self
{
@@ -3565,7 +3574,7 @@ public function getRandomValues(int $number): self
* The values of all elements in this array, in the order they
* appear in the array.
*
- * @phpstan-return static
+ * @phpstan-return static
*/
public function getValues()
{
@@ -3602,7 +3611,7 @@ public function getValuesYield(): \Generator
* (Immutable)
*
* @phpstan-param \Closure(T,TKey):TKey|TKey $grouper
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function group($grouper, bool $saveKeys = false): self
@@ -3741,7 +3750,7 @@ public function implodeKeys(string $glue = ''): string
* (Immutable)
*
* @phpstan-param array-key $key
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function indexBy($key): self
@@ -3792,7 +3801,7 @@ public function indexOf($value)
* @return static
* (Immutable)
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function initial(int $to = 1): self
@@ -3814,7 +3823,7 @@ public function initial(int $to = 1): self
* (Immutable)
*
* @phpstan-param array $search
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function intersection(array $search, bool $keepKeys = false): self
@@ -3853,7 +3862,7 @@ static function ($a, $b) {
* (Immutable)
*
* @phpstan-param array> ...$array
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function intersectionMulti(...$array): self
@@ -3893,7 +3902,7 @@ public function intersects(array $search): bool
* (Immutable)
*
* @phpstan-param callable(T,mixed=):mixed $callable
- * @phpstan-return static|static
+ * @phpstan-return static|static
* @psalm-mutation-free
*/
public function invoke($callable, $arguments = []): self
@@ -4149,7 +4158,7 @@ public function keyExists($key): bool
* (Immutable) An array of all the keys in input.
*
* @phpstan-param null|T|T[] $search_values
- * @phpstan-return static
+ * @phpstan-return static
*
* @psalm-mutation-free
*/
@@ -4236,7 +4245,7 @@ public function keys(
* @return $this
* (Mutable) Return this Arrayy object.
*
- * @phpstan-return static
+ * @phpstan-return static
*/
public function krsort(int $sort_flags = 0): self
{
@@ -4259,7 +4268,7 @@ public function krsort(int $sort_flags = 0): self
* @return $this
* (Immutable)
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function krsortImmutable(int $sort_flags = 0): self
@@ -4328,7 +4337,7 @@ public function lastKey()
* @return static
* (Immutable)
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function lastsImmutable(?int $number = null): self
@@ -4374,7 +4383,7 @@ public function lastsImmutable(?int $number = null): self
* @return $this
* (Mutable)
*
- * @phpstan-return static
+ * @phpstan-return static
*/
public function lastsMutable(?int $number = null): self
{
@@ -4423,7 +4432,7 @@ public function length(int $mode = \COUNT_NORMAL): int
* The output value type.
*
* @phpstan-param callable(T,TKey=,mixed=):T2 $callable
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function map(
@@ -4579,7 +4588,7 @@ public function max()
* (Immutable)
*
* @phpstan-param array $array
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function mergeAppendKeepIndex(array $array = [], bool $recursive = false): self
@@ -4621,7 +4630,7 @@ public function mergeAppendKeepIndex(array $array = [], bool $recursive = false)
* (Immutable)
*
* @phpstan-param array $array
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function mergeAppendNewIndex(array $array = [], bool $recursive = false): self
@@ -4662,7 +4671,7 @@ public function mergeAppendNewIndex(array $array = [], bool $recursive = false):
* (Immutable)
*
* @phpstan-param array $array
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function mergePrependKeepIndex(array $array = [], bool $recursive = false): self
@@ -4704,7 +4713,7 @@ public function mergePrependKeepIndex(array $array = [], bool $recursive = false
* (Immutable)
*
* @phpstan-param array $array
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function mergePrependNewIndex(array $array = [], bool $recursive = false): self
@@ -4790,7 +4799,7 @@ public function mostUsedValue()
* @return static
* (Immutable)
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function mostUsedValues(?int $number = null): self
@@ -4812,7 +4821,7 @@ public function mostUsedValues(?int $number = null): self
* @return static
* (Immutable)
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function moveElement($from, $to): self
@@ -4860,7 +4869,7 @@ public function moveElement($from, $to): self
* @return static
* (Immutable)
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function moveElementToFirstPlace($key): self
@@ -4891,7 +4900,7 @@ public function moveElementToFirstPlace($key): self
* @return static
* (Immutable)
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function moveElementToLastPlace($key): self
@@ -4939,7 +4948,7 @@ public function next()
* @return static
* (Immutable)
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function nth(int $step, int $offset = 0): self
@@ -4971,7 +4980,7 @@ public function nth(int $step, int $offset = 0): self
* (Immutable)
*
* @phpstan-param array-key[] $keys
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function only(array $keys): self
@@ -5002,7 +5011,7 @@ public function only(array $keys): self
* @return static
* (Immutable) Arrayy object padded to $size with $value.
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function pad(int $size, $value): self
@@ -5027,7 +5036,7 @@ public function pad(int $size, $value): self
* contains the array of elements where the predicate returned FALSE.
*
* @phpstan-param \Closure(T,TKey):bool $closure
- * @phpstan-return array>
+ * @phpstan-return array
*/
public function partition(\Closure $closure): array
{
@@ -5076,7 +5085,7 @@ public function pop()
*
* @phpstan-param T $value
* @phpstan-param TKey|null $key
- * @phpstan-return static
+ * @phpstan-return static
*/
public function prepend($value, $key = null)
{
@@ -5110,7 +5119,7 @@ public function prepend($value, $key = null)
*
* @phpstan-param T $value
* @phpstan-param TKey $key
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function prependImmutable($value, $key = null)
@@ -5146,7 +5155,7 @@ public function prependImmutable($value, $key = null)
* @return static
* (Immutable) Return an Arrayy object, with the prepended keys.
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function prependToEachKey($suffix): self
@@ -5184,7 +5193,7 @@ public function prependToEachKey($suffix): self
* @return static
* (Immutable) Return an Arrayy object, with the prepended values.
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function prependToEachValue($suffix): self
@@ -5264,7 +5273,7 @@ public function pull($keyOrKeys = null, $fallback = null)
* @noinspection ReturnTypeCanBeDeclaredInspection
*
* @phpstan-param array ...$args
- * @phpstan-return static
+ * @phpstan-return static
*/
public function push(...$args)
{
@@ -5297,7 +5306,7 @@ public function push(...$args)
* @return static
* (Immutable)
*
- * @phpstan-return static
+ * @phpstan-return static
*/
public function randomImmutable(?int $number = null): self
{
@@ -5371,7 +5380,7 @@ public function randomKey()
* @return static
* (Immutable)
*
- * @phpstan-return static
+ * @phpstan-return static
*/
public function randomKeys(int $number): self
{
@@ -5414,7 +5423,7 @@ public function randomKeys(int $number): self
* @return $this
* (Mutable) Return this Arrayy object.
*
- * @phpstan-return static
+ * @phpstan-return static
*/
public function randomMutable(?int $number = null): self
{
@@ -5475,7 +5484,7 @@ public function randomValue()
* @return static
* (Mutable)
*
- * @phpstan-return static
+ * @phpstan-return static
*/
public function randomValues(int $number): self
{
@@ -5492,11 +5501,11 @@ public function randomValues(int $number): self
* @param array $array
* @param int|null $number How many values you will take?
*
- * @return static
+ * @return static
* (Immutable)
*
* @phpstan-param array<(int&T)|(string&T),int> $array
- * @phpstan-return static
+ * @phpstan-return static
*/
public function randomWeighted(array $array, ?int $number = null): self
{
@@ -5539,7 +5548,7 @@ public function randomWeighted(array $array, ?int $number = null): self
* @phpstan-param callable(T2, T, TKey): T2 $callable
* @phpstan-param T2 $initial
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function reduce($callable, $initial = []): self
@@ -5548,7 +5557,7 @@ public function reduce($callable, $initial = []): self
$initial = $callable($initial, $value, $key);
}
- /** @var static $return - help for phpstan */
+ /** @var static $return - help for phpstan */
$return = static::create(
$initial,
$this->iteratorClass,
@@ -5564,7 +5573,7 @@ public function reduce($callable, $initial = []): self
* @return static
* (Immutable)
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function reduce_dimension(bool $unique = true): self
@@ -5601,7 +5610,7 @@ public function reduce_dimension(bool $unique = true): self
* @return $this
* (Mutable) Return this Arrayy object, with re-indexed array-elements.
*
- * @phpstan-return static
+ * @phpstan-return static
*/
public function reindex(): self
{
@@ -5628,7 +5637,7 @@ public function reindex(): self
* (Immutable)
*
* @phpstan-param \Closure(T,TKey):bool $closure
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function reject(\Closure $closure): self
@@ -5662,7 +5671,7 @@ public function reject(\Closure $closure): self
* (Mutable)
*
* @phpstan-param TKey|TKey[] $key
- * @phpstan-return static
+ * @phpstan-return static
*/
public function remove($key)
{
@@ -5697,7 +5706,7 @@ public function remove($key)
* (Immutable)
*
* @phpstan-param T $element
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function removeElement($element)
@@ -5715,7 +5724,7 @@ public function removeElement($element)
* @return static
* (Immutable)
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function removeFirst(): self
@@ -5741,7 +5750,7 @@ public function removeFirst(): self
* @return static
* (Immutable)
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function removeLast(): self
@@ -5770,7 +5779,7 @@ public function removeLast(): self
* (Immutable)
*
* @phpstan-param T $value
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function removeValue($value): self
@@ -5805,7 +5814,7 @@ public function removeValue($value): self
* @return static
* (Immutable)
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function repeat($times): self
@@ -5839,7 +5848,7 @@ public function repeat($times): self
* @phpstan-param TKey $oldKey
* @phpstan-param TKey $newKey
* @phpstan-param T $newValue
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function replace($oldKey, $newKey, $newValue): self
@@ -5879,7 +5888,7 @@ public function replace($oldKey, $newKey, $newValue): self
*
*
* @phpstan-param array $keys
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function replaceAllKeys(array $keys): self
@@ -5923,7 +5932,7 @@ public function replaceAllKeys(array $keys): self
*
*
* @phpstan-param array $array
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function replaceAllValues(array $array): self
@@ -5954,7 +5963,7 @@ public function replaceAllValues(array $array): self
* (Immutable)
*
* @phpstan-param array $keys
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function replaceKeys(array $keys): self
@@ -5989,7 +5998,7 @@ public function replaceKeys(array $keys): self
*
* @phpstan-param T $search
* @phpstan-param T $replacement
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function replaceOneValue($search, $replacement = ''): self
@@ -6022,7 +6031,7 @@ public function replaceOneValue($search, $replacement = ''): self
* @return static
* (Immutable)
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function replaceValues($search, $replacement = ''): self
@@ -6047,7 +6056,7 @@ public function replaceValues($search, $replacement = ''): self
* @return static
* (Immutable)
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function rest(int $from = 1): self
@@ -6071,7 +6080,7 @@ public function rest(int $from = 1): self
* @return $this
* (Mutable) Return this Arrayy object.
*
- * @phpstan-return static
+ * @phpstan-return static
*/
public function reverse(): self
{
@@ -6092,7 +6101,7 @@ public function reverse(): self
* @return $this
* (Mutable) Return this Arrayy object.
*
- * @phpstan-return static
+ * @phpstan-return static
*/
public function reverseKeepIndex(): self
{
@@ -6115,7 +6124,7 @@ public function reverseKeepIndex(): self
* @return $this
* (Mutable) Return this Arrayy object.
*
- * @phpstan-return static
+ * @phpstan-return static
*/
public function rsort(int $sort_flags = 0): self
{
@@ -6138,7 +6147,7 @@ public function rsort(int $sort_flags = 0): self
* @return $this
* (Immutable) Return this Arrayy object.
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function rsortImmutable(int $sort_flags = 0): self
@@ -6194,7 +6203,7 @@ public function searchIndex($value)
* (Immutable) Will return a empty Arrayy if the value wasn't found.
*
* @phpstan-param TKey $index
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function searchValue($index): self
@@ -6245,7 +6254,7 @@ public function searchValue($index): self
*
* @phpstan-param TKey $key
* @phpstan-param T $value
- * @phpstan-return static
+ * @phpstan-return static
*/
public function set($key, $value): self
{
@@ -6315,7 +6324,7 @@ public function shift()
* (Immutable)
*
* @phpstan-param array $array
- * @phpstan-return static
+ * @phpstan-return static
*/
public function shuffle(bool $secure = false, ?array $array = null): self
{
@@ -6520,7 +6529,7 @@ public function sizeRecursive(): int
* @return static
* (Immutable) A slice of the original array with length $length.
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function slice(int $offset, ?int $length = null, bool $preserveKeys = false)
@@ -6554,7 +6563,7 @@ public function slice(int $offset, ?int $length = null, bool $preserveKeys = fal
* @return static
* (Mutable) Return this Arrayy object.
*
- * @phpstan-return static
+ * @phpstan-return static
*/
public function sort(
$direction = \SORT_ASC,
@@ -6582,7 +6591,7 @@ public function sort(
* @return static
* (Immutable) Return this Arrayy object.
*
- * @phpstan-return static
+ * @phpstan-return static
*/
public function sortImmutable(
$direction = \SORT_ASC,
@@ -6618,7 +6627,7 @@ public function sortImmutable(
* @return $this
* (Mutable) Return this Arrayy object.
*
- * @phpstan-return static
+ * @phpstan-return static
*/
public function sortKeys(
$direction = \SORT_ASC,
@@ -6644,7 +6653,7 @@ public function sortKeys(
* @return $this
* (Immutable) Return this Arrayy object.
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function sortKeysImmutable(
@@ -6675,7 +6684,7 @@ public function sortKeysImmutable(
* @return static
* (Mutable)
*
- * @phpstan-return static
+ * @phpstan-return static
*/
public function sortValueKeepIndex(
$direction = \SORT_ASC,
@@ -6698,7 +6707,7 @@ public function sortValueKeepIndex(
* @return static
* (Mutable)
*
- * @phpstan-return static
+ * @phpstan-return static
*/
public function sortValueNewIndex($direction = \SORT_ASC, int $strategy = \SORT_REGULAR): self
{
@@ -6731,7 +6740,7 @@ public function sortValueNewIndex($direction = \SORT_ASC, int $strategy = \SORT_
* (Immutable)
*
* @pslam-param callable|T|null $sorter
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function sorter($sorter = null, $direction = \SORT_ASC, int $strategy = \SORT_REGULAR): self
@@ -6785,7 +6794,7 @@ static function ($value) use ($sorter) {
* (Immutable)
*
* @phpstan-param array $replacement
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function splice(int $offset, ?int $length = null, $replacement = []): self
@@ -6819,7 +6828,7 @@ public function splice(int $offset, ?int $length = null, $replacement = []): sel
* @return static
* (Immutable)
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function split(int $numberOfPieces = 2, bool $keepKeys = false): self
@@ -6889,7 +6898,7 @@ public function split(int $numberOfPieces = 2, bool $keepKeys = false): self
* @return static
* (Immutable)
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function stripEmpty(): self
@@ -6918,7 +6927,7 @@ static function ($item) {
* @return static
* (Immutable)
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function swap($swapA, $swapB): self
@@ -7034,7 +7043,7 @@ public function toJson(int $options = 0, int $depth = 512): string
*
* @return static|static[]
*
- * @phpstan-return static>
+ * @phpstan-return static
*/
public function toPermutation(?array $items = null, array $helper = []): self
{
@@ -7063,7 +7072,7 @@ public function toPermutation(?array $items = null, array $helper = []): self
}
}
- /** @var static> $return - help for phpstan */
+ /** @var static $return - help for phpstan */
$return = static::create(
$return,
$this->iteratorClass,
@@ -7096,7 +7105,7 @@ public function toString(string $separator = ','): string
* @return $this
* (Mutable)
*
- * @phpstan-return static
+ * @phpstan-return static
*/
public function uniqueNewIndex(): self
{
@@ -7127,7 +7136,7 @@ static function ($resultArray, $value, $key) {
* @return $this
* (Mutable)
*
- * @phpstan-return static
+ * @phpstan-return static
*/
public function uniqueKeepIndex(): self
{
@@ -7164,7 +7173,7 @@ static function ($resultArray, $key) use ($array) {
*
* @see Arrayy::unique()
*
- * @phpstan-return static
+ * @phpstan-return static
*/
public function unique(): self
{
@@ -7180,7 +7189,7 @@ public function unique(): self
* (Mutable) Return this Arrayy object, with prepended elements to the beginning of array.
*
* @phpstan-param array ...$args
- * @phpstan-return static
+ * @phpstan-return static
*/
public function unshift(...$args): self
{
@@ -7233,7 +7242,7 @@ public function validate(\Closure $closure): bool
* @return static
* (Immutable)
*
- * @phpstan-return static
+ * @phpstan-return static
* @psalm-mutation-free
*/
public function values(): self
@@ -7277,7 +7286,7 @@ function () {
*
* @phostan-param TExtra $userData
* @phpstan-param callable(T,TKey,?TExtra):void $callable
- * @phpstan-return static
+ * @phpstan-return static
*/
public function walk(
$callable,
@@ -7318,7 +7327,7 @@ public function walk(
*
* @return static
*
- * @phpstan-return static
+ * @phpstan-return static
*/
public function where(string $keyOrPropertyOrMethod, $value): self
{
@@ -7491,7 +7500,7 @@ protected function callAtPath($path, $callable, &$currentOffset = null)
/**
* Extracts the value of the given property or method from the object.
*
- * @param static $object
+ * @param static $object
* The object to extract the value from.
* @param string $keyOrPropertyOrMethod
* The property or method for which the
@@ -7502,7 +7511,7 @@ protected function callAtPath($path, $callable, &$currentOffset = null)
* @return mixed
*
The value extracted from the specified property or method.
*
- * @phpstan-param self $object
+ * @phpstan-param self $object
*/
final protected function extractValue(self $object, string $keyOrPropertyOrMethod)
{
@@ -8214,7 +8223,7 @@ protected function setInitialValuesAndProperties(array &$data, bool $checkProper
* (Mutable) Return this Arrayy object.
*
* @phpstan-param array $elements
- * @phpstan-return static
+ * @phpstan-return static
*/
protected function sorterKeys(
array &$elements,
@@ -8249,7 +8258,7 @@ protected function sorterKeys(
* (Mutable) Return this Arrayy object.
*
* @phpstan-param array $elements
- * @phpstan-return static
+ * @phpstan-return static
*/
protected function sorting(
array &$elements,
diff --git a/src/ArrayyIterator.php b/src/ArrayyIterator.php
index 7b21732..f2b1e22 100644
--- a/src/ArrayyIterator.php
+++ b/src/ArrayyIterator.php
@@ -14,7 +14,7 @@ class ArrayyIterator extends \ArrayIterator
/**
* @var string
*
- * @phpstan-var string|class-string<\Arrayy\Arrayy>
+ * @phpstan-var string|class-string<\Arrayy\Arrayy>>
*/
private $class;
@@ -55,7 +55,7 @@ public function current()
* Will return a "Arrayy"-object instead of an array.
*
* @phpstan-param TKey $offset
- * @param-return Arrayy|mixed
+ * @param-return Arrayy>|mixed
*/
#[\ReturnTypeWillChange]
public function offsetGet($offset)
diff --git a/src/ArrayyMeta.php b/src/ArrayyMeta.php
index 5b31258..f22093a 100644
--- a/src/ArrayyMeta.php
+++ b/src/ArrayyMeta.php
@@ -24,7 +24,7 @@ public function __get($name): string
*
* @return $this
*
- * @phpstan-param class-string<\Arrayy\Arrayy> $className
+ * @phpstan-param class-string<\Arrayy\Arrayy>> $className
*/
public function getMetaObject(string $className): self
{
@@ -36,7 +36,7 @@ public function getMetaObject(string $className): self
}
$reflector = new \ReflectionClass($className);
- /** @var Arrayy $instance */
+ /** @var Arrayy> $instance */
$instance = $reflector->newInstanceWithoutConstructor();
foreach ($instance->getPhpDocPropertiesFromClass() as $propertyName => $_) {
$this->{$propertyName} = $propertyName;
diff --git a/src/ArrayyRewindableGenerator.php b/src/ArrayyRewindableGenerator.php
index cdaadb7..3f97f8a 100644
--- a/src/ArrayyRewindableGenerator.php
+++ b/src/ArrayyRewindableGenerator.php
@@ -16,7 +16,7 @@ class ArrayyRewindableGenerator extends \ArrayIterator
/**
* @var string
*
- * @phpstan-var string|class-string<\Arrayy\Arrayy>
+ * @phpstan-var string|class-string<\Arrayy\Arrayy>>
*/
protected $class;
diff --git a/src/ArrayyStrict.php b/src/ArrayyStrict.php
index 32f1e79..bec2f4a 100644
--- a/src/ArrayyStrict.php
+++ b/src/ArrayyStrict.php
@@ -12,7 +12,7 @@
*
* @template TKey of array-key
* @template T
- * @extends \Arrayy\Arrayy
+ * @extends \Arrayy\Arrayy>
*/
class ArrayyStrict extends Arrayy implements \Arrayy\Type\TypeInterface
{
diff --git a/src/Collection/AbstractCollection.php b/src/Collection/AbstractCollection.php
index d99057d..a964198 100644
--- a/src/Collection/AbstractCollection.php
+++ b/src/Collection/AbstractCollection.php
@@ -23,7 +23,7 @@
*
* @template TKey of array-key
* @template T
- * @extends Arrayy
+ * @extends Arrayy>
* @implements CollectionInterface
*/
abstract class AbstractCollection extends Arrayy implements CollectionInterface
@@ -62,7 +62,7 @@ abstract class AbstractCollection extends Arrayy implements CollectionInterface
* true, otherwise this option didn't not work anyway.
*
*
- * @phpstan-param array|\Arrayy\Arrayy|\Closure():array|mixed $data
+ * @phpstan-param array|\Arrayy\Arrayy>|\Closure():array|mixed $data
* @phpstan-param class-string<\Arrayy\ArrayyIterator> $iteratorClass
*/
public function __construct(
@@ -105,7 +105,7 @@ public function __construct(
*
* @phpstan-param T|static $value
* @phpstan-param TKey|null $key
- * @phpstan-return static
+ * @phpstan-return static
*/
public function append($value, $key = null): Arrayy
{
@@ -168,7 +168,7 @@ public function offsetSet($offset, $value)
*
* @phpstan-param T|static $value
* @phpstan-param TKey|null $key
- * @phpstan-return static
+ * @phpstan-return static
*/
public function prepend($value, $key = null): Arrayy
{
@@ -237,7 +237,7 @@ abstract public function getType();
* @return $this
*
* @phpstan-param CollectionInterface ...$collections
- * @phpstan-return static
+ * @phpstan-return static
*/
public function merge(CollectionInterface ...$collections): self
{
@@ -264,7 +264,7 @@ public function merge(CollectionInterface ...$collections): self
* @template TCreate as T
* @phpstan-param array $data
* @phpstan-param class-string<\Arrayy\ArrayyIterator> $iteratorClass
- * @phpstan-return static
+ * @phpstan-return static
*
* @psalm-mutation-free
*/
@@ -273,11 +273,14 @@ public static function create(
string $iteratorClass = ArrayyIterator::class,
bool $checkPropertiesInConstructor = true
) {
- return new static( // @phpstan-ignore new.static
+ /** @var static $instance */
+ $instance = new static( // @phpstan-ignore new.static
$data,
$iteratorClass,
$checkPropertiesInConstructor
);
+
+ return $instance;
}
/**
@@ -286,7 +289,7 @@ public static function create(
* @return static
* (Immutable) Returns an new instance of the CollectionInterface object.
*
- * @phpstan-return static
+ * @phpstan-return static
*
* @psalm-mutation-free
*/
@@ -326,7 +329,7 @@ public static function createFromJsonMapper(string $json)
}
}
- /** @phpstan-var static */
+ /** @phpstan-var static */
return $return;
}
diff --git a/src/Collection/Collection.php b/src/Collection/Collection.php
index b1ffe87..a562505 100644
--- a/src/Collection/Collection.php
+++ b/src/Collection/Collection.php
@@ -97,7 +97,7 @@ class Collection extends AbstractCollection
*
* @param TypeInterface|null $type
*
- * @phpstan-param array|array|\Arrayy\Arrayy $data
+ * @phpstan-param array|array|\Arrayy\Arrayy> $data
* @phpstan-param class-string<\Arrayy\ArrayyIterator> $iteratorClass
*/
public function __construct(
@@ -136,7 +136,7 @@ public function __construct(
* @template TConstruct
* @phpstan-param string|class-string|class-string|TypeInterface|TypeCheckArray|array $type
* @phpstan-param array $data
- * @phpstan-return static
+ * @phpstan-return static
*/
public static function construct(
$type,
diff --git a/src/Collection/CollectionInterface.php b/src/Collection/CollectionInterface.php
index 42f1b2b..2debd7d 100644
--- a/src/Collection/CollectionInterface.php
+++ b/src/Collection/CollectionInterface.php
@@ -164,7 +164,7 @@ public function containsValueRecursive($value): bool;
* @template TCreate as T
* @phpstan-param array $data
* @phpstan-param class-string<\Arrayy\ArrayyIterator> $iteratorClass
- * @phpstan-return static
+ * @phpstan-return static
*
* @psalm-mutation-free
*/
diff --git a/src/Create.php b/src/Create.php
index 7e43f48..6b01ce8 100644
--- a/src/Create.php
+++ b/src/Create.php
@@ -69,7 +69,7 @@ function array_key_last(array $array)
*
* @param mixed $data
*
- * @return Arrayy
+ * @return Arrayy>
*/
function create($data): Arrayy
{
diff --git a/src/Mapper/Json.php b/src/Mapper/Json.php
index 0e46f14..a5724e0 100644
--- a/src/Mapper/Json.php
+++ b/src/Mapper/Json.php
@@ -416,7 +416,7 @@ private function inspectProperty(\ReflectionClass $rc, $name): array
$accessor = null;
/** @var \Arrayy\Arrayy[] $ARRAYY_CACHE */
- /** @phpstan-var array> $ARRAYY_CACHE */
+ /** @phpstan-var array>> $ARRAYY_CACHE */
static $ARRAYY_CACHE = [];
if (\is_subclass_of($class->name, \Arrayy\Arrayy::class)) {
diff --git a/src/StaticArrayy.php b/src/StaticArrayy.php
index 5dac472..0b09613 100644
--- a/src/StaticArrayy.php
+++ b/src/StaticArrayy.php
@@ -70,7 +70,7 @@ public static function __callStatic(string $name, $arguments)
* @param int|null $stop The stopping point
* @param int $step How many to increment of
*
- * @return Arrayy
+ * @return Arrayy>
*
* @psalm-suppress InvalidReturnStatement - why?
* @psalm-suppress InvalidReturnType - why?
@@ -93,7 +93,7 @@ public static function range(int $base, ?int $stop = null, int $step = 1): Array
* @param float|int|string|null $data
* @param int $times
*
- * @return Arrayy
+ * @return Arrayy>
*
* @psalm-suppress InvalidReturnStatement - why?
* @psalm-suppress InvalidReturnType - why?
diff --git a/src/Type/DetectFirstValueTypeCollection.php b/src/Type/DetectFirstValueTypeCollection.php
index 1010247..b0693a1 100644
--- a/src/Type/DetectFirstValueTypeCollection.php
+++ b/src/Type/DetectFirstValueTypeCollection.php
@@ -26,7 +26,7 @@ final class DetectFirstValueTypeCollection extends Collection implements TypeInt
* @param string $iteratorClass
* @param bool $checkPropertiesInConstructor
*
- * @phpstan-param array|Arrayy $data
+ * @phpstan-param array|Arrayy> $data
* @phpstan-param class-string<\Arrayy\ArrayyIterator> $iteratorClass
*/
public function __construct(
diff --git a/tests/BasicArrayTest.php b/tests/BasicArrayTest.php
index ae7e722..20b82e3 100644
--- a/tests/BasicArrayTest.php
+++ b/tests/BasicArrayTest.php
@@ -24,7 +24,7 @@ final class BasicArrayTest extends \PHPUnit\Framework\TestCase
const TYPE_NUMERIC = 'numeric';
/**
- * @var class-string<\Arrayy\Arrayy>
+ * @var class-string<\Arrayy\Arrayy>>
*/
protected $arrayyClassName = A::class;
diff --git a/tests/CityData.php b/tests/CityData.php
index 636b6d3..62170a2 100644
--- a/tests/CityData.php
+++ b/tests/CityData.php
@@ -7,7 +7,7 @@
* @property string $name
* @property string[] $infos
*
- * @extends \Arrayy\Arrayy
+ * @extends \Arrayy\Arrayy>
*/
class CityData extends \Arrayy\Arrayy
{
diff --git a/tests/Collection/CollectionTest.php b/tests/Collection/CollectionTest.php
index d29a654..eb705e0 100644
--- a/tests/Collection/CollectionTest.php
+++ b/tests/Collection/CollectionTest.php
@@ -88,7 +88,7 @@ public function testUserDataCollectionFromJsonMulti()
{
$json = '[{"id":1,"firstName":"Lars","lastName":"Moelleken","city":{"name":"Düsseldorf","plz":null,"infos":["lall"]}}, {"id":1,"firstName":"Sven","lastName":"Moelleken","city":{"name":"Köln","plz":null,"infos":["foo"]}}]';
/** @var \Arrayy\Arrayy|\Arrayy\tests\UserData[] $userDataCollection */
- /** @phpstan-var \Arrayy\Arrayy $userDataCollection */
+ /** @phpstan-var \Arrayy\Arrayy> $userDataCollection */
$userDataCollection = UserDataCollection::createFromJsonMapper($json);
$userDataCollection->getAll();
diff --git a/tests/DocBlockScalarData.php b/tests/DocBlockScalarData.php
index 21c4cbc..b687c1b 100644
--- a/tests/DocBlockScalarData.php
+++ b/tests/DocBlockScalarData.php
@@ -5,7 +5,7 @@
/**
* @property scalar $value
*
- * @extends \Arrayy\Arrayy
+ * @extends \Arrayy\Arrayy>
*/
class DocBlockScalarData extends \Arrayy\Arrayy
{
diff --git a/tests/GetAccountsResponse.php b/tests/GetAccountsResponse.php
index 708102a..677a524 100644
--- a/tests/GetAccountsResponse.php
+++ b/tests/GetAccountsResponse.php
@@ -7,7 +7,7 @@
/**
* @property \Arrayy\tests\AccountCollection $accounts
*
- * @extends \Arrayy\Arrayy
+ * @extends \Arrayy\Arrayy>
*/
class GetAccountsResponse extends Arrayy
{
diff --git a/tests/JsonMapperCoverageTest.php b/tests/JsonMapperCoverageTest.php
index 397d291..a8d599f 100644
--- a/tests/JsonMapperCoverageTest.php
+++ b/tests/JsonMapperCoverageTest.php
@@ -194,7 +194,7 @@ final class JsonMapperAccountHolderFixture
/**
* @property \Arrayy\tests\CityData $city
- * @extends \Arrayy\Arrayy
+ * @extends \Arrayy\Arrayy>
*/
final class JsonMapperArrayyCityHolderFixture extends Arrayy
{
diff --git a/tests/ModelA.php b/tests/ModelA.php
index 0112e57..fdddd25 100644
--- a/tests/ModelA.php
+++ b/tests/ModelA.php
@@ -5,7 +5,7 @@
use Arrayy\ArrayyIterator;
/**
- * @extends \Arrayy\Arrayy
+ * @extends \Arrayy\Arrayy>
*/
class ModelA extends \Arrayy\Arrayy implements ModelInterface
{
diff --git a/tests/ModelB.php b/tests/ModelB.php
index ad0f2ad..388a66f 100644
--- a/tests/ModelB.php
+++ b/tests/ModelB.php
@@ -3,7 +3,7 @@
namespace Arrayy\tests;
/**
- * @extends \Arrayy\Arrayy
+ * @extends \Arrayy\Arrayy>
*/
class ModelB extends \Arrayy\Arrayy implements ModelInterface
{
diff --git a/tests/NativeCityData.php b/tests/NativeCityData.php
index 8aa5f78..e4b0c8e 100644
--- a/tests/NativeCityData.php
+++ b/tests/NativeCityData.php
@@ -3,7 +3,7 @@
namespace Arrayy\tests;
/**
- * @extends \Arrayy\Arrayy
+ * @extends \Arrayy\Arrayy>
*/
class NativeCityData extends \Arrayy\Arrayy
{
diff --git a/tests/NativeIntersectionData.php b/tests/NativeIntersectionData.php
index f6225ce..214880c 100644
--- a/tests/NativeIntersectionData.php
+++ b/tests/NativeIntersectionData.php
@@ -3,7 +3,7 @@
namespace Arrayy\tests;
/**
- * @extends \Arrayy\Arrayy
+ * @extends \Arrayy\Arrayy>
*/
class NativeIntersectionData extends \Arrayy\Arrayy
{
diff --git a/tests/NativeUserData.php b/tests/NativeUserData.php
index 044047a..af30579 100644
--- a/tests/NativeUserData.php
+++ b/tests/NativeUserData.php
@@ -3,7 +3,7 @@
namespace Arrayy\tests;
/**
- * @extends \Arrayy\Arrayy
+ * @extends \Arrayy\Arrayy>
*/
class NativeUserData extends \Arrayy\Arrayy
{
diff --git a/tests/PHPStan/ArrayShapeAccessTest.php b/tests/PHPStan/ArrayShapeAccessTest.php
new file mode 100644
index 0000000..66e781a
--- /dev/null
+++ b/tests/PHPStan/ArrayShapeAccessTest.php
@@ -0,0 +1,40 @@
+ 1,
+ 'firstName' => 'Lars',
+ 'lastName' => 'Moelleken',
+ 'city' => new ArrayShapeCity([
+ 'name' => 'Düsseldorf',
+ 'plz' => null,
+ ]),
+ ]);
+
+ \PHPStan\Testing\assertType('int|null', $user['id']);
+ \PHPStan\Testing\assertType('string|null', $user['firstName']);
+ \PHPStan\Testing\assertType('string|null', $user['lastName']);
+ \PHPStan\Testing\assertType('Arrayy\tests\PHPStan\ArrayShapeCity|null', $user['city']);
+
+ if ($user['city'] !== null) {
+ \PHPStan\Testing\assertType('Arrayy\tests\PHPStan\ArrayShapeCity', $user['city']);
+ \PHPStan\Testing\assertType('string|null', $user['city']['name']);
+ \PHPStan\Testing\assertType('string|null', $user['city']['plz']);
+ }
+
+ self::assertSame('Moelleken', $user['lastName']);
+ self::assertInstanceOf(ArrayShapeCity::class, $user['city']);
+ }
+}
diff --git a/tests/PHPStan/ArrayShapeCity.php b/tests/PHPStan/ArrayShapeCity.php
new file mode 100644
index 0000000..ca2097e
--- /dev/null
+++ b/tests/PHPStan/ArrayShapeCity.php
@@ -0,0 +1,16 @@
+, value-of, T>
+ */
+final class ArrayShapeCity extends \Arrayy\Arrayy
+{
+ protected $checkPropertyTypes = true;
+
+ protected $checkPropertiesMismatchInConstructor = true;
+}
diff --git a/tests/PHPStan/ArrayShapeUser.php b/tests/PHPStan/ArrayShapeUser.php
new file mode 100644
index 0000000..762e393
--- /dev/null
+++ b/tests/PHPStan/ArrayShapeUser.php
@@ -0,0 +1,16 @@
+, value-of, T>
+ */
+final class ArrayShapeUser extends \Arrayy\Arrayy
+{
+ protected $checkPropertyTypes = true;
+
+ protected $checkPropertiesMismatchInConstructor = true;
+}
diff --git a/tests/TypeCheckCoreCoverageTest.php b/tests/TypeCheckCoreCoverageTest.php
index 6e1693b..4f0671d 100644
--- a/tests/TypeCheckCoreCoverageTest.php
+++ b/tests/TypeCheckCoreCoverageTest.php
@@ -763,7 +763,7 @@ final class TypeCheckDocOverridesNativeFixture
/**
* @template T of array{id: int, firstName: int|string, lastName: string, city?: \Arrayy\tests\CityData|null, infos: string[]}
- * @extends \Arrayy\Arrayy, value-of>
+ * @extends \Arrayy\Arrayy, value-of, T>
*/
final class TypeCheckArrayShapeUserData extends \Arrayy\Arrayy
{
@@ -777,7 +777,7 @@ final class TypeCheckArrayShapeUserData extends \Arrayy\Arrayy
/**
* @property int $legacyId
* @template T of array{id: int}
- * @extends \Arrayy\Arrayy, value-of>
+ * @extends \Arrayy\Arrayy, value-of, T>
*/
final class TypeCheckMixedPropertyAnnotationsData extends \Arrayy\Arrayy
{
@@ -786,7 +786,7 @@ final class TypeCheckMixedPropertyAnnotationsData extends \Arrayy\Arrayy
/**
* @property int $legacyId
- * @extends \Arrayy\Arrayy
+ * @extends \Arrayy\Arrayy>
*/
abstract class TypeCheckPropertyTagParentData extends \Arrayy\Arrayy
{
@@ -805,7 +805,7 @@ final class TypeCheckMixedPropertyAnnotationsInheritanceData extends TypeCheckPr
* may be absent; null must still be rejected when the key is present.
*
* @template T of array{score?: int}
- * @extends \Arrayy\Arrayy, value-of>
+ * @extends \Arrayy\Arrayy