Laravel Data and Value Objects
Tags: laravel, programming, php, webdev
Published: 2023-09-30 20:39:52

---
canonical_url: https://seankegel.com/laravel-data-and-value-objects
---

Recently, I was presented with a problem using value objects with the [Laravel Data](https://spatie.be/docs/laravel-data/v3/introduction) package by Spatie. I have been trying to use value objects a lot more in my code for things like money, emails, phone numbers, etc. When I am working with data from an external API, it is very helpful to convert this data to value objects when I can.

If you haven’t been using value objects or data transfer objects, here’s some helpful articles to learn more:

- [Value Objects Everywhere — Martin Joo](https://martinjoo.dev/value-objects-everywhere)
- [Is it a DTO or a Value Object — Matthias Noback](https://matthiasnoback.nl/2022/09/is-it-a-dto-or-a-value-object/)
- [Building Resilient Code: Harnessing the Power of Value Objects](https://www.conroyp.com/articles/building-resilient-code-harnessing-the-power-of-value-objects)

When using my own custom data transfer objects, I can create `fromArray` and `toArray` methods to automatically instantiate these value objects. However, Laravel Data provides a lot of nice features out of the box that can help reduce some of the boilerplate code in my data transfer objects. The problem is, I didn’t know the best ways to use Laravel Data to instantiate my value objects. I knew of some of the various features of Laravel Data, like casts and transformers, but had never used them, until now.

In my project, I receive order data from an API. The order data that comes into the application might look something like the following:

```json
{
    "id": 123,
    "user_id": 345,
    "product_id": 678,
    "amount": "10.99",
    "status": "success",
    "processed_at": "2023-09-30T10:00:00+00:00",
    "created_at": "2023-09-28T10:00:00+00:00",
    "updated_at": "2023-09-30T10:00:00+00:00",
}
```

To model this in Laravel Data, I could have a class like the following:

```php
class OrderData extends Data
{
    public function __construct(
        public int $id,
        public int $user_id,
        public int $product_id,
        public string $amount,
        public string $status,
        public string $processed_at,
        public string $created_at,
        public string $updated_at,
    ) {}
}
```

This will map the data from the API fine, but it can be a lot better. The first things that jumps out to me is the `amount` comes in as a string. In my application, I typically prefer to deal with monetary values as cents using integers. However, maybe I have another external service that is expecting monetary values to be passed as a float. This is a great case for using a value object so I am not constantly doing these conversions all over the application.

Here’s a simple example of what my `Currency` value object might look:

```php
class Currency
{
    public readonly string $display;
    public readonly int $cents;
    public readonly float $dollars;

    public function __construct(
        public readonly mixed $value,
    )
    {
        match (true) {
            is_int($value) => $this->cents = $value,
            is_float($value) => $this->cents = $this->floatToCents($value),
            is_string($value) => $this->cents = $this->stringToCents($value),
            default => throw new InvalidArgumentException('Invalid value for Currency'),
        };

        $this->dollars = $this->cents / 100;
        $this->display = number_format($this->dollars, 2);
    }

    private function floatToCents(float $value): int
    {
        return (int) (round($value, 2) * 100);
    }

    private function stringToCents(string $value): int
    {
        return $this->floatToCents((float) $value);
    }
}
```

The `Currency` class can accept an integer, string, or float value, and convert as needed into a cents integer. However, it also gives me the option to get a dollars float value or even a display string. It also has some built-in validation to make sure any other value that might be passed into this class will throw an exception. This is just a simple example and in a normal application, you might also be tracking the type of currency or need some additional validation, but this will work for my purposes right now.

So now, I can update my `OrderData` class to the following:

```php
class OrderData extends Data
{
    public function __construct(
        public int $id,
        public int $user_id,
        public int $product_id,
        public Currency $amount,
        public string $status,
        public string $processed_at,
        public string $created_at,
        public string $updated_at,
    ) {}
}
```

Now the `$amount` is a `Currency` type. However, Laravel Data does not know how to actually instantiate this object. This is where a cast comes into play. A cast in Laravel Data is used to convert simple API data into a complex object. To create this in Laravel Data, I need a class that implements the `Spatie\LaravelData\Casts\Cast` interface, which looks like the following:

```php
interface Cast
{
    public function cast(DataProperty $property, mixed $value, array $context): mixed;
}
```

The `$property` parameter is an object that represents the property on the Laravel Data object and stores various information about the property. You can read more [here](https://spatie.be/docs/laravel-data/v3/advanced-usage/internal-structures#content-dataproperty). The `$value` parameter is the value that is being passed into the Laravel data object for the property, in my case, this will be the money string `"10.99"`. Finally, the `$context` array is an array of the rest of the data being passed into the data object.

A cast implementation for my `Currency` object looks like the following:

```php
class CurrencyCast implements Cast
{
    public function cast(DataProperty $property, mixed $value, array $context): Currency
    {
        return new Currency($value);
    }
}
```

Pretty simple right? I just need to return a new Currency object by passing the $value to it. To make this work with my data object, I can use a property attribute:

```php
class OrderData extends Data
{
    public function __construct(
        public int $id,
        public int $user_id,
        public int $product_id,
        #[WithCast(CurrencyCast::class)]
        public Currency $amount,
        public string $status,
        public string $processed_at,
        public string $created_at,
        public string $updated_at,
    ) {}
}
```

Now, any time my `OrderData` object is created, instead of just having a string value for `$amount`, I now have a much more helpful `Currency` object.

This can still be improved though! This data object has three different date strings and I’d really prefer to use those as a `Carbon` object in Laravel. You can actually think of a `Carbon` date as a value object and I want to cast my various dates to that. The good news, this comes out of the box in Laravel Data, all I need to do is update the types in my object.

```php
class OrderData extends Data
{
    public function __construct(
        public int $id,
        public int $user_id,
        public int $product_id,
        #[WithCast(CurrencyCast::class)]
        public Currency $amount,
        public string $status,
        public Carbon $processed_at,
        public Carbon $created_at,
        public Carbon $updated_at,
    ) {}
}
```

Now, if I create a new data object, I have `Carbon` instances instead of strings.

```php
$data = OrderData::from([
    'id' => 123,
    'user_id' => 345,
    'product_id' => 678,
    'amount' => "10.99",
    'status' => "success",
    'processed_at' => '2023-09-30T10:00:00+00:00',
    'created_at' => '2023-09-28T10:00:00+00:00',
    'updated_at' => '2023-09-30T10:00:00+00:00',
]);

$data->processed_at::class;
// "Carbon\Carbon"
```

You might be wondering how this works since I didn’t use a cast anywhere. Like I mentioned, this is built-in with Laravel Data and it is handled in the configuration file using a global cast.

```php
// /app/config/data.php

return [
    ...
    /*
     * Global casts will cast values into complex types when creating a data
     * object from simple types.
     */
    'casts' => [
        DateTimeInterface::class => Spatie\LaravelData\Casts\DateTimeInterfaceCast::class,
        BackedEnum::class => Spatie\LaravelData\Casts\EnumCast::class,
    ],
    ...
];
```

When Laravel Data runs across a complex type, it will first check if a `Cast` has been configured in the object definition, and if not, it will attempt to fallback to the global casts. For `Carbon`, this is the `DateTimeInterfaceCast`. If Laravel Data sees a property that has a type that implements the `DateTimeInterface`, which `Carbon` does, it will attempt to cast the value of that property to the type specified.

Now, imagine I have many other data transfer objects that might contain monetary values, which could be integers, strings, or floats. Instead of explicitly adding the cast attribute in each data transfer object, it can instead be added to the global casts array.

```php
return [
  ...
  /*
   * Global casts will cast values into complex types when creating a data
   * object from simple types.
   */
  'casts' => [
      DateTimeInterface::class => Spatie\LaravelData\Casts\DateTimeInterfaceCast::class,
      BackedEnum::class => Spatie\LaravelData\Casts\EnumCast::class,
      \App\ValueObjects\Currency::class => \App\Data\Casts\CurrencyCast::class,
  ],
  ...
];
```

With the global cast set, the `OrderData` object no longer needs the cast attribute:

```php
class OrderData extends Data
{
    public function __construct(
        public int $id,
        public int $user_id,
        public int $product_id,
        public Currency $amount,
        public string $status,
        public Carbon $processed_at,
        public Carbon $created_at,
        public Carbon $updated_at,
    ) {}
}
```

Though not necessarily a value object, the `$status` can also be improved here. Let’s say status can be one of three values, “pending”, “success”, or “failed”. This is a perfect case for an enum in PHP which could look like the following:

```php
enum OrderStatus: string
{
    case PENDING = 'pending';
    case SUCCESS = 'success';
    case FAILED = 'failed';
}
```

To handle this in the Laravel Data object, I just need to update the type:

```php
class OrderData extends Data
{
    public function __construct(
        public int $id,
        public int $user_id,
        public int $product_id,
        public Currency $amount,
        public OrderStatus $status,
        public Carbon $processed_at,
        public Carbon $created_at,
        public Carbon $updated_at,
    ) {}
}
```

Similar to the `Carbon` casts, Laravel Data has built in support for casting to enums using `Spatie\LaravelData\Casts\EnumCast::class`.

I’ve covered using casts in Laravel Data, now I will move on to transformers. A transformer is essentially the opposite of a cast. A transformer takes a complex object and converts it to simple values to pass to JSON.

In my `OrderData` example, if I wanted to pass the data to another API, I probably don’t want to pass `Currency` or `Carbon` objects. When I convert my `OrderData` instance to JSON, I get something like the following:

```json
{
    "id": 123,
    "user_id": 345,
    "product_id": 678,
    "amount": {
        "display": "$10.99",
        "cents": 1099,
        "dollars": 10.99,
        "value": "10.99"
    },
    "status": "success",
    "processed_at": "2023-09-30T10:00:00+00:00",
    "created_at": "2023-09-28T10:00:00+00:00",
    "updated_at": "2023-09-30T10:00:00+00:00"
}
```

Some good news and bad news. Like the built-in casts, Laravel Data has built-in transformers for `BackedEnum` and `DateTimeInterface` objects, so my `$status` field and various date fields have been converted to strings. However, my `$amount` field is incompatible with the API I am calling. I need that data back into a string, so I need a custom transformer class.

To create the transformer, I need to use the `Spatie\LaravelData\Transformers\Transformer` interface:

```php
interface Transformer
{
    public function transform(DataProperty $property, mixed $value): mixed;
}
```

So, for my `Currency` object, a transformer could look like the following:

```php
class CurrencyTransformer implements Transformer
{
    public function transform(DataProperty $property, mixed $value): string
    {
        return $value->display;
    }
}
```

With that in place, I can add an attribute to my `OrderData` class.

```php
class OrderData extends Data
{
    public function __construct(
        public int $id,
        public int $user_id,
        public int $product_id,
        #[WithTransformer(CurrencyTransformer::class)]
        public Currency $amount,
        public OrderStatus $status,
        public Carbon $processed_at,
        public Carbon $created_at,
        public Carbon $updated_at,
    ) {}
}
```

Now, when converting to JSON, my output looks like the following:

```json
{
    "id": 123,
    "user_id": 345,
    "product_id": 678,
    "amount": "10.99",
    "status": "success",
    "processed_at": "2023-09-30T10:00:00+00:00",
    "created_at": "2023-09-28T10:00:00+00:00",
    "updated_at": "2023-09-30T10:00:00+00:00"
}
```

Just like global casts, global transformers can be configured as well.

I hope this article was helpful in learning how to use casts and transformers in Laravel Data to work with value objects. Refer to the documentation for more information:

- [Creating a transformer | laravel-data](https://spatie.be/docs/laravel-data/v3/advanced-usage/creating-a-transformer)
- [Creating a cast | laravel-data](https://spatie.be/docs/laravel-data/v3/advanced-usage/creating-a-cast)

Laravel Data is an extremely useful package and is very flexible to support whatever needs may arise. To learn more, I recommend looking into [pipelines](https://spatie.be/docs/laravel-data/v3/advanced-usage/pipeline) for Laravel Data as a next step.

Thanks for reading!