laravel / framework

The Laravel Framework.
https://laravel.com
MIT License
32.2k stars 10.89k forks source link

Accessor Caching results in multiple setter calls #48616

Closed ktanakaj closed 11 months ago

ktanakaj commented 11 months ago

Laravel Version

10.25.1

PHP Version

8.2.8

Database Driver & Version

No response

Description

I am using the set accessor for bcrypt() to handle tokens, but I have noticed that the setter is being called multiple times, rather than just once. This issue occurs when I set an object instance instead of a primitive value. Surprisingly, in my case, the token setter even gets called when I use $id = $this->id;!

I suspect that the setter is being triggered by HasAttributes::mergeAttributesFromAttributeCasts() when it's called from getAttributes().

In my batch processing scenario, the excessive setter calls are consuming a significant portion of the execution time. I am concerned that getAttributes() should not invoke the setter.

Steps To Reproduce

This unit test reproduces the issue.

<?php

namespace Tests\Unit\Models;

use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
use Tests\TestCase;

class TestModelTest extends TestCase
{
    public function test(): void
    {
        $m = new TestModel();
        $m->id = 1;
        $this->assertSame(0, $m->calledCounter);

        $m->token = (string) Str::uuid();
        $this->assertSame(1, $m->calledCounter);

        $dummy = $m->id;
        $this->assertSame(1, $m->calledCounter);

        $m->token = Str::uuid();
        $this->assertSame(2, $m->calledCounter);

        $dummy = $m->id;
        $this->assertSame(2, $m->calledCounter); // NG: calledCounter = 3
    }
}

class TestModel extends Model
{
    public $calledCounter = 0;

    protected function token(): Attribute
    {
        return Attribute::make(
            set: function ($value) {
                $this->calledCounter++;
                return bcrypt($value);
            }
        );
    }
}
crynobone commented 11 months ago

This seems to be an expected behavior, since in some use cases token might depend on id attribute value and we wouldn't be able to determine that unless Attribute::$set is explicitly called.

crynobone commented 11 months ago

Hey there,

While this may be a legitimate issue, can you first try posting your problem or question on one of the support channels below? If this issue can be definitively identified as a bug, feel free to open up a new issue with a link to the original one and we'll gladly help you out.

Thanks!