Skip to content

Register $appends entries as magic properties when no explicit accessor exists #923

@alies-dev

Description

@alies-dev

Problem

ModelPropertyAccessorHandler registers magic properties for Eloquent attribute access only when a get{Name}Attribute() (legacy) or new-style Attribute cast method exists. It does not read protected $appends = [...].

When a model declares an attribute in $appends but resolves it through a dynamic mechanism (overridden __get/getAttribute, a trait, or magic value resolution), there is no accessor method to detect, so the plugin falls through and Psalm emits UndefinedMagicPropertyFetch.

Reproducer

corcel/corcel v9.0 (Corcel\Model\Attachment) declares:

class Attachment extends Post
{
    use Aliases;

    protected $appends = ['title', 'url', 'type', 'description', 'caption', 'alt'];

    protected static $aliases = [
        'title' => 'post_title',
        'url'   => 'guid',
        // ...
    ];
}

The Aliases trait overrides getAttribute() to look entries up in $aliases. No getUrlAttribute() method exists.

Corcel\Model\Meta\ThumbnailMeta::size():

return $this->attachment->url;

emits:

UndefinedMagicPropertyFetch: Magic instance property Corcel\Model\Attachment::$url is not defined

Real-app benchmark (psalm-app-benchmark against corcel @ v3.10.2) produces 4 such gaps on Attachment ($url ×3, $guid ×1 — $guid is the raw target column, separate concern).

Proposed fix

In ModelPropertyAccessorHandler::doesPropertyExist / isPropertyVisible / getPropertyType, after the existing accessor checks fail:

  1. Read the default value of protected $appends on the model's class storage.
  2. If $propertyName is in that list, return true for existence/visibility.
  3. For type: defer to accessor handlers (already runs earlier); if no accessor matches, fall back to mixed.

Sketch:

if (self::isInAppends($codebase, $fqcn, $propertyName)) {
    return true;
}

isInAppends reads $classStorage->properties['appends'] default value via the AST or suggested_type.

Scope notes

  • Generic: ships for every Laravel app declaring $appends without @property PHPDoc, not just corcel.
  • Independent of Validate $appends entries have corresponding accessors #694 (validate $appends entries have accessors). Validate $appends entries have corresponding accessors #694 is a write-side lint; this is a read-side resolver. They share the $appends parser.
  • Does not address the related class of issues where the column itself (e.g. Attachment::$guid, Term::$name) is a raw DB column on a schema-less model (corcel reads WordPress tables, no migrations exist). That needs a separate "permissive fallback for schema-less Eloquent models" issue.

Acceptance

  • New type test in tests/Type/tests/ covering a model with $appends + no accessor → property resolves as mixed, no UndefinedMagicPropertyFetch.
  • Existing test where $appends entry has an accessor still uses the accessor's return type (current behavior unchanged).
  • Real-app benchmark on corcel: UndefinedMagicPropertyFetch count drops by the appends-resolved entries.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions