filament icon indicating copy to clipboard operation
filament copied to clipboard

Eager-loaded relations throw "Attempted to lazy load" error when using Action on View page

Open devsumitparkash opened this issue 7 months ago • 1 comments

https://github.com/user-attachments/assets/83a89c4b-6243-4fec-9e28-1777cd74b235

Package

filament/filament

Package Version

v3.3.14

Laravel Version

v12.13.0

Livewire Version

No response

PHP Version

PHP 8.4

Problem description

I'm working on an OrderResource where the Order model has two hasMany relations: items and totals. Lazy loading is disabled via Model::preventLazyLoading() in my application.

The Order model has a currency_code column. We need this column value when formatting values on the Items and totals models.

In the View page of the resource, I correctly eager-load both relationships using load() inside the mount() method like this:

public function mount(int|string $record): void
{
    parent::mount($record);

    $this->record->load([
        'items' => fn ($query) => $query->chaperone(),
        'totals' => fn ($query) => $query->chaperone(),
    ]);
}

This works fine when the page loads.

However, when I add a custom button to update the order status (a Filament\Actions\Action), clicking on it results in this error:

Attempted to lazy load [order] on model [App\Models\Business\Order\OrderTotal].

https://drive.google.com/file/d/1WXs_oG1vStIEx5eWZwTDrBv8gcdVL1Ez/view

Expected behavior

The eager-loaded relations should persist, or actions should not try to access relations that weren’t explicitly loaded or provide a way to load the missing relationship.

Steps to reproduce

  1. composer install
  2. npm install
  3. npm run build
  4. php artisan migrate:fresh --seed
  5. login user: [email protected], password: password
  6. Go to the Orders resource.
  7. Click View on any order.
  8. Click the Update Status button.
  9. The issue will appear.

Reproduction repository (issue will be closed if this is not valid)

https://github.com/devsumitparkash/filmaent-lazy-load-disabled-issue

Relevant log output


devsumitparkash avatar May 13 '25 09:05 devsumitparkash

Same thing happening here which makes it difficult to create complex logic without resorting to lazy loading.

I did not eager-load the relationships in the View but I did it in the getEloquentQuery method Resource.php:

    public static function getEloquentQuery(): \Illuminate\Database\Eloquent\Builder
    {
        return parent::getEloquentQuery()
            ->withoutGlobalScopes()
            ->with('a')
            ->with('b');
    }

CRIO-frans avatar Jun 04 '25 19:06 CRIO-frans

Hi @devsumitparkash

This works:

Actions\Action::make('update-order-status')
    ->mountUsing(function (Action $action) {
        $this->record->load([
            'items' => fn ($query) => $query->chaperone(),
            'totals' => fn ($query) => $query->chaperone(),
        ]);
    })

Your OrderTotal model has computed attributes that depend on the parent Order model:

protected function valueAsMoneyObject(): Attribute
{
    $column = 'value';

    return Attribute::make(
        get: function (mixed $value, array $attributes) use ($column): Money {
            return $this->monetaryAsMoneyObject($column, $this->order->currency_code);
        },
    );
}

Which triggers the lazy loading error when the modal opens because Livewire doesn't persist eager-loaded relationships between requests. The page's mount() method runs on page load, but clicking the action is a new request. Using mountUsing() on the action makes sure relationships are loaded each time the action is mounted.

@danharrin This can be closed, unless you think Filament should handle this automatically somehow? See: https://github.com/devsumitparkash/filmaent-lazy-load-disabled-issue/blob/3b8ab0d360295f02f152004a9f31686b051ac5fb/app/Filament/Resources/OrderResource/Pages/ViewOrder.php#L36

binaryfire avatar Jun 30 '25 09:06 binaryfire

You're right @binaryfire, mount() is only executed on the first Livewire request too, which is not the same request as when the modal is opened, which is why the eager loading is not persisted

danharrin avatar Jun 30 '25 09:06 danharrin

Hi @devsumitparkash

This works:

Actions\Action::make('update-order-status') ->mountUsing(function (Action $action) { $this->record->load([ 'items' => fn ($query) => $query->chaperone(), 'totals' => fn ($query) => $query->chaperone(), ]); }) Your OrderTotal model has computed attributes that depend on the parent Order model:

protected function valueAsMoneyObject(): Attribute { $column = 'value';

return Attribute::make(
    get: function (mixed $value, array $attributes) use ($column): Money {
        return $this->monetaryAsMoneyObject($column, $this->order->currency_code);
    },
);

} Which triggers the lazy loading error when the modal opens because Livewire doesn't persist eager-loaded relationships between requests. The page's mount() method runs on page load, but clicking the action is a new request. Using mountUsing() on the action makes sure relationships are loaded each time the action is mounted.

@danharrin This can be closed, unless you think Filament should handle this automatically somehow? See: https://github.com/devsumitparkash/filmaent-lazy-load-disabled-issue/blob/3b8ab0d360295f02f152004a9f31686b051ac5fb/app/Filament/Resources/OrderResource/Pages/ViewOrder.php#L36

Thanks for the suggestion — adding the ->mountUsing(...) method does help with eager loading and avoids the lazy loading error when using chaperone(). However, I’m noticing a new issue where the default value of a Select field isn’t getting selected anymore.

Here’s the field definition:

Select::make('status')
    ->options(
        collect(OrderStatus::cases())->mapWithKeys(fn ($case) => [
            $case->value => $case->getLabel(),
        ])->toArray(),
    )
    ->default(fn (Order $record) => $record->status->value)
    ->required(),

When I use ->mountUsing(...) and manually load the relationships, the default is not selected. But if I remove mountUsing() and instead just apply ->chaperone() on the relationship like:

public function items(): HasMany
{
    return $this->hasMany(OrderItem::class)->chaperone();
}

...then the default value works as expected.

Also worth noting: if I remove the ->successRedirectUrl(...) and just show a success notification, I get the following error:

Attempted to lazy load [order] on model [App\Models\Business\Order\OrderItem] but lazy loading is disabled.

Any ideas, please?

Also, make sure to pull the latest changes (git pull) to get the updated implementation.

devsumitparkash avatar Jun 30 '25 12:06 devsumitparkash