Eloquent Attribute Casting

Admin   Laravel   318  2022-06-30 22:00:02

Eloquent Castable attributes are one of the more powerful features of Laravel; some people use them religiously while others tend to shy away from them. In this tutorial, I will walk through a few different examples of using them and, most importantly, why you should be using them.

You can create a new Laravel project or use an existing one for this tutorial. Feel free to follow along with me, or if you want to read and remember - that is fine too. The critical thing to take away from this is just how good eloquent casts can be.

Let's start with our first example, similar to the one in the Laravel documentation. The Laravel documentation shows getting a user's address but using a Model too. Now imagine if this was instead a JSON column on the user or any model for that fact. We can get and set some data and add extra to make this work as we want. We need to install a package by Jess Archer called Laravel Castable Data Transfer Object. You can install this using the following composer command:

1composer require jessarcher/laravel-castable-data-transfer-object

Now we have this installed, let's design our Castable class:

1namespace App\Casts;
2 
3use JessArcher\CastableDataTransferObject\CastableDataTransferObject;
4 
5class Address implements CastableDataTransferObject
6{
7 public string $nameOrNumber,
8 public string $streetName,
9 public string $localityName,
10 public string $town,
11 public string $county,
12 public string $postalCode,
13 public string $country,
14}

We have a PHP class that extends the CastableDataTransferObject, which allows us to mark the properties and their types, then handles all the getting and setting options behind the scenes. This package uses a Spatie package called Data Transfer Object under the hood, which has been quite popular in the community. So now, if we wanted to extend this at all, we could:

1namespace App\Casts;
2 
3use JessArcher\CastableDataTransferObject\CastableDataTransferObject;
4 
5class Address implements CastableDataTransferObject
6{
7 public string $nameOrNumber,
8 public string $streetName,
9 public string $localityName,
10 public string $town,
11 public string $county,
12 public string $postalCode,
13 public string $country,
14 
15 public function formatString(): string
16 {
17 return implode(', ', [
18 "$this->nameOrNumber $this->streetName",
19 $this->localityName,
20 $this->townName,
21 $this->county,
22 $this->postalCode,
23 $this->country,
24 ]);
25 }
26}

We have an Address class that extends the CastableDataTransferObject class from the package, which handles all of our getting and setting of data to the database. We then have all the properties we want to store - this is the UK format address as it is where I live. Finally, we have a method that we have added that helps us format this address as a string - if we want to display this in any form of the user interface. We could take it a step further with postcode validation using regex or an API - but that might defeat the point of the tutorial.

Let's move on to another example: money. We all understand money; we know that we can use money in its smallest form of coins and its larger form of notes. So imagine we have an e-commerce store where we store our products (a simple store so we don't have to worry about variants, etc.), and we have a column called price which we store in the smallest denominator of our currency. I am in the UK, so I will call this pence. However, this in the US is cents. A common approach to storing monetary values in our database as it avoids floating-point math issues. So let's design a cast for this price column, this time using the php moneyphp/money package:

1namespace App\Casts;
2 
3use Money\Currency;
4use Money\Money;
5use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
6 
7class Money implements CastsAttributes
8{
9 public function __construct(
10 protected int $amount,
11 ) {}
12 
13 public function get($model, string $key, $value, array $attributes)
14 {
15 return new Money(
16 $attributes[$this->amount],
17 new Currency('GBP'),
18 );
19 }
20 
21 public function set($model, string $key, $value, array $attributes)
22 {
23 return [
24 $this->amount => (int) $value->getAmount(),
25 $this->curreny => (string) $value->getCurrency(),
26 ];
27 }
28}

So this class doesn't use the DTO package but instead returns a new instance with its own methods. In our constructor, we pass in an amount. When we get and set the amount, we are either casting to an array for storage in a database or parsing an array to return a new money object. Of course, we could make this a data transfer object instead and control how we handle money a little more. However, the php money library is pretty well tested and reliable, and if I am honest - there isn't much I would do differently.

Let's go to a new example. We have a CRM application, and we want to store business open hours or days. Each business needs to be able to mark the days they are open, but only in a simple true or false way. So we can create a new cast, but first, we will make the class that we want to cast to, much like the money PHP class above.

1namespace App\DataObjects;
2 
3class Open
4{
5 public function __construct(
6 public readonly bool $monday,
7 public readonly bool $tuesday,
8 public readonly bool $wednesday,
9 public readonly bool $thursday,
10 public readonly bool $friday,
11 public readonly bool $saturday,
12 public readonly bool $sunday,
13 public readonly array $holidays,
14 ) {}
15}

To begin with, this is fine; we just want to store if the business is open each day and have an array of days they count as holidays. Next, we can design our cast:

1namespace App\Casts;
2 
3use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
4 
5class OpenDaysCast implements CastsAttributes
6{
7 public function __construct(
8 public readonly array $dates,
9 ) {}
10 
11 public function set($model, string $key, $value, array $attributes)
12 {
13 return $this->dates;
14 }
15 
16 public function get($model, string $key, $value, array $attributes)
17 {
18 return Open::fromArray(
19 dates: $this->dates,
20 );
21 }
22}

Our constructor will accept an array of dates to simplify the saving (however, you will need to make sure you validate input properly here). Then when we want to get the data back out from the database, we create a new Open object, passing in the dates. However, here we are calling a fromArray method that we have yet to create, so let's design that now:

1public static function fromArray(array $dates): static
2{
3 return new static(
4 monday: (bool) data_get($dates, 'monday'),
5 tuesday: (bool) data_get($dates, 'tuesday'),
6 wednesday: (bool) data_get($dates, 'wednesday'),
7 thursday: (bool) data_get($dates, 'thursday'),
8 friday: (bool) data_get($dates, 'friday'),
9 saturday: (bool) data_get($dates, 'saturday'),
10 sunday: (bool) data_get($dates, 'sunday'),
11 holidays: (array) data_get($dates, 'holidays'),
12 );
13}

So we manually build up our Open object using the Laravel helper data_get, which is extremely handy, making sure that we are casting to the correct type. Now when we query, we have access:

1$business = Business:query()->find(1);
2 
3// Is this business open on mondays?
4$business->open->monday; // true|false
5 
6// Is the business open on tuesdays?
7$business->open->tuesday; // true|false
8 
9// What are the busines holiday dates?
10$business->open->holidays; // array

As you can see, we can make this extremely readable so that the developer experience is logical and easy to follow. Can we then extend this to add additional methods, such as is it open today?

1public function today(): bool
2{
3 $date = now();
4 
5 $day = strtolower($date->englishDayOfWeek());
6 
7 if (! $this->$day) {
8 return false;
9 }
10 
11 return ! in_array(
12 $date->toDateString(),
13 $this->holidays,
14 );
15}

So this method may make a small assumption that we are storing holidays in a date string, but the logic follows: get the day of the week, in English, as that is our property name; if the day is false, then return false. However, we also need to check the holidays. If the current date string is not in the holiday's array, it is open; otherwise, it is closed.

So we can then check to see if the business is open using the today method on our cast:

1$business = Business:query()->find(1);
2 
3// Is the business open today?
4$business->open->today();

As you can probably imagine, you can add many methods to this, allowing you to check multiple things and even add ways to display this information nicely.

Do you use Attribute casting in your application? Do you have any other cool examples you could share? Please drop us a tweet with your examples and share the power of knowledge.

Source: laravel-news.com