-
Notifications
You must be signed in to change notification settings - Fork 11.1k
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
[8.x] Attribute Cast / Accessor Improvements #40022
Conversation
Is |
@inxilpro suggested using return type of the method instead of a PHP 8 attribute to determine if a method is an accessor / mutator. That would probably allow this feature to work on PHP 7.3+, etc: public function title() : Attribute
{
// ...
} However, @mpociot suggested that accessors are more common than mutators (in his opinion) and it would be nice to be able to do this to just define an accessor: These two suggestions aren't compatible - so will need to decide on a route forward here. |
What about extending your PR a bit like suggested here https://twitter.com/robboclancy/status/1469203713470386182?s=21 |
I like this suggestion but I wonder if it clashes with the long-established convention around these types of “floating” methods where property vs method call is for relationship vs query builder. If it can be made to work with just the |
I vote to keep
I don't understand why. |
If we wanted to support read-only or write-only attributes we could do something like this: interface ReadableAttribute
{
public function get();
}
interface WritableAttribute extends ReadableAttribute
{
public function set($value);
}
class Mutator implements WritableAttribute
{
// ...
}
class Accessor implements ReadableAttribute
{
// ...
}
class Attribute implements ReadableAttribute, WritableAttribute
{
// ...
} That way you can do any of the following: public function firstName(): Accessor
{
return new Accessor(fn($value) => ucfirst($value));
}
public function email(): Mutator
{
return new Mutator(fn($value) => strtolower($value));
}
public function name(): Attribute
{
return new Attribute(
get: fn() => "{$this->attributes['first_name']} {$this->attributes['last_name']}",
set: function($value) {
[$first, $last] = explode(' ', $value, 2);
$this->attributes['first_name'] = $first;
$this->attributes['last_name'] = $last;
},
);
} That said, I really don't see a huge upside for supporting the individual use-cases, especially with named parameters. It just feels like always returning an This is barely more work than @mpociot's suggestion, and keeps everything under public function firstName(): Attribute
{
return new Attribute(get: fn($value) => ucfirst($value));
} |
We could add a static helper method to make it a tiny bit cleaner: public static function get(Closure $mutator)
{
return new static(get: $mutator);
} That way you could just do |
I have updated this PR to implement @inxilpro's suggestion of "marking" the methods using a return type instead of an attribute: /**
* Get the user's title.
*/
protected function title(): Attribute
{
return new Attribute(
get: fn ($value) => strtoupper($value),
set: fn ($value) => strtolower($value),
);
} |
@taylorotwell if we use return types here, and especially if #39947 gets merged, it probably makes sense to add a method to the Reflector::methodReturnsType($reflectionMethod, Attribute::class); |
@taylorotwell my point was that I see no incompatibility, both approaches are able to coexist without a conflict: protected function title(): Attribute
{
return new Attribute(
get: fn ($value) => strtoupper($value),
set: fn ($value) => strtolower($value),
);
}
#[AsAccessor]
protected function firstName($name)
{
return ucfirst($name);
} But the second one is not very necessary, because |
@inxilpro I think that the "new" one-way attribute approach would be too verbose compared to the old one or attribute suggestion: // return type
protected function firstName(): Accessor
{
return new Accessor(fn($value) => ucfirst($value));
}
// Attribute
#[AsAccessor]
protected function firstName($name)
{
return ucfirst($name);
}
// Old
public function getFirstNameAttribute($name)
{
return ucfirst($name);
} Easier to comprehend what's going on when the function body does the lifting instead of a class-wrapped callback. |
I don't think it should get replaced in documentation with this new method. Rather add it as the "other option". |
This reverts commit 69565a1.
IMHO the proposed option is much cleaner, simpler to understand, and extendable. As I understand, we could have one "style" of working with attributes, I will allow me to call something like |
Is it possible to somehow define the type of an Attribute using this approach so my IDE can understand? |
Add a |
But we can also extend the Attribute class and use custom attribute with that approach, right?
Best,
Vladyslav G.
… 14.12.2021, в 22:43, Taylor Otwell ***@***.***> написал(а):
Is it possible to somehow define the type of an Attribute using this approach so my IDE can understand?
Add a @Property annotation to the top of your class.
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub, or unsubscribe.
|
When I saw Taylor's comment I though this would be a great practice to encourage. In fact, another benefit of marking this methods as protected is that they will not be displayed by the IDE during auto-completion in other classes. laravel/framework#40022 (comment)
Is PHP 8+ requirement too soon? Constructor promotion would be great for this. (The methods should also get a return type)
|
Laravel 8.x supports PHP 7.3+, so constructor promotion is a no. |
This. Also the reason why we cannot use named parameters instead of passing null to the constructor as the first parameter. |
I am a little late to the party, but now that it's possible to do something like protected function name(): Attribute
{
return Attribute::get(fn($value) => strtoupper($value));
} wouldn't it also be desirable to add non-static protected function name(): Attribute
{
return Attribute::get(fn($value) => strtoupper($value))
->set(fn($value) => strtolower($value));
} This would really offer the developer the full monty 😁 |
Is there a way to have other named parameters, or extend the Attribute class to allow them? I'm just thinking it'd be nice for Laravel Collective to have
and so on. |
is it only running on PHP 8 ? |
hooo, its can running on PHP 7.4 too. thanks @taylorotwell for a great feature |
The named parameters approach will only work from PHP 8. So if you only define a getter or a getter and setter (in that order), it will work on lower versions as well, but if you try to only define a setter, you will need to manually pass null as the first parameter for Argument instantiation. |
Could you explain what is the difference between the new Attributes and existing Casts feature? So lets take an example strait from Laravel's documentation class Json implements CastsAttributes
{
public function get($model, $key, $value, $attributes)
{
return json_decode($value, true);
}
public function set($model, $key, $value, $attributes)
{
return json_encode($value);
}
}
class User extends Model
{
protected $casts = [
'options' => Json::class,
];
} vs class Json extends Attribute
{
public function __construct()
{
parent::__construct(
get: fn ($value) => json_decode($value, true),
set: fn ($value) => json_encode($value),
);
}
}
class User extends Model
{
protected function options(): Attribute
{
return new Json();
};
} |
Have an issue with this,
getting this worked before switching to new way of accessors/mutators. |
I haven't fully switched to this new approach, at least for the getter/accessor part, because I haven't found a way to not loose access to the original attribute value. I mean, the one that hasn't been modified by the accessor mechanism. At least with the getXXXAttribute() approach you can add a new attribute with a different name and you don't loose access to the original attribute value. Am I missing something? Thanks a lot. |
@nachopitt If I understood you correctly, you can create new attributes the same way.
Then you can access your new attribute
|
Summary
This pull request adds a new way to define attribute "accessors / mutators" as they are currently called in the
documentation: https://laravel.com/docs/8.x/eloquent-mutators#accessors-and-mutators
Currently, accessors and mutators are added to a model by defining
get{Foo}Attribute
andset{Foo}Attribute
methods on the model. These conventionally named methods are then used when the developers attempts to access the$model->foo
property on the model.This aspect of the framework has always felt a bit "dated" to me. To be honest, I think it's one of the least elegant parts of the framework that currently exists. First, it requires two methods. Second, the framework does not typically prefix methods that retrieve or set data on an object with
get
andset
- it just hasn't been part of Laravel's style (e.g.,$request->input()
vs.$request->getInput()
,$request->ip()
vs.$request->getIp
, etc.This pull request adds a way to define attribute access / mutation behavior in a single method marked by the
Illuminate\Database\Eloquent\Casts\Attribute
return type. In combination with PHP 8+ "named parameters", this allows developers to define accessor and mutation behavior in a single method with fluent, modern syntax by returning anIlluminate\Database\Eloquent\Casts\Attribute
instance:Accessors / Mutators & Casts
As some might have noticed by reading the documentation already, Eloquent attribute "casts" serve a very similar purpose to attribute accessors and mutators. In fact, they essentially serve the same purpose; however, they have two primary benefits over attribute accessors and mutators.
First, they are reusable across different attributes and across different models. A developer can assign the same cast to multiple attributes on the same model and even multiple attributes on different models. An attribute accessor / mutator is inherently tied to a single model and attribute.
Secondly, as noted in the documentation, cast classes allow developers to hydrate a value object that aggregates multiple properties on the model (e.g.
Address
composed ofaddress_line_one
,address_line_two
, etc.), immediately set a property on that value object, and thensave
the model like so:The current, multi-method implementation of accessor / mutators currently does not allow this and it can not be added to that implementation minor breaking changes.
However, this improved implementation of attribute accessing does support proper value object persistence in the same way as custom casts - by maintaining a local cache of value object instances:
Mutation Comparison
In addition, as you may have noticed, this implementation of accessors / mutators does not require you to manually set the properties in the
$this->attributes
array like a traditional mutator method requires you to. You can simply return the transform value or array of key / value pairs that should be set on the model:"Old", two-method approach:
New approach:
FAQs
What if I already have a method that has the same name as an attribute?
Your application will not be broken because the method does not have the
Attribute
return type, which did not exist before this pull request.Will the old, multi-method approach of defining accessors / mutators go away?
No. It will just be replaced in the documentation with this new approach.