Custom casts have been in Laravel since version 7 and it’s one of the best features we’ve had in years (it had been requested for a long time). Often when developing with Laravel’s models it becomes difficult to store what you want with the basic array or object casts as this often meant what you actually wanted was a class instance instead with it’s own properties and methods to manipulate the attribute. We could get around this with attribute accessors and mutators but it often meant lots of clumsy logic loaded into the model and defined for each attribute using this. There was also problematic behaviour with mutations of objects and array on a model’s attribute requiring the whole thing to be set again. Instead with custom casts it’s very easy to make reusable logic now across projects and doesn’t have this issue.
For the sake the article we’re going to talk about real world examples of different times a cast could come in hand. These 3 challenges is, storing a monetary value, a location (including address) and a date/time interval. To demo this we’re going to create an Order class that will use all of these casts, we’ll create it with the following command:
php artisan make:model Order -m
After this, we will have a app/Models/Order.php file read to use. We’ve used the -m flag as well because we want a migration to create the order table and add some fields. Don’t forget to use the migrate command each time we change our migration.
php artisan migrate:fresh
Working with money is often more complicated than initially thought. The way money is calculated can leave things like remainers which need to be accounted for. Brick’s Money package for PHP is incredibly useful for this as it creates value objects for money that can be interacted with to perform these operations. First we need to require the package with the following command:
composer require brick/money
Now we’re going to make a cast for the model to be able to associate with the attributes.
php artisan make:cast Money
This will provide us with a class in app/Casts/Money.php. An important thing to remember is that a cast is only the mechanism that performs the transformation, this may seem obvious but was something I didn’t understand at the time of them being released. Each cast only has a get and set method by default, this will handle retrieving and storing an object to database fields. You still need have a class, array, primitive etc to cast to and from.
In this example we need two fields in our orders table. One to store the price and another to store the currency code. This is the information we need to persist from the object and to create a new one when retrieving a model from the database.
In our migration database/migrations/_create_orders_table.php we can then add these columns in the table creation statement:
Now we can start working with our cast to convert these two columns into a Brick Money instance.
As you can see this is a fairly simple method call to create and return the object. There’s a notable flaw though and that is the attributes being used are hard coded to the price column and currency column. This is fine but what if we wanted to have two amounts on the Model? Well we can fix this by using a constructor to set the columns we do want to use, in fact we’ll also add a new parameter for our cast called useMinor.
And now as you can see the cast will be able to adapt to the fields in the table when it comes to getting the object. The purpose of the useMinor is also a little clearer when you see the code. Often when we store monetary values we think about money having 2 decimal points, I being from the UK would refer to these decimals and pennies and America’s would call them cents. Not all currencies use this though, Japanese Yen for example does not use decimals. By using the useMinor property we are choosing to store the value in pennies and cents over Pounds and Dollars respectively removing any decimal points.
Now we need to complete the custom cast by storing the value back when the model is saved. Like the get method we need to work with both values of an amount and a currency.
You’ll notice immediately that the set method is returning an array, this is because we need to make sure both fields are set and by returning an array the columns will be added to the data. We also again have some logic to look at if we want to save the amount as a minor amount or not.
To make use of the cast we will then have to configure the model. We do this by adding the $casts property which is an array mapping field to type.
As you can see in the example, we haven’t used any parameters with this cast because we’ve allowed them to be defaults but if we wished to we could do so by adding to the field mapping like so:
Note we must use a 0 instead of typing false for the last parameter as that would lead to the cast being initiated with a value that is true. It’s a common issue that might catch someone out.
The next example is date time intervals. For example, you might want to store a value in the database as say 20 days rather than storing a set date. Not adding those days to a DateTime instance would be trivial but if you want to be a bit more advanced you could make a DateInterval object. To do this we’re actually going to use the Carbon DateInterval class and although it’s in all Laravel projects by default we’re going to add the dependency to our Laravel project just to be safe. We can do this using:
composer require nesbot/carbon
Then like with the money example before this, we’re going to add a column to the table migration so that it will store the number of days.
After that we’ll need to create our DateInterval cast.
php artisan make:cast DateInterval
Then we can implement the get and set methods to handle our conversions between formats.
Note in this example compared to the previous that we only return a primitive value for the set method. We don’t have to specify which columns because the cast doesn’t rely on more than one database column, instead the key of the field mapping with be used by default.
Likewise you’ll notice we also don’t have a constructor because we don’t need to configure the columns, the cast will handle all that to make our date interval on the model.
One thing that would be useful though is to actually add a constructor and parameter to be able to set the unit of the intervals, namely if it’s minutes, hours or days etc.
Then when we apply the cast to the model we can state which unit to store the interval in.
A question you might have about this is why not just store the raw format. As anyone who’s worked with DateInterval before, you probably know that there’s a format you can use e.g. 1h30m and you would be right in that we could save ourselves some trouble but this might become an issue when you start to run queries in the database. For instance you might want to query by the days column and ultimately find that your database doesn’t know how to process the format whereas adding a simple integer as days to a timestamp is relatively easy. This is ultimately something you should consider when creating custom casts, will you need to query that data at a database level and how will you do it when you need to?
The idea of storing a location in a database is pretty universal, this might be for addresses across the world but could also be for latitude and longitude coordinates. Equally it can be difficult to fetch these addresses from multiple sources e.g. Google Maps and other equivalent services.
A useful library is Geocoder PHP which is designed to work with a number of services for looking up addresses or finding coordinates for an address. Much like the other two examples, we’re going to install this library to use a class from this package to represent the location. We do this with the following command:
composer require willdurand/geocoder
And this time we’re going to add only a json column to the migration for the address. JSON columns are really useful for when we want to store more complex data, typically these columns are just text columns which can store an infinite number of characters.
We’re then going to again make a new cast:
php artisan make:cast Address
And this time we’re going to purely make it so that when we store the address we’re both creating the value from an array that we decode from a JSON string that we then fetch as an array from the object and encode back into JSON when we save the model.
As you can see this is a pretty common approach for storing more complex data which is equally easy to query against with MySQL and Postgres because they allow JSON support. For example we might use the following query in the knowledge we know a key exists in the JSON:
DB::table(‘orders’) ->where(‘address->postalCode’, ‘30582–0378’) ->get();
Equally if we wished to we could potentially write a more advanced custom cast like our money example which would break the address data into multiple columns should we want to be able to apply database indexes for particular fields like the postcode.
Using custom casts is an incredibly powerful tool and if this is the first time looking at them I really suggest going through the documentation for them. They’re awesome but can be difficult to master at first. Also I really can’t stress enough that you should consider writing unit tests for your custom casts that ensure what comes into and out of the casts is correct because it can be a real nightmare to only discover later down the line a mistake has been made.
In making this article I created my usual example repository but equally I have created both feature tests for the model and individual unit tests for the custom casts so you can see the behaviours described in this article for yourselves.
I’m Peter Fox, a software developer in the UK who works with Laravel among other things. Thank you for reading my article, I’ve got several more on both medium and dev.to. If you want to know more about me, head over to https://www.peterfox.me. I’m also now also Sponsorable on GitHub. If you’d like to encourage me to write more articles like this please do consider dropping a small one off donation.