Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

### Upcoming release

- add PHPStan inference for literal `Arrayy::get()` dot-notation paths on typed subclasses while keeping custom path separators sound
- improve callable generic inference and preserve transformed value types across `each()` and `map()`
- fix nested dot-notation removal so removing a deep key preserves the root array and sibling values
- preserve `Traversable` entries when mapping JSON data and harden array/object path traversal around scalar intermediates
- fix `average()` so non-numeric values no longer error on modern PHP versions
- make `changeKeyCase()` Unicode case conversion deterministic across PHP 8.0–8.5
- strengthen native property type checks, array-shape contracts, and regression coverage across Json mapper and collection helpers
Expand Down
20 changes: 17 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,12 +120,26 @@ $arrayy->Lars->lastname; // 'Müller'

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. `meta()` is also understood by PHPStan, so `meta()`-derived keys such as `$userMeta->city` and `$cityMeta->name` keep precise literal-string information during static analysis. When you want PHPStan to check reads precisely, prefer array-like access with literal keys (for example `$user['lastName']`) or narrowed `meta()` keys on these array-shape-based models. Do not combine array-shape annotations and `@property` tags on the same model.

If you use PHPStan and call `YourArrayySubclass::meta()`, you can register the custom return-type extension from `src/PHPStan/MetaDynamicStaticMethodReturnTypeExtension.php`. Use it when you want PHPStan to understand that `meta()` returns an object shape whose properties are the exact array keys collected from your array-shape annotations, `@property` tags, or native declared properties. That keeps expressions such as `$userMeta->id` typed as the literal string `'id'`, helps nested lookups like `$user[$userMeta->city][$cityMeta->name]`, and lets PHPStan report invalid meta-property access such as `$userMeta->ghost`.
If you use PHPStan, register the return-type extensions from `src/PHPStan`. `GetDynamicMethodReturnTypeExtension` resolves literal dot-notation paths against a typed subclass's `TData` array shape, including fallback values. `MetaDynamicStaticMethodReturnTypeExtension` understands that `meta()` returns an object shape whose properties are the exact keys collected from array-shape annotations, `@property` tags, or native declared properties.

All three access styles can therefore participate in static analysis:

```php
$name = $user->get('profile.name', 'Guest'); // dot path resolved from TData
$name = $user['profile']['name']; // ArrayAccess / array-shape inference
$name = $user->profile->name; // @property-read declarations
```

Dot-notation inference intentionally applies to literal dotted paths on typed `Arrayy` subclasses with a stable array-shape `TData` that implement `Arrayy\PHPStan\DefaultDotNotationTypeInterface`. Implementing this marker is a promise that the subclass keeps Arrayy's default `.` separator and does not switch it with `changeSeparator()`; without that promise, splitting the path statically would be unsound. Dynamic strings, wildcard paths, custom separators, and plain `Arrayy` instances retain the method's safe general return type. Object-property access should be declared with `@property` or `@property-read`; `meta()` keeps generated key access precise.
Comment on lines +125 to +133

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

rg -n -C4 '`@property`(-read)?|class (User|City)' README.md build/docs/base.md

Repository: voku/Arrayy

Length of output: 14366


🌐 Web query:

PHPStan undefined property @property-read example

💡 Result:

The @property-read PHPDoc tag is used in PHPStan to declare "magic" properties that are available for reading but not writing, typically when a class implements the get magic method [1][2][3]. If you are encountering an "Access to an undefined property" error despite using @property-read, it is usually due to one of the following reasons: 1. Improper Placement: The @property-read tag must be placed in the PHPDoc block immediately preceding the class or trait definition [2][3]. It cannot be used to declare "abstract properties" on interfaces or classes in a way that enforces implementation by subclasses [4][5]. 2. PHP 8.2+ Dynamic Property Restrictions: Since PHP 8.2, native support for dynamic properties has been restricted. When you type-hint an interface that uses @property-read, PHPStan may report an "Access to an undefined property" error because PHP interfaces do not natively support property declarations [6][4]. To resolve this, you can use the @phpstan-require-extends tag on the interface to reference a base class that supports dynamic properties [6][4]. 3. Missing Type Information: PHPStan requires a type definition for @property-read tags (e.g., / @property-read string $name */) [7]. Omitting the type will cause a parsing error [7]. Recommended Solutions: - Verify Placement: Ensure the tag is above the class/trait [1][2]. - Use @phpstan-require-extends: If using interfaces in PHP 8.2+, add @phpstan-require-extends [6][4]. - Use Proper Syntax: Ensure the tag follows the format @property-read [Type] [name] [1][2]. - Alternatives: For complex scenarios, consider using a custom class reflection extension or configuring universalObjectCratesClasses in your phpstan.neon file [6][8][9]. If the code is static, refactoring to use a standard getter method (e.g., getFieldName) is the most robust approach [5].

Citations:


🌐 Web query:

"Access to an undefined property" PHPStan array-shape without @property-read``

💡 Result:

The "Access to an undefined property" error when working with PHPStan object-shapes (often referred to as array-shapes in PHPStan notation) typically occurs when you attempt to access a property that is not explicitly defined in the type definition [1][2]. If you are encountering this error even for optional properties (e.g., object{prop?: string}), ensure you are using the latest version of PHPStan [3][4]. Recent updates (particularly in the 2.1.x development branch) have significantly improved how PHPStan handles optional properties in object shapes to reduce false positives [3][5][4]. If the error persists or is intentional, you have several ways to address it: 1. Use Null Coalescing Operator: If the property is optional, accessing it directly may trigger an error. Using the null coalescing operator (e.g., $object->prop ?? 'default') is the idiomatic way to handle potentially missing properties safely [6][4]. 2. Suppress the Error: If you are certain the property will exist at runtime, you can ignore the specific error using its identifier, property.notFound [2]. This can be done inline: // @phpstan-ignore property.notFound $value = $object->prop; Or globally in your phpstan.neon configuration: parameters: ignoreErrors: - identifier: property.notFound 3. Narrow the Type: Ensure that the variable is correctly typed as the object-shape before access [1]. If the variable is typed as a broader object or mixed, PHPStan cannot guarantee the property exists [1]. 4. Check Configuration: Ensure that strict dynamic property checking rules are not set too aggressively if your codebase relies on dynamic access, though this is less common for fixed object-shapes [6]. For most scenarios involving optional object-shape properties, upgrading your PHPStan version is the most effective solution, as the maintainers have addressed several issues where valid optional property accesses were incorrectly flagged as undefined [3][5][4].

Citations:


Declare the magic properties used by the PHPStan examples.

The examples in README.md and build/docs/base.md read $user->profile and $user->profile->name, but the shown User and City classes declare only @template T array shapes and no @property/@property-read tags. Add matching declarations to the demonstrated models, or state that property-style access is runtime-only in this context.

📍 Affects 2 files
  • README.md#L125-L133 (this comment)
  • build/docs/base.md#L124-L132
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 125 - 133, Add matching `@property` or `@property-read`
declarations to the demonstrated User and City model classes used in README.md
lines 125-133 and build/docs/base.md lines 124-132, covering profile and nested
name access shown by $user->profile->name. Keep the existing TData examples
unchanged and ensure both documentation sites consistently declare the magic
properties rather than implying unsupported static access.


The repository's own `phpstan.neon` registers the extension like this; copy the same service definition into your project's PHPStan config because this repository file is not shipped in the Composer package:

```neon
services:
-
class: Arrayy\PHPStan\GetDynamicMethodReturnTypeExtension
tags:
- phpstan.broker.dynamicMethodReturnTypeExtension
-
class: Arrayy\PHPStan\MetaDynamicStaticMethodReturnTypeExtension
tags:
Expand All @@ -137,7 +151,7 @@ services:
* @template T of array{id: int, firstName: int|string, lastName: string, city?: City|null}
* @extends \Arrayy\Arrayy<key-of<T>,value-of<T>,T>
*/
class User extends \Arrayy\Arrayy
class User extends \Arrayy\Arrayy implements \Arrayy\PHPStan\DefaultDotNotationTypeInterface
{
protected $checkPropertyTypes = true;

Expand All @@ -148,7 +162,7 @@ class User extends \Arrayy\Arrayy
* @template T of array{plz: string|null, name: string, infos: string[]}
* @extends \Arrayy\Arrayy<key-of<T>,value-of<T>,T>
*/
class City extends \Arrayy\Arrayy
class City extends \Arrayy\Arrayy implements \Arrayy\PHPStan\DefaultDotNotationTypeInterface
{
protected $checkPropertyTypes = true;

Expand Down
20 changes: 17 additions & 3 deletions build/docs/base.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,26 @@ $arrayy->Lars->lastname; // 'Müller'

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. `meta()` is also understood by PHPStan, so `meta()`-derived keys such as `$userMeta->city` and `$cityMeta->name` keep precise literal-string information during static analysis. When you want PHPStan to check reads precisely, prefer array-like access with literal keys (for example `$user['lastName']`) or narrowed `meta()` keys on these array-shape-based models. Do not combine array-shape annotations and `@property` tags on the same model.

If you use PHPStan and call `YourArrayySubclass::meta()`, you can register the custom return-type extension from `src/PHPStan/MetaDynamicStaticMethodReturnTypeExtension.php`. Use it when you want PHPStan to understand that `meta()` returns an object shape whose properties are the exact array keys collected from your array-shape annotations, `@property` tags, or native declared properties. That keeps expressions such as `$userMeta->id` typed as the literal string `'id'`, helps nested lookups like `$user[$userMeta->city][$cityMeta->name]`, and lets PHPStan report invalid meta-property access such as `$userMeta->ghost`.
If you use PHPStan, register the return-type extensions from `src/PHPStan`. `GetDynamicMethodReturnTypeExtension` resolves literal dot-notation paths against a typed subclass's `TData` array shape, including fallback values. `MetaDynamicStaticMethodReturnTypeExtension` understands that `meta()` returns an object shape whose properties are the exact keys collected from array-shape annotations, `@property` tags, or native declared properties.

All three access styles can therefore participate in static analysis:

```php
$name = $user->get('profile.name', 'Guest'); // dot path resolved from TData
$name = $user['profile']['name']; // ArrayAccess / array-shape inference
$name = $user->profile->name; // @property-read declarations
```

Dot-notation inference intentionally applies to literal dotted paths on typed `Arrayy` subclasses with a stable array-shape `TData` that implement `Arrayy\PHPStan\DefaultDotNotationTypeInterface`. Implementing this marker is a promise that the subclass keeps Arrayy's default `.` separator and does not switch it with `changeSeparator()`; without that promise, splitting the path statically would be unsound. Dynamic strings, wildcard paths, custom separators, and plain `Arrayy` instances retain the method's safe general return type. Object-property access should be declared with `@property` or `@property-read`; `meta()` keeps generated key access precise.

The repository's own `phpstan.neon` registers the extension like this; copy the same service definition into your project's PHPStan config because this repository file is not shipped in the Composer package:

```neon
services:
-
class: Arrayy\PHPStan\GetDynamicMethodReturnTypeExtension
tags:
- phpstan.broker.dynamicMethodReturnTypeExtension
-
class: Arrayy\PHPStan\MetaDynamicStaticMethodReturnTypeExtension
tags:
Expand All @@ -136,7 +150,7 @@ services:
* @template T of array{id: int, firstName: int|string, lastName: string, city?: City|null}
* @extends \Arrayy\Arrayy<key-of<T>,value-of<T>,T>
*/
class User extends \Arrayy\Arrayy
class User extends \Arrayy\Arrayy implements \Arrayy\PHPStan\DefaultDotNotationTypeInterface
{
protected $checkPropertyTypes = true;

Expand All @@ -147,7 +161,7 @@ class User extends \Arrayy\Arrayy
* @template T of array{plz: string|null, name: string, infos: string[]}
* @extends \Arrayy\Arrayy<key-of<T>,value-of<T>,T>
*/
class City extends \Arrayy\Arrayy
class City extends \Arrayy\Arrayy implements \Arrayy\PHPStan\DefaultDotNotationTypeInterface
{
protected $checkPropertyTypes = true;

Expand Down
8 changes: 8 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
parameters:
level: 8
reportUnmatchedIgnoredErrors: true
excludePaths:
analyse:
- %currentWorkingDirectory%/tests/PHPStan/ArrayShapeInvalidUsage.php
- %currentWorkingDirectory%/tests/PHPStan/MetaInvalidUsage.php
paths:
- %currentWorkingDirectory%/src/
- %currentWorkingDirectory%/tests/

services:
-
class: Arrayy\PHPStan\GetDynamicMethodReturnTypeExtension
tags:
- phpstan.broker.dynamicMethodReturnTypeExtension
-
class: Arrayy\PHPStan\MetaDynamicStaticMethodReturnTypeExtension
tags:
Expand Down
Loading
Loading