-
Notifications
You must be signed in to change notification settings - Fork 11.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[10.x] Support array syntax for casts #46649
Conversation
I'm a little concerned about the breaking change to |
In my very unscientific testing, the overhead for a model with 15 casts, 5 of which are arrays, is 0.001ms: Benchmark::dd(function () use ($_casts) {
array_map(static function (array|string $castType) {
if (is_array($castType)) {
[$castType, $arguments] = [array_shift($castType), $castType];
$castType = $castType . ':' . implode(',', $arguments);
}
return $castType;
}, $_casts);
}, 15_000); I don't think this overhead will be noticeable, and we still have the solution to add a cache if it is. Your suggestion is also better, less code needs to be changed. I'll update the PR 👍 |
This comment was marked as outdated.
This comment was marked as outdated.
@johanrosenson This is not valid PHP, you can only use literals and constant expressions to initialize class properties... I also wanted that syntax myself but that's not a thing, unfortunately. :( |
You are 100% correct, i didn't think about exactly where the casts are defined, too bad, it would have been neat :) |
@johanrosenson I agree and have often wanted that syntax myself but, as noted, it's not allowed by PHP. We would have to have a method (instead of a property) that returns an array of casts for the model. |
One concern I have with this PR is performance. We've now added a loop over all of the defined casts everytime |
As I said in my previous comment, I did a basic benchmark — the loop wasn't noticeable. But if this is still a concern, we can add a static cache to only perform the loop once? |
Well, tried to add cache, but tests fail because it's static and isn't reset between tests. I'm not sure if a static cache there could cause troubles in a real app though. Should I just fix the tests? Or should I remove the cache? |
The static property needs to be defined on each model itself to be cache per model instance, otherwise it's shared across all models and the key probably should have a model class prefix, but I don't think a static cache is a good option, because you can specify casts dynamically per model basis, as in with a query builder: MyModel::query()->withCasts(['my_joined_prop' => 'bool'])->get(); which would break. |
You're totally right that the cache is being shared, my bad. For the dynamic casts though, you're right but we could simply clear the cache when this happens. EDIT: I've been trying to clear the cache where needed, but honestly I think the cache i overkill and adds too much complexity — I couldn't see any performance penalty in my tests |
@innocenzi Can you elaborate on your benchmark. I don't think it's 0.001ms for 15,000 iterations. 0.001 is the average amount of time the callback took across the 15,000 iterations - not the sum of all iterations. I would be more curious about the sum of 15,000 invocations of |
Yeah it was definitely the average, sorry if I wasn't clear. It should be a total of ~15ms overhead for 15k |
So, here is another benchmark, the average time for 15k
Benchmark code$durations = [];
for ($iteration = 0; $iteration < 1000; $iteration++) {
$stopwatch = new Stopwatch(true);
$stopwatch->start('default');
for ($call = 0; $call < 15_000; $call++) {
Listing::make()->getCasts();
}
$result = $stopwatch->stop('default');
$durations[] = $result->getDuration();
}
dd(collect($durations)->average()); |
I see you committed the cache? Can you remove it? I don't think it's totally bulletproof. Casts are an instance level concept and managing them at the static / global level could be problematic. For example, imagine you merge some casts into one model, cache is rebuilt... but the next model you interact with of that same type will use the rebuilt cache even though you never merged that specific cast into its cast definition. In my own benchmarking of 150,000 invocations I see |
This reverts commits after e66088e.
Sure, I removed the cache. Can I ask what was the code for your benchmark? If the performance overhead is definitely an issue, I think the only solution would be to make a backwards incompatible implementation where |
$start = microtime(true);
$model = new Flight;
for ($i = 0; $i < 150000; $i++) {
$model->getCasts();
}
dd(number_format((microtime(true) - $start) * 1000, 2)); Flight had casts like: protected $casts = [
'name' => 'string',
'age' => 'int',
'votes' => 'int',
'admin' => 'bool',
'cancelled' => 'bool',
'revoked' => 'bool',
'banned' => 'bool',
'repairing' => 'bool',
'options' => 'array',
'metadata' => 'array',
'final' => [AsCollection::class, 'something'],
]; |
Right, I have about the same results as you with this benchmark. What do you suggest? Is this acceptable, or should we make this more performant (but with a BC break on |
I personally would be interested in brainstorming a way to define casts via a method. That would allow for even better syntax such as invoking methods to build cast strings, etc. while avoiding any performance penalty since the method could return an array of simple strings that don't have to be iterated over. public function someCastDefinitionFunction()
{
return [
'something' => AsCollection::using(CustomClass::class),
];
} |
@taylorotwell Sorry, but, Isn't it simple one:
Just add a builder, which also could be implemented as a trait to cast-class. |
Personally I still think exploring the array syntax is worth, even if it means waiting for 11.x, because adding casts to a class property is easier and having to add a method is more cumbersome. With that being said, the idea of having methods to build cast strings is really appealing, even if it means it has to be in a method. @Angel5a If this feature has to be implemented using a method, I'd look for a way without traits, because that'd be even more cumbersome. I'm not sure what the overhead would be, but I guess calling the As for the name, I like |
Using the same benchmark as earlier, Code// HasAttributes.php
public function getCasts()
{
if (method_exists($this, 'defineCasts')) {
$this->casts = array_merge($this->defineCasts(), $this->casts);
}
if ($this->getIncrementing()) {
return array_merge([$this->getKeyName() => $this->getKeyType()], $this->casts);
}
return $this->casts;
} |
I'll be honest that IDK on how that could be applied here or if this is something that you want as the design for this. But in the DTO package that I created, the cast is defined like this: protected function casts(): array
{
return [
'property' => new CarbonImmutableCast(),
];
} And when needed any parameters we just send it to the constructor: protected function casts(): array
{
return [
'property' => new CarbonImmutableCast('Europe/Lisbon', 'Y-m-d'),
];
} Here's the source code of the CarbonImmutableCast: https://github.com/WendellAdriel/laravel-validated-dto/blob/main/src/Casting/CarbonImmutableCast.php And the only thing to create custom casts is to implement this simple interface: https://github.com/WendellAdriel/laravel-validated-dto/blob/main/src/Casting/Castable.php This works like a charm for the package but IDK if this design would be something to bring to the Eloquent casts. |
@innocenzi My example is just to show, that this is possible without any intrusion onto framework right now. A word on your sketch. I don't know if call to A word on the idea of array syntax. I like it. Nice readable syntax. It's worth to wait 11.x. All this string concatenations looks ugly. |
@Angel5a yes, you are correct that is one way to accomplish a method based approach now. |
@taylorotwell I refactored the PR to use If you are interested, I can open a new PR in addition to this one for a |
@innocenzi Let me know what do you think about this: #47237. |
@nunomaduro LGTM and I think we can close this PR and track the feature in #47237 👍 |
Currently, to pass arguments to casts, we use string concatenation:
This pull request implements the array syntax to perform the same thing, but in a more readable way: