php - How does Laravel find and display the dynamic attributes on Eloquent models when using the tinker CLI?

When we use artisan tinker, and reference an Eloquent model object, the REPL automatically prints the model's attributes, much like how it prints the public properties of any standard object we reference:

>>> (object) ['hello' => 'world']
 => {
      +"hello": "world",
    }

>>> App\User::first()
 => App\User {
      id: 1,
      name: "...",
   }

What's less obvious to me is how these virtual attributes can be made to appear here, as if they were already defined as public properties of the class. I understand that much of the attribute assignment for the model is handled internally by the HasAttributes trait, but even looking there, I still don't see how the Laravel authors were able to achieve this behavior.

I've tried building a class like this:

class Bunch implements Arrayable, ArrayAccess, Jsonable, JsonSerializable { ... }

but even with working array access and a toArray method, when I reference it directly from artisan tinker:

>>> $b = new Bunch()
 => Bunch {}
>>> $b->one = 1
 => 1
>>> $b['one']
 => 1
>>> $b
 => Bunch {}

How can we change the representation that the REPL uses when it prints an object like this?

Answer

Solution:

The code that determines how to display the attributes of a model in Tinker is in the Tinker code, not in the model:

Casters are part of the Symfony VarDumper component which is used by PsySH to display classes. Tinker is built on top of PsySH.

Tinker maps the casters to the classes in its Console command class:

This returns the mapping of the classes to their casters:

/**
     * Get an array of Laravel tailored casters.
     *
     * @return array
     */
    protected function getCasters()
    {
        $casters = [
            'Illuminate\Support\Collection' => 'Laravel\Tinker\TinkerCaster::castCollection',
            'Illuminate\Support\HtmlString' => 'Laravel\Tinker\TinkerCaster::castHtmlString',
            'Illuminate\Support\Stringable' => 'Laravel\Tinker\TinkerCaster::castStringable',
        ];

        if (class_exists('Illuminate\Database\Eloquent\Model')) {
            $casters['Illuminate\Database\Eloquent\Model'] = 'Laravel\Tinker\TinkerCaster::castModel';
        }

        if (class_exists('Illuminate\Foundation\Application')) {
            $casters['Illuminate\Foundation\Application'] = 'Laravel\Tinker\TinkerCaster::castApplication';
        }

        return $casters;
    }

This sets the casters on the VarDumper:

        $config->getPresenter()->addCasters(
            $this->getCasters()
        );

If you want to add additional properties from the model to be displayed in Tinker, you can use the $appends property on the model:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * The accessors to append to the model's array form.
     *
     * @var array
     */
    protected $appends = ['is_admin'];
}

Answer

Solution:

Eloquent automatically fills Model attributes using the Database connection set up.

Source