phplaraveleloquentlaravel-filamentfilamentphp

Why don’t the new class-based Laravel Eloquent accessors apply with `->pluck('name')` as they did with the old magic methods?


I’m working on a Laravel (and FilamentPHP) project and noticed a discrepancy between the old “magic” accessor method and the new class-based Attribute accessor introduced in Laravel 8.40+. Specifically, when I use:

Model::query()->pluck('name', 'id')

Here’s a simplified example of my model:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use App\Enums\SomeEnumStatusState;

class MyModel extends Model
{
    // OLD Magic Accessor (works with pluck)
    // public function getNameAttribute($value)
    // {
    //     return ucwords(strtolower($value));
    // }

    // NEW Class-based Accessor (not applying with pluck)
    protected function name(): Attribute
    {
        return Attribute::make(
            get: fn ($value) => ucwords(strtolower($value))
        );
    }

    public function scopeOpen(Builder $query): Builder
    {
        return $query->where('state', SomeEnumStatusState::OPEN);
    }

    protected function casts(): array
    {
        return [
            'state' => SomeEnumStatusState::class,
        ];
    }
}

And in my FilamentPHP Resource:

Forms\Components\Select::make('model_id')
    ->label('Status')
    ->relationship('modelRelationship', 'name')
    ->options(MyModel::query()->pluck('name', 'id')),

My questions are:

  1. Why does Model::query()->pluck('name', 'id') return the raw database values when using the new class-based name(): Attribute but returns transformed values under the old getNameAttribute() method?

  2. Is this the intended behavior, or am I missing something about how pluck() interacts with the newer Attribute accessors?

  3. What is the recommended or “best practice” way to retrieve transformed attribute values (especially with FilamentPHP) under the new accessor approach?

Any insights, explanations, or code examples showing how to ensure pluck() respects the class-based accessor would be greatly appreciated!


Solution

  • Looking into the vendor files for this, the pluck method respects mutators as follows:

            // If the model has a mutator for the requested column, we will spin through
            // the results and mutate the values so that the mutated version of these
            // columns are returned as you would expect from these Eloquent models.
            if (! $this->model->hasGetMutator($column) &&
                ! $this->model->hasCast($column) &&
                ! in_array($column, $this->model->getDates())) {
                return $results;
            }
    

    Taking a dive deeper into the hasGetMutator method we can see that this will only check for method names using the previous magic methods.

        public function hasGetMutator($key)
        {
            return method_exists($this, 'get'.Str::studly($key).'Attribute');
        }
    

    One way i've found to get around this would be to specify the attribute on the $appends array (relevant docs) on the model and then ->get() before ->pluck(...) like so:

    Model::query()->get()->pluck('name', 'id')