Skip to content

protected $previous = [] in base Model breaks Eloquent relationships named previous when accessed from within the Model #55828

Closed
@MannikJ

Description

@MannikJ

Laravel Version

12.15.0

PHP Version

8.4.7

Database Driver & Version

MariaDB 11.7.2

Description

Version affected: Laravel v12.15.0
Introduced by: #55729

After upgrading from Laravel 12.14.1 to 12.15.0, existing applications using an Eloquent relationship named previous are broken due to a new class property:

protected $previous = [];

This property was introduced in the Model class to "preserve" a relationship internally, but it shadows any existing Eloquent relationship using the name previous.


Impact

In PHP, class properties take precedence over __get() dynamic access, so:

$this->previous

now always returns the protected $previous array, not the related model.

This causes existing apps with a relationship like:

public function previous()
{
    return $this->belongsTo(Task::class, 'previous_id');
}

to suddenly break, returning [] instead of a Task instance.


Why This is a Breaking Change

  • Laravel's own conventions encourage expressive, semantic relation names like previous, next, parent, etc.
  • This change silently breaks existing public API contracts with no warning or deprecation notice.
  • It introduces an invisible namespace collision between class properties and relations.
  • It violates the principle of least surprise in a way that’s hard to debug (e.g., returns an empty array, not null or exception).

Suggested Fix

At the very least there has to be warning about this problem. In my opinion it should never have been released as a minor update. I would vote for to actually revert and postpone this to a major version.

This incident highlights a larger structural limitation in Laravel's current Eloquent model system: attributes and class properties share the same namespace, and magic access provides no guardrails against collisions.

A longer-term fix could involve refactoring the HasAttributes trait to encapsulate model attributes in a dedicated container, clearly separating user-defined class properties from Eloquent-managed state. This would eliminate accidental shadowing and make models safer and more predictable at scale.


References

Please consider reverting this change or implementing a safer alternative.


Steps To Reproduce

  1. Create a model with a previous() relationship:

    class Task extends Model
    {
        public function previous()
        {
            return $this->belongsTo(Task::class, 'previous_id');
        }
    }
    
  2. Load and access the relation:

    $task = Task::with('previous')->find(1);
    dd($task->previous); // returns []
    
  3. If you call the relation manually, it works:

    dd($task->getRelation('previous')); // correct Task model
    
  4. Downgrade to Laravel 12.14.1 → works again as expected.

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