Skip to content

Commit e0c2620

Browse files
authored
[8.x] Attribute Cast / Accessor Improvements (#40022)
### 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` and `set{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` and `set` - 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 an `Illuminate\Database\Eloquent\Casts\Attribute` instance: ```php /** * Get the user's title. */ protected function title(): Attribute { return new Attribute( get: fn ($value) => strtoupper($value), set: fn ($value) => strtolower($value), ); } ``` ### 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 of `address_line_one`, `address_line_two`, etc.), immediately set a property on that value object, and then `save` the model like so: ```php use App\Models\User; $user = User::find(1); $user->address->lineOne = 'Updated Address Value'; $user->save(); ``` 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: ```php /** * Get the user's address. */ protected function address(): Attribute { return new Attribute( get: fn ($value, $attributes) => new Address( $attributes['address_line_one'], $attributes['address_line_two'], $attributes['city'], ), set: fn ($value) => [ 'address_line_one' => $value->addressLineOne, 'address_line_two' => $value->addressLineTwo, 'city' => $value->city, ], ); } ``` ### 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: ```php public function setTitleAttribute($value) { $this->attributes['title'] = strtolower($value); } ``` New approach: ```php protected function title(): Attribute { return new Attribute( set: fn ($value) => strtolower($value), ); } ``` ### 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.
1 parent 739d847 commit e0c2620

File tree

5 files changed

+544
-10
lines changed

5 files changed

+544
-10
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
namespace Illuminate\Database\Eloquent\Casts;
4+
5+
class Attribute
6+
{
7+
/**
8+
* The attribute accessor.
9+
*
10+
* @var callable
11+
*/
12+
public $get;
13+
14+
/**
15+
* The attribute mutator.
16+
*
17+
* @var callable
18+
*/
19+
public $set;
20+
21+
/**
22+
* Create a new attribute accessor / mutator.
23+
*
24+
* @param callable $get
25+
* @param callable $set
26+
* @return void
27+
*/
28+
public function __construct(callable $get = null, callable $set = null)
29+
{
30+
$this->get = $get;
31+
$this->set = $set;
32+
}
33+
34+
/**
35+
* Create a new attribute accessor.
36+
*
37+
* @param callable $get
38+
* @return static
39+
*/
40+
public static function get(callable $get)
41+
{
42+
return new static($get);
43+
}
44+
45+
/**
46+
* Create a new attribute mutator.
47+
*
48+
* @param callable $set
49+
* @return static
50+
*/
51+
public static function set(callable $set)
52+
{
53+
return new static(null, $set);
54+
}
55+
}

0 commit comments

Comments
 (0)