deondazy's avatar

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')
  • The old magic accessor (getNameAttribute()) applied ucwords() to the plucked values.
  • The new class-based accessor (protected function name(): Attribute) does not apply the transformation when plucking.

Here’s a simplified example of my model:

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!

0 likes
3 replies
LaryAI's avatar
Level 58

The behavior you're observing is due to how the pluck() method interacts with Eloquent models in Laravel. Here's a breakdown of your questions and the recommended approach:

  1. Why does Model::query()->pluck('name', 'id') return the raw database values with the new class-based name(): Attribute?

    The pluck() method directly queries the database and retrieves the specified columns without instantiating the model objects. This means it bypasses any accessors, including the new class-based accessors. The old magic accessors (getNameAttribute()) were applied because they were part of the model's attribute retrieval process, which pluck() does not trigger.

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

    Yes, this is the intended behavior. The pluck() method is designed to be a lightweight way to retrieve specific columns directly from the database, which is why it doesn't apply any model accessors or mutators.

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

    If you need to apply transformations defined in accessors, you should retrieve the models and then map over them to apply the accessor logic. Here's how you can do it:

    $models = MyModel::all()->mapWithKeys(function ($model) {
        return [$model->id => $model->name];
    });
    

    In the context of FilamentPHP, you can use this approach to ensure that the transformed values are used:

    Forms\Components\Select::make('model_id')
        ->label('Status')
        ->relationship('modelRelationship', 'name')
        ->options(MyModel::all()->mapWithKeys(function ($model) {
            return [$model->id => $model->name];
        }));
    

    This way, you ensure that the accessor logic is applied to the name attribute, as the models are fully instantiated and the accessors are invoked when accessing the attributes.

By using all() and mapWithKeys(), you ensure that the accessors are applied, and you still get a key-value pair suitable for options in a select component.

Merklin's avatar

If you are using ->relationship(), why setting options at all?

->relationship(User, name) will return <option value="id">name</option> by default.

It is only viable if you modify the name attribute by adding something to it. For example, if you have an attribute last_name and want to return in the select options both name and last_name. But even in this case, there is no need to use pluck(), as you can use:

Select::make('user_id')
                ->label(__('User'))
                ->relationship('user', 'name')
                ->getOptionLabelFromRecordUsing(fn (User $record) => $record->full_name)
                ->searchable(['users.name', 'users.last_name']),

where full_name is an attribute set in the User model and returns the desired combination of name + last_name.

Please or to participate in this conversation.