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;
}
}
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();
}
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,
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();
}
}
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
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 😬
Top comments (0)