Skip to content

Report array_column() reading a property absent from the array's object elements#5887

Open
phpstan-bot wants to merge 7 commits into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-vt4ilzi
Open

Report array_column() reading a property absent from the array's object elements#5887
phpstan-bot wants to merge 7 commits into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-vt4ilzi

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

Summary

PHP's array_column() can read a property off an array of objects
(array_column($objects, 'propertyName')). The reporter expected PHPStan to flag
calls that use a property name the objects don't have, since such a call silently
yields an empty array and is a refactoring footgun. PHPStan already infers the
correct return type for array_column() on object arrays, but it never reported
the obviously-wrong property name.

This adds a rule that reports array_column() calls whose $column_key /
$index_key names a property absent from the objects in the source array.

Changes

  • src/Rules/Functions/ArrayColumnRule.php (new): a Rule<FuncCall> that,
    for array_column() calls, takes the iterable value type of the source array
    and — when the elements are objects — checks that the constant-string column
    and index arguments name an existing property. Reports
    arrayColumn.property errors otherwise. Honors treatPhpDocTypesAsCertain
    and emits the corresponding tip.
  • conf/bleedingEdge.neon, conf/config.neon, conf/parametersSchema.neon:
    add the arrayColumnObjectArrays feature toggle (on under bleeding edge,
    off by default).
  • conf/config.level5.neon: register ArrayColumnRule (level 5, conditional
    on the feature toggle), alongside the existing array-function rules.
  • tests/PHPStan/Rules/Functions/ArrayColumnRuleTest.php and
    .../data/array-column.php (new): the rule test and fixture.

Root cause

This is not a regression but a missing check. The return-type extension
(ArrayColumnHelper) already resolves object properties for array_column(),
so a bad property name just collapses the result to array{}/list with no
diagnostic. The new rule fills that gap, mirroring how AccessPropertiesRule
reports Access to an undefined property for direct property fetches.

Key behavioral notes baked into the rule (each verified against real PHP
array_column() semantics):

  • array_column() reads object properties, never ArrayAccess offsets, even
    when the element implements ArrayAccess. The check therefore keys off
    Type::isObject() rather than offset accessibility (a non-final class reports
    isOffsetAccessible() as maybe because a subclass could implement
    ArrayAccess, which would otherwise suppress the report the reporter wanted).
  • Visibility is scope-dependent at runtime, and dynamic properties exist on
    PHP < 8.2, so classes that allow dynamic properties or expose __isset + __get
    are skipped. Enums are skipped because name/value always resolve.

Parallel cases considered

  • #2 $column_key#3 $index_key: both arguments are checked with the
    same logic (fixed together).
  • Element kinds: object (reported), array element / offset access (skipped —
    left to other rules), scalar (skipped), enum (skipped), ArrayAccess object
    (reported via property, matching runtime), union of object+array (skipped, not
    definitely an object).
  • Class kinds: final, non-final, abstract and interface element types are all
    reported — consistent with AccessPropertiesRule.
  • Magic / dynamic: __isset + __get classes and dynamic-property classes
    are skipped to avoid false positives.
  • Visibility (existing-but-private property): deliberately not reported.
    array_column() reads private properties only from inside the declaring class
    scope, so a correct check would need scope-aware visibility; this is left as a
    follow-up to keep the rule focused on undefined properties (the reported issue).

Test

ArrayColumnRuleTest runs the rule over a fixture that includes the original
issue reproducer ([new a(), new a()] with a wrong property name) plus
non-final/final classes, the $index_key argument, and the
magic/dynamic/enum/array/union element types that must not be flagged. The
whole suite (make tests) and self-analysis (make phpstan, which runs with
bleeding edge) pass.

Fixes phpstan/phpstan#5101

…ject elements

- Add `PHPStan\Rules\Functions\ArrayColumnRule` that checks `array_column()`
  calls whose source array holds objects: when the `$column_key` (and
  `$index_key`) argument is a constant string naming a property that none of the
  element classes declare, it reports the call instead of silently inferring an
  empty array.
- Cover both parallel arguments: `phpstan#2 $column_key` and `phpstan#3 $index_key` are checked
  with the same logic.
- Stay conservative to avoid false positives: only objects are checked (array
  elements use offset access, scalars are skipped), enums are skipped (their
  `name`/`value` are always present), and classes with `__isset` + `__get` magic
  or that still allow dynamic properties (e.g. on PHP < 8.2) are skipped because
  the property might exist at runtime. This matches `AccessPropertiesRule`, which
  likewise reports undefined properties on non-final/abstract/interface types.
- `array_column()` reads object properties even on `ArrayAccess` elements (never
  the offset), so the check keys off `Type::isObject()` rather than offset
  accessibility.
- Gate the rule behind the new `arrayColumnObjectArrays` bleeding-edge feature
  toggle and register it on level 5 next to the other array-function rules.
- Add `ArrayColumnRuleTest` plus a fixture covering non-final/final classes,
  the index argument, magic/dynamic/enum/array/union element types and the
  original issue reproducer.

@staabm staabm left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

the rule should re-use ArrayColumnHelper

Move the object-property-missing detection used by ArrayColumnRule into
ArrayColumnHelper, the single home for array_column() semantics, and have
the rule delegate to it instead of duplicating the property resolution.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@phpstan-bot

Copy link
Copy Markdown
Collaborator Author

Done. I addressed the reviewer's request to reuse ArrayColumnHelper.

What I changed

The tension: The rule reports missing properties on concrete and non-final element classes (the whole point of issue #5101, whose reproducer is [new a(), new a()]). The helper's existing type-inference path (getOffsetOrProperty) deliberately treats non-final/concrete objects as Maybe — because a subclass could declare the property — so it returns list<mixed> rather than a definite "empty". That means the rule's reporting decision genuinely cannot be delegated to getOffsetOrProperty without losing the core feature.

The resolution: I moved the rule's property-missing detection into ArrayColumnHelper — the natural single home for array_column() semantics — as a new public findMissingObjectProperties() method (with a private isObjectPropertyMissing() helper). The rule now delegates to it instead of carrying its own isPropertyMissing(). A doc comment on the new method explains why it resolves at the ClassReflection level (matching AccessPropertiesRule) rather than the type-inference level, so the two coexisting code paths in the helper are clearly distinguished.

Mechanics:

  • src/Type/Php/ArrayColumnHelper.php: added findMissingObjectProperties() + isObjectPropertyMissing().
  • src/Rules/Functions/ArrayColumnRule.php: injected ArrayColumnHelper, replaced the inlined logic with delegation. Behavior (including the treatPhpDocTypesAsCertain tip) is unchanged.
  • tests/.../ArrayColumnRuleTest.php: fetch ArrayColumnHelper from the container.

Verification: the rule test, the array_column inference tests, the full suite (make tests), and self-analysis (make phpstan, bleeding edge) all pass. Committed and pushed.

One note for the reviewer: I kept the rule's existing behavior intact (reports non-final/concrete, skips enums/dynamic/magic/private) rather than switching it to the helper's Maybe-based inference semantics, since the latter would stop reporting the issue's own reproducer. If you'd actually prefer the rule to share the getOffsetOrProperty "definite empty" signal instead, that's a deliberate behavior change I can make — let me know.

@staabm staabm left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this PR should also cover phpstan/phpstan#12188

Comment thread src/Rules/Functions/ArrayColumnRule.php
Comment thread tests/PHPStan/Rules/Functions/data/array-column.php

@staabm staabm left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

add a test covering phpstan/phpstan#9671

phpstan-bot and others added 4 commits June 17, 2026 05:22
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A pure enum exposes only `name`; a backed enum also exposes `value`.
Any other property name read via array_column() yields an empty result,
so report it - mirroring the object-property check.

Closes phpstan/phpstan#12188

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Covers array<NonFinalObject|FinalObject>: a property present on either
member is not reported, only one absent from both members is.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… arrays

Adds the issue #9671 reproducer: array_column() on a list of internal-class
objects (DateTimeImmutable) reading a property the class lacks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@phpstan-bot

Copy link
Copy Markdown
Collaborator Author

Done. Here's a summary of what I addressed.

What the current review asked for

add a test covering phpstan/phpstan#9671

Issue #9671's reproducer is array_column($dates, "Price") where $dates is list<\DateTimeImmutable> — an array of objects read with a property name the class doesn't have. The rule already reports this correctly (verified by reproducing it with bin/phpstan), so the gap was test coverage.

Changes

  • tests/.../data/array-column.php: added the #9671 reproducer near-verbatim (a HelloWorld::bug9671() method taking list<\DateTimeImmutable> and calling array_column($dates, 'Price')).
  • tests/.../ArrayColumnRuleTest.php: added the expected arrayColumn.property error for DateTimeImmutable at line 130 (with the PHPDoc tip).

Reconciliation note

While I worked, another commit (891a66abc, "Add ArrayColumnRule test for union element type") landed on the branch, covering the array<NonFinalObject|FinalObject> union case — which I had also independently added. I dropped my duplicate union test, reset onto that commit, and kept only the new #9671 coverage so there's no redundancy.

Verification

  • ArrayColumnRuleTest passes (1 test, 1 assertion).
  • make phpstan passes (run before the rebase, on a superset of this change; the final change is purely test-fixture additions with no source changes).

The other earlier review points were already handled in prior commits on the branch:

  • "check args count earlier"e65531c0a
  • enum coverage (#12188)ddc142529 (pure enums now report a missing value)
  • union test891a66abc

Commit pushed: c15084f93.

Comment thread src/Rules/Functions/ArrayColumnRule.php
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

array_column on object arrays

2 participants