New Custom Casting For Eloquent Models Has Excessive set() calls #31778
Replies: 12 comments 19 replies
-
Going to leave this to Taylor to review |
Beta Was this translation helpful? Give feedback.
-
Yeah, probably worth exploring if we can get rid of some of those redundant calls for sure. |
Beta Was this translation helpful? Give feedback.
-
Even though nothing's broken, I'm gonna mark this as a bug so we can take a look at this later on. Appreciating prs in the meantime! |
Beta Was this translation helpful? Give feedback.
-
Suppose we set the attributes of Eloquent Model through the way specified in the test: $model->address = $address = new Address('110 Kingsbrook St.', 'My Childhood House');
$address->lineOne = '117 Spencer St.'; The What if we add a property like $classCastCache['address'] = unserialize(serialize($originalClassCastCache['address'])); And the protected function mergeAttributesFromClassCasts()
{
foreach ($this->classCastCache as $key => $value) {
if ($this->classCastCache[$key] == $this->originalClassCastCache[$key] ) {
continue;
}
$caster = $this->resolveCasterClass($key);
$this->attributes = array_merge(
$this->attributes,
$caster instanceof CastsInboundAttributes
? [$key => $value]
: $this->normalizeCastClassResponse($key, $caster->set($this, $key, $value, $this->attributes))
);
}
} |
Beta Was this translation helpful? Give feedback.
-
@ralphschindler I made a fairly simple change that had a yuge reduction in |
Beta Was this translation helpful? Give feedback.
-
I did some further investigation on this. There are
|
Beta Was this translation helpful? Give feedback.
-
@taylorotwell @ralphschindler I have done some work on the issue regards Excessive Would you mind to check / test this out - linaspasv/framework@06f189123bb17762d9473697201a1c831df77fac (updated - branch is based on master) ? I would love to get some feedback before PR submission. Thank you. My setup: $model = Model::find(1);
$model->options->get(10); // <--- ::get() is executed
// before the update used to execute ::set()
$model->options->get(20); // <--- reading from $classCastCache
// before the update used to execute ::set()
$model->options->put('test', 30); // <---- updating object at $classCastCache
// before the update:
// - 7 ::set() calls when custom cast value has been changed
// - 4 ::set() calls when custom cast value has not been changed
$model->save(); // <--- single set() is executed class Model extends Eloquent
{
protected $table = 'models';
protected $casts = [
'options' => CollectionCast::class
];
} class CollectionCast implements CastsAttributes
{
public function get($model, string $key, $value, array $attributes)
{
return new Collection(json_decode($value, true));
}
public function set($model, string $key, $value, array $attributes)
{
if ($value instanceof Collection) {
$value = $value->toArray();
}
return json_encode($value);
}
} |
Beta Was this translation helpful? Give feedback.
-
Maybe my thought in my previous comment isn't good solution. |
Beta Was this translation helpful? Give feedback.
-
I encountered this problem when trying to use separate classes to write and read some attribute. Reader class cannot be serialized with And exception is thrown when i tried to access model attribute: Maybe this will help someone. My custom cast class looks like: <?php
namespace App\Casts;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use App\Exceptions\ServiceResultCastException;
use App\Tools\Services\Contracts\ServiceResultReaderInterface;
use App\Tools\Services\MyService\ServiceResultReader;
class ServiceResult implements CastsAttributes
{
public function get($model, $key, $value, $attributes)
{
return ServiceResultReader::fromArray(json_decode($value, true));
}
public function set($model, $key, $value, $attributes)
{
if ($value instanceof ServiceResultReaderInterface) {
return [];
}
$result = json_encode($value);
if ($result === false) {
throw new ServiceResultCastException(
"Service result serialization failed"
);
}
return $result;
}
} |
Beta Was this translation helpful? Give feedback.
-
Hi @ralphschindler ! Is there any update on this issue? I also encountered your issue while I was creating a custom cast to simplify file uploads. Thank you for your contribution! 👍 Best regards, |
Beta Was this translation helpful? Give feedback.
-
Any chance we can get a solution to this in the main framework? I was rather confused and surprised why set was called so many times for my custom Caster that implements |
Beta Was this translation helpful? Give feedback.
-
@linaspasv said:
Yes - Taylor is understandably strict about accepting PRs which have any risk of creating backwards compatibility issues i.e. a breaking change within a Laravel version - breaking changes are reserved for new Laravel versions. But if these changes are straight forward and fix an obvious bug with no side effects, then he will accept it. I think the issue would come if you had a setter which did not produce the same result each time (as an example, say a counter that incremented every time there was a change) and where your code is now expecting the result from having been called multiple times. Yes, IMO this is an edge case rather than a common coding paradigm, nevertheless it might create a backwards compatibility issue. It is probably NOT too late to submit this as a (potentially) breaking change for Laravel 11 which is due out in a few weeks, especially if there is a pedigree of the trait working without issues for some time. |
Beta Was this translation helpful? Give feedback.
-
n/a
Description:
When attempting to write a CustomCast that will convert between a complex Object and the serialization for the database,
CastAttributes::set()
is called an excessive amount of times. This could be problematic (and unnecessary) if the work to serialize is non-trivial (like converting points into a binary representation.)This is due to excessive calls to
HasAttributes::mergeAttributesFromClassCasts()
that originate from multiple places in bothModel::getAttributes()
andModel::save()
calls:save()
itself: https://github.com/laravel/framework/blob/master/src/Illuminate/Database/Eloquent/Model.php#L670save()
callingperformInsert()
callinggetAttributes()
:https://github.com/laravel/framework/blob/master/src/Illuminate/Database/Eloquent/Model.php#L827
save()
callingfinishSave()
callingisDirty()
callinggetDirty()
callinggetAttributes()
: https://github.com/laravel/framework/blob/master/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php#L1326save()
callingfinishSave()
callingsyncOriginal()
callinggetAttributes()
: https://github.com/laravel/framework/blob/master/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php#L1205There is probably more to unpack here, and perhaps custom attributes should only be doing lightweight transformation, but it feels like this could be a good tool if Eloquent could manage to only transform the values to a savable value once before insert/update. Should we continue this discussion?
Steps To Reproduce:
7.x...ralphschindler:custom-cast-excessive-calls(branch now based off master: master...ralphschindler:custom-cast-excessive-calls)
Beta Was this translation helpful? Give feedback.
All reactions