DEV Community

Cover image for Creating casts like Carbon in Laravel
Salih for Kodeas

Posted on

Creating casts like Carbon in Laravel

Most of us using Laravel have used Carbon for date casting since it comes with the package and also simply because it is just awesome.

What if you wanted to create a similar experience for your own custom casts?

So, here is a short guide how to achieve something similar:
(I will use Currency as an example however you can apply this to anything)

Firstly, we will need our trusty type class with a simple constructor and a a couple static initiators (just like Carbon::parse('05/05/22')). Also it would be great to have some getters like toUsd() and toCents() for ease of use.

class Currency
{
    public function __construct(int $amountInCents)
    {
        $this->amountInCents = $amountInCents;
    }

    public static function fromCents(int $amountInCents):Currency
    {
        return new self($amountInCents);
    }

    public static function fromUsd(float|int $amount): Currency
    {
        return new self($amount * 100);
    }

    public function toUsd(): float|int
    {
        return $this->amountInCents / 100;
    }

    public function toCents(): int
    {
        return $this->amountInCents;
    }
}
Enter fullscreen mode Exit fullscreen mode

Now we are able to initiate our class using Currency::fromCents(100), I prefer to store all my currencies in cents since floats are weird in database level but let's not get into the nitty gritty here.

Secondly, we will utilize Laravel's custom casts feature for this

class Currency implements CastsAttributes
{
    /**
     * Cast the given value.
     *
     * @param  Model  $model
     * @param  string  $key
     * @param  mixed  $value
     * @param  array  $attributes
     * @return CurrencyType
     */
    public function get($model, string $key, mixed $value, array $attributes): CurrencyType
    {
        return CurrencyType::fromCents($value);
    }

    /**
     * @param  Model  $model
     * @param  string  $key
     * @param  CurrencyType|string|float|int  $value
     * @param  array  $attributes
     * @return int
     * @throws InvalidCurrencyFormatException
     */
    public function set($model, string $key, mixed $value, array $attributes): int
    {
        if (!($value instanceof CurrencyType)) {
            $currency = CurrencyType::class;
            $type = getType($value);
            throw new InvalidCurrencyFormatException("The given value must be $currency. $type was given.");
        }

        return $value->toCents();
    }
Enter fullscreen mode Exit fullscreen mode

Please have a look at the docs for more info about this step

We now have our shiny Currency cast that we can use on any model like:

'money_column' => Currency::class,
Enter fullscreen mode Exit fullscreen mode

I am sure we are all aware of how magical🪄 casts are but let me outline we achieved here.

  • Whenever you want to store this column you can just use a Currency object and just send it over to the database and it will automatically to store it in cents without questions.

  • Whenever you fetch this record from the database, it will automatically be in the format of Currency and you can use the getters as you wish.

Now you can stop at this point and call it a day, I wouldn't blame you at all but we are here to have the full Carbon style cast experience. So lets go a little bit further, you won't regret it I am sure (I hope you won't).

Our next step is a beautiful detail that most of us take for granted about Carbon.(at least I did until recently)

When a Carbon object is passed to front-end (blade or json) it magically turns into a formatted string instead of an Object.

Let's add this to our Currency class.

use JsonSerializable;

class Currency implements JsonSerializable
{
    protected int $amountInCents;

    public function __construct(int $amountInCents)
    {
        $this->amountInCents = $amountInCents;
    }

    public static function fromCents(int $amountInCents): Currency
    {
        return new self($amountInCents);
    }

    public static function fromUsd(float|int $amount): Currency
    {
        return new self($amount * 100);
    }

    public function toUsd(): float|int
    {
        return $this->amountInCents / 100;
    }

    public function toCents(): int
    {
        return $this->amountInCents;
    }

    public function toReadable(): string
    {
        return number_format($this->toUsd(), 2);
    }

    public function jsonSerialize(): string
    {
        return $this->toReadable();
    }

    public function __toString(): string
    {
        return $this->toReadable();
    }
}
Enter fullscreen mode Exit fullscreen mode

JsonSerializable is a native php interface for more info have a look at php docs

Using _JsonSerializable interface makes sure whenever you class is going through a json_encode() the jsonSerialize( function is used and Laravel almost always runs any response through json_encode() within the ResponseFactory class.
I have to admit it took me way too long to figure this out

I also added the __toString() method so if at any point you would like to use this class as a string you wouldn't need any other helpers.

$currency = Currency::fromUsd(1);
json_encode($currency); //"1.00"
echo $currency; //"1.00"

...
return response()->json(['amount' => $currency]) //would be "1.00" on the front-end
Enter fullscreen mode Exit fullscreen mode

I think that is just elegant, all my worries about storing currency in cents or usd are gone.

If you have any other use-cases for something like this I would love to see it in the comments, I am sure there must be many.

Thanks for reading, drop a ❤️ if you made it all the way down here.

Also let me know in the comments if you have any further improvements to this or found this useful or just plain hate the idea 😬

Oldest comments (0)