This trick saves me having to cast all my foreign keys so they are always cast, using Eloquent’s built in casting mechanisms, to an integer. I attempted to put this in a trait, but had little luck as I couldn’t hook into a good event that would ensure the casts are available even when ‘newing’ up an object.
Doing this allows me to do strict comparisons against foreign keys and integers, it also saves me having to cast to integers in my api transformers. Of course I could manually add my foreign keys to the casts attribute…but I’m busy that day 😎
But really, I just feel that the snozberries should taste like snozberries, and that my integers should be integers. I love Laravel because it has taught me to care about how my code feels and it doesn’t feel right to me to see my values as strings when I’m expecting an integer.
Casting in Eloquent Models
Laravel offers a really simple way to cast your attributes to different value types. The reason you may want to cast your values is that when they are retrieved and added to the model, they are simply stored as strings. Casting allows us to say “Hey Laravel, be a good friend and make is_active a boolean for me - cheers”.
To setup a your casts on your model you just add to the $casts
property array. Use the attribute name as the key and the type to cast to as the value. Let’s check it out:
class User extends Eloquent
{
protected $casts = [
'is_active' => 'boolean'
];
}
Check out the available casting types.
Laravels Foreign Key Convention
Laravel’s foreign key convention is simply postfixing the database column name with _id
, for example if we are working with the classic User - Post relationship, on our posts table we would create a column called user_id
. By no means does Laravel enforce this convention, it just makes it easier for you if you follow it as you don’t need to override it when setting up eloquent relations in your models.
Creating Your Base Model
You will want to create a base class to inherit from Eloquent
and then all your models should inherit from your base class. I personally put my base models in my /app/Models
folder, like so:
<?php
namespace App\Models;
class Eloquent extends Model
{
//
}
so now in your model classes, you extend your new eloquent base model, ensuring that all the new functionality that we add is inherited everywhere, like so:
<?php
namespace App;
use App\Models\Eloquent;
class Post extends Eloquent
{
// your class definition...
}
Great, you can now add some goodness to your base class to use across all your models.
Settings For Foreign Key Casting
The first thing we need to do is setup some values. I use constants for the values that don’t change during runtime, but if you need to change anything on the fly, you might consider using properties instead.
class Eloquent extends Model
{
const FOREIGN_KEY_CAST_TYPE = 'integer';
const FOREIGN_KEY_CASTING_EXCEPTIONS = [];
const FOREIGN_KEY_REGEX = '/.+_id$/';
protected $foreignKeyCastsAdded = false;
}
Okay, so I think these are self explanatory, but hey, if I don’t talk about it all, I might as well just post the code right?!?
FOREIGN_KEY_CAST_TYPE
: is the type of cast that is going to occur. I want all my foreign keys to be cast to integers.
FOREIGN_KEY_CASTING_EXCEPTIONS
: if there are any attributes that match the foreign key RegEx but you don’t want to automatically include, you can add them to this array.
FOREIGN_KEY_REGEX
: allows you to change the RegEx used to detect if an attribute is a foreign key. This RegEx basically says find any string that ends with _id
, but you can adjust to suit your needs if you use another convention.
$foreignKeyCastsAdded
: is just a flag that we will use to determine if we have added our casts already so we don’t need to do it repeatedly.
Making It Happen
So now we need to simply add the foreign key attributes to the $casts
attribute provided by Eloquent. To do this, we are going to hook into the getCasts
method:
public function getCasts()
{
if (!$this->foreignKeyCastsAdded) {
$this->addForeignKeyCasts();
}
return parent::getCasts();
}
So now when Eloquent retrieves the casts, we will first add our custom casts and then continue on as usual. Now, in the infamous words of the Joker: 🤡 “here we go”
protected function addForeignKeyCasts()
{
foreach ($this->attributes as $key => $value) {
if ($this->shouldCastForeignKey($key)) {
$this->addForiegnKeyCast($key);
}
}
$this->foreignKeyCastsAdded = true;
}
protected function shouldCastForeignKey($key)
{
return $this->isForeignKey($key)
&& !$this->isForeignKeyCastingException($key);
}
protected function isForeignKey($key)
{
return preg_match(static::FOREIGN_KEY_REGEX, $key);
}
protected function isForeignKeyCastingException($key)
{
return in_array($key, static::FOREIGN_KEY_CASTING_EXCEPTIONS);
}
protected function addForiegnKeyCast($key)
{
if (!in_array($key, $this->casts)) {
$this->casts[$key] = static::FOREIGN_KEY_CAST_TYPE;
}
}
Again hopefully this is pretty readable, but if not, let’s step through it.
- Loop through all models attributes.
- Check if the attribute matches the foreign key RegEx and that it isn’t in the exception array.
- If is to be added, we add the attribute to the casts array, using the cast type.
Notes
- I tried doing this in a trait and also hooking into the constructor, but for the time being, the
getCasts
method looks like the best method to hook into. If you know a better place to put this, I’d love to hear from you. - I’m no RegEx master - so feedback welcomed.
- I’ve added this to a Gist, so check it out for any improvements.
- ‘Foreign’ is freaking hard to spell right every single time…seriously! I’m sure ‘i’ was supposed to come before ‘e’ right 🤓
Updates
So yea, this was a cool idea and fun to put together but at the end of the day I realised it had no practical outcome, especially in a language like PHP where we can lean on its dynamic nature. Learnt a few things along to way though - so not a total waste of time!