Eloquent Type Casting: A Laravel Sushi case study

There are instances in Laravel applications where type consistency is very important, else subtle bugs creep in. I learned this recently about Laravel Sushi ― a package by our beloved Caleb Porzio ― that lets you create Eloquent models backed by arrays instead of database tables as is typical in Laravel apps. In this article, I'll show how Eloquent's casting feature can help fix an issue with Livewire, and hopefully also show how this makes your code more maintainable.

The Boolean Trap in Sushi Models

Laravel Sushi creates models from arrays, but without proper type definitions, you can run into unexpected behavior. Consider this simple example:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Sushi\Sushi;

class FeatureFlag extends Model
{
    use Sushi;

    protected $rows = [
        ['id' => 1, 'name' => "Dark Mode", 'enabled' => 1],
        ['id' => 2, 'name' => "Beta Features", 'enabled' => 0],
    ];

    // No casts defined yet!
}
Databases in VS Code? Get DevDb

When you use this model in a Livewire component:

<?php

namespace App\Livewire;

use Livewire\Component;
use App\Models\FeatureFlag;

class FeatureSettings extends Component
{
    public $feature = [
        'enabled' => false,
    ];

    public function loadFeature($id)
    {
        $flag = FeatureFlag::find($id);
        $this->feature = [
            'enabled' => $flag->enabled, // This is 1 or 0, not true/false!
        ];
    }

    public function render()
    {
        return view('livewire.feature-settings');
    }
}

And bind it to a checkbox:

<div>
    <label>
        <input type="checkbox" wire:model="feature.enabled">
        {{ $feature['name'] ?? 'Select a feature' }}
    </label>

    <div class="mt-4">
        <button wire:click="loadFeature(1)">Load Dark Mode</button>
        <button wire:click="loadFeature(2)">Load Beta Features</button>
    </div>

    <!-- Debug info -->
    <div class="mt-2 text-sm text-gray-600">
        Value: {{ var_export($feature['enabled'], true) }}
    </div>
</div>

You'll notice that the checkbox doesn't always reflect the correct state. Why? Because Sushi returns integers (0 or 1) rather than booleans (false or true), and evidently livewire does a strict equality check for wire:model binding.

Eloquent Casting to the Rescue

The solution is simple but extremely important. Add the $casts property to your Sushi model:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Sushi\Sushi;

class FeatureFlag extends Model
{
    use Sushi;

    protected $rows = [
        ['id' => 1, 'name' => "Dark Mode", 'enabled' => 1],
        ['id' => 2, 'name' => "Beta Features", 'enabled' => 0],
    ];

    protected $casts = [
        'enabled' => 'boolean',
    ];
}

With this single line, Eloquent will automatically convert the integer values to proper booleans when retrieving them from the model. Now, when you load your feature flag, $flag->enabled will be true or false instead of 1 or 0 as livewire's model binding now has the appropriate value and type to check against.

This also ensures consistent types throughout your application regardless of where the model is used, and the model becomes the source of truth for the data types because another developer in your team or your future self can simply glance at the $casts definition in the model class and know the data types.

Beyond Booleans: Other Useful Casts

As you may already know, casting isn't just for booleans. Laravel provides several useful cast types:

protected $casts = [
    'enabled' => 'boolean',
    'settings' => 'array',
    'last_used_at' => 'datetime',
    'price' => 'decimal:2',
];

Each cast ensures that your data is consistently formatted regardless of its source, whether it's a database, a Sushi array, or an API response.

Conclusion

Type consistency is critical for building reliable Laravel applications. Eloquent's casting feature provides a clean, centralized way to ensure your models always return the expected data types. When working with Laravel Sushi, proper casting becomes even more important, as you're working with hardcoded array values that might not match your application's expected types.

By leveraging casts appropriately, you can prevent subtle bugs related to type mismatches like the one we discussed in this and make your code more predictable. Next time you're building a Sushi model (or any Eloquent model), take a moment to define your casts – your future self will thank you.

Wanna chat about what you just read, or anything at all? Click here to tweet at me on 𝕏