PHPackages                             zero-to-prod/service-model - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. zero-to-prod/service-model

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

zero-to-prod/service-model
==========================

A modern approach to extensible, typesafe DTOs with factory support.

v2.10.0(8mo ago)37.8k↓33.3%MITPHPPHP ^8.1CI passing

Since Nov 24Pushed 8mo ago1 watchersCompare

[ Source](https://github.com/zero-to-prod/service-models)[ Packagist](https://packagist.org/packages/zero-to-prod/service-model)[ Docs](https://github.com/zero-to-prod/service-models)[ GitHub Sponsors](https://github.com/zero-to-prod)[ RSS](/packages/zero-to-prod-service-model/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (10)Dependencies (4)Versions (64)Used By (0)

Service Models
==============

[](#service-models)

[![Repo](https://camo.githubusercontent.com/9a90a3efeee26aed7d7f2feee9cd84566a26f9c362cc773b184d076210906e1c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6769746875622d677261793f6c6f676f3d676974687562)](https://github.com/zero-to-prod/service-models)[![Latest Version on Packagist](https://camo.githubusercontent.com/dd7266e61856e599fd8fb36a12bfdf42ad51d7e188b132478b76f6ae3f065a92/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f7a65726f2d746f2d70726f642f736572766963652d6d6f64656c2e737667)](https://packagist.org/packages/zero-to-prod/service-model)[![test](https://github.com/zero-to-prod/service-models/actions/workflows/test.yml/badge.svg)](https://github.com/zero-to-prod/service-models/actions/workflows/test.yml/badge.svg)[![Downloads](https://camo.githubusercontent.com/cd9e8092212d969215e2c87b38894ce8f6612914feec4fc9dc4a25847494ce0a/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f7a65726f2d746f2d70726f642f736572766963652d6d6f64656c2e7376673f7374796c653d666c61742d737175617265292535442868747470733a2f2f7061636b61676973742e6f72672f7061636b616765732f7a65726f2d746f2d70726f642f736572766963652d6d6f64656c26233431)](https://camo.githubusercontent.com/cd9e8092212d969215e2c87b38894ce8f6612914feec4fc9dc4a25847494ce0a/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f7a65726f2d746f2d70726f642f736572766963652d6d6f64656c2e7376673f7374796c653d666c61742d737175617265292535442868747470733a2f2f7061636b61676973742e6f72672f7061636b616765732f7a65726f2d746f2d70726f642f736572766963652d6d6f64656c26233431)[![codecov](https://camo.githubusercontent.com/5583e05b12a7cdc6d52d9e39b3f6325fc94b21be72ee8139dac8bc0bc9fb83c3/68747470733a2f2f636f6465636f762e696f2f67682f7a65726f2d746f2d70726f642f736572766963652d6d6f64656c732f67726170682f62616467652e7376673f746f6b656e3d4133505439333136484f)](https://codecov.io/gh/zero-to-prod/service-models)

Contents
--------

[](#contents)

- [Introduction](#introduction)
- [Requirements](#requirements)
- [Features](#features)
- [Installation](#installation)
- [Documentation Publishing](#documentation-publishing)
    - [Automatic Documentation Publishing](#automatic-documentation-publishing)
- [Usage](#usage)
    - [Setting Up Your Model](#setting-up-your-model)
    - [Accessing Type Safe Properties](#accessing-type-safe-properties)
- [Factory Support](#factory-support)
- [Basic Implementation](#basic-implementation)
- [Native Object Support](#native-object-support)
- [Enums](#enums)
- [Classes](#classes)
    - [Simple Class Casting](#simple-class-casting)
    - [Using a Class Method for Parsing](#using-a-class-method-for-parsing)
    - [One-to-many Class Casting](#one-to-many-casting)
- [Value Casting](#value-casting)
- [One-to-many Casting](#one-to-many-casting)
- [Plugins](#plugins)
- [Factories](#factories)
- [Extending the `ServiceModel` Trait](#extending-the-servicemodel-trait)
- [Mapping](#mapping)
    - [Renaming](#renaming)
    - [Mapping Nested Properties](#mapping-nested-properties)
- [Validation](#validation)
    - [Using the `Strict` Trait](#using-the-strict-trait)
    - [Manually Validating](#manually-validating)
- [Lifecycle Hooks](#lifecycle-hooks)
- [Caching](#caching)
- [Resource Support](#resource-support)
    - [Build Your Own Resource Transformer](#build-your-own-resource-transformer)
- [Upgrading to v2](#upgrading-to-v2)
- [Local Development](./LOCAL_DEVELOPMENT.md)
- [Contributing](#contributing)

Introduction
------------

[](#introduction)

A modern approach to [extensible](#extending-the-servicemodel-trait), [typesafe](#setting-up-your-model) Data Transfer Objects (DTOs) with [factory](#factories) support.

This [zero-dependency](https://raw.githubusercontent.com/zero-to-prod/service-models/master/composer.json) package provides a [way](#getting-started) to [serialize](https://en.wikipedia.org/wiki/Serialization) data into typesafe [DTOs](#setting-up-your-model).

In the [Extract Transform Load](https://en.wikipedia.org/wiki/Extract,_transform,_load) (ETL) process, this package assist in the ***Transformation*** of data into a model.

Requirements
------------

[](#requirements)

- PHP 8.1 or higher.

Features
--------

[](#features)

- **Simple**: Use the `ServiceModel` [trait](#basic-implementation) to automatically map your data.
- **Custom Type Casting**: Define your own value [casters](#value-casting) for infinite control.
- **Plugin Architecture**: Build your own [plugins](#plugins) with PHP Attributes.
- **Nested Relationships**: Easily define [one-to-many](#one-to-many-casting) relationships with native PHP attributes.
- **Mapping**: Rename and [map](#mapping) your data how you please.
- **Validation**: Control when required properties are [validated](#validation).
- **Factory Support**: Use the `factory()` [method](#factories) to make a DTO with default values.
- **Native Object Support**: [Native object support](#native-object-support) for [Enums](#enums)and [Classes](#classes), with no extra steps.
- **Resource Support**: Transform you ServiceModel to an associative array with [snake\_case](#resource-support) keys or implement your own.
- **Fast**: Designed with [performance](#caching) in mind.

Installation
------------

[](#installation)

If upgrading from v1, see the [upgrade guide](#upgrading-to-v2).

Install `Zerotoprod\ServiceModel` via [Composer](https://getcomposer.org/):

```
composer require zero-to-prod/service-model
```

This will add the package to your project's dependencies and create an autoloader entry for it.

Documentation Publishing
------------------------

[](#documentation-publishing)

You can publish this README to your local documentation directory.

This can be useful for providing documentation for AI agents.

This can be done using the included script:

```
# Publish to default location (./docs/zero-to-prod/service-model)
vendor/bin/zero-to-prod-service-model

# Publish to custom directory
vendor/bin/zero-to-prod-service-model /path/to/your/docs
```

### Automatic Documentation Publishing

[](#automatic-documentation-publishing)

You can automatically publish documentation by adding the following to your `composer.json`:

```
{
    "scripts": {
        "post-install-cmd": [
            "zero-to-prod-service-model"
        ],
        "post-update-cmd": [
            "zero-to-prod-service-model"
        ]
    }
}
```

Use the `ServiceModel` trait in your model.

Add properties to your model that match the keys of your data.

```
use Zerotoprod\ServiceModel\ServiceModel;

class Order
{
    use ServiceModel;

    public readonly int $id;
}
```

Pass an associative array or json string to the `make()` method of your [model](#setting-up-your-model).

```
$Order = Order::make(['id' => 1]);
$Order->id; // 1

$Order = Order::make('{"id":1}');
$Order->id; // 1
```

Use the `factory()` method to make a new [model](#setting-up-your-model) with default values.

See the [Factories](#factories) section for more information.

```
$order = Order::factory()->make();
$order->id; // 1
```

Usage
-----

[](#usage)

Create a `ServiceModel` by passing an associative array or json string to the `make()` method of your model that uses the `ServiceModel` trait.

```
$Order = Order::make(['id' => 1]);
$Order->id; // 1

$Order = Order::make('{"id":1}');
$Order->id; // 1
```

### Setting Up Your Model

[](#setting-up-your-model)

Define properties in your class to match the keys of your data.

The `ServiceModel` trait will automatically match the keys, detect the type, and cast the value.

```
use Zerotoprod\ServiceModel\Attributes\Cast;
use Zerotoprod\ServiceModel\Attributes\CastUsing;
use Zerotoprod\ServiceModel\Attributes\MapFrom;
use Zerotoprod\ServiceModel\Attributes\ArrayOf;
use Zerotoprod\ServiceModel\ServiceModel;

class Order
{
    use ServiceModel;

    /**
     * Automatically cast OrderDetails to a model by using
     * the ServiceModel trait in OrderDetails.
     */
    public readonly OrderDetails $details;

    /**
     * Define you own value caster.
     */
     #[Cast(ToJson::class)]
    public readonly string $metadata;

    /**
     * If you have a class the can cast the value,
     * you can use the CastUsing attribute to
     * define the method to pass values to.
     */
    #[CastUsing('set')]
    public readonly TimeClass $time;

    /**
     * Rename your values.
     */
    #[MapFrom('AcknowledgedAt')]
    public readonly string $acknowledged_at;

    /**
     * Remap your values.
     */
    #[MapFrom('vendor_details.serial_number')]
    public readonly string $serial_number;

    /**
     * Use a value-backed enum to automatically cast the value.
     */
    public readonly Status $status;

    /**
     * Casts to an array of enums.
     * @var Tag[] $tags
     */
    #[ArrayOf(Tag::class)]
    public readonly array $tags;

    /**
     * Unpacks the array into the constructor of the type-hinted class.
     * NOTE: PickupInfo does not use the ServiceModel trait.
     */
    public readonly PickupInfo $PickupInfo;

    /**
     * Casts to an array of PickupInfo.
     * NOTE: PickupInfo does not use the ServiceModel trait.
     *
     * @var PickupInfo[] $previous_pickups
     */
    #[ArrayOf(PickupInfo::class)]
    public readonly array $previous_pickups;

    /**
     * Because Carbon uses the static method `parse`, this will
     * cast the value to a Carbon instance for free.
     */
    public readonly Carbon $created_at;

    /**
     * Creates an array of Items.
     * @var Item[] $items
     */
    #[ArrayOf(Item::class)]
    public readonly array $items;

    /**
     * Use a custom cast.
     * @var Collection $views
     */
    #[CollectionOf(View::class)]
    public readonly Collection $views;
}
```

Pass an associative array or json string to the `make()` method of your model.

```
$order = Order::make([
    'details' => ['id' => 1, 'name' => 'Order 1'],
    'metadata' => ['id' => 1, 'name' => 'Order 1'],
    'time' => '2021-01-01 00:00:00',
    'vendor_details' => ['serial_number' => '123456789'],
    'status' => 'pending',
    'tags' => ['important', 'rush'],
    'PickupInfo' => ['location' => 'Location 1', 'time' => '2021-01-01 00:00:00'],
    'created_at' => '2021-01-01 00:00:00',
    'items' => [
        ['id' => 1,'name' => 'Item 1'],
        ['id' => 2,'name' => 'Item 2']],
    'views' => [
        ['id' => 1,'name' => 'View 1'],
        ['id' => 2,'name' => 'View 2']],
]);
```

### Accessing Type Safe Properties

[](#accessing-type-safe-properties)

Access your data with arrow syntax.

```
// Nested Models
$details = $order->details; // Order::class
$details = $order->details->name; // 'Order 1'

// Custom Casters
$metadata = $order->metadata; // '{"id":1,"name":"Order 1"}'

// CastUsing
$time = $order->time; // TimeClass::class
$time = $order->time->value; // '2021-01-01 00:00:00'

// Remapped Properties
$serial_number = $order->serial_number; // '123456789'

// Enums
$status = $order->status; // Status::pending
$status = $order->status->value; // 'pending'

// Array of Enums
$tags = $order->tags[0]; // Tag::important
$tags = $order->tags[0]->value; // 'important'

// Value Casting
$created_at = $order->created_at; // Carbon::class
$created_at = $order->created_at->toDateTimeString(); // '2021-01-01 00:00:00'

// One-to-many array Casting
$item_id = $order->items[0]; // Item::class
$item_id = $order->items[0]->id; // 1

// One-to-many custom Casting
$view_name = $order->views->first(); // View::class
$view_name = $order->views->first()->name; // 'View 1'
```

Factory Support
---------------

[](#factory-support)

Use the `factory()` method to make a new model instance with default values.

See the [Factories](#factories) section for how to set up and use factories.

```
$order = Order::factory()->make();

$order->status; // Status::pending
```

Basic Implementation
--------------------

[](#basic-implementation)

Define properties in your class to match the keys of your data.

The `ServiceModel` trait will automatically match the keys, detect the type, and cast the value.

```
use Zerotoprod\ServiceModel\ServiceModel;

class Order
{
    use ServiceModel;

    /**
     * Using the `ServiceModel` trait in the child class (OrderDetails)
     * class will automatically instantiate new class.
     */
    public readonly OrderDetails $details;
}
```

> IMPORTANT: Use the `ServiceModel` trait in the child classes.

```
use Zerotoprod\ServiceModel\ServiceModel;

class OrderDetails
{
    use ServiceModel;

    public readonly int $id;
    public readonly string $name;
}
```

> NOTICE: The `details` key matches the `$details` property in `Order`.

```
$order = Order::make([
    'details' => [
        'id' => 1,
        'name' => 'Order 1'
    ],
]);

// This is also equivalent.
$order = Order::make([
    'details' => OrderDetail::make([
        'id' => 1,
        'name' => 'Order 1'
    ]),
]);

$order->details->id; // 1
$order->details->name; // 'Order 1'
```

Native Object Support
---------------------

[](#native-object-support)

This package provides native support for the following objects:

- [Enums](#enums)
- [Classes](#classes)

### Enums

[](#enums)

Use a value backed enum to automatically cast the value.

```
use Zerotoprod\ServiceModel\ServiceModel;

class Order
{
    use ServiceModel;

    /**
     * Casts to a Status enum
     */
    public readonly Status $status;

    /**
     * Casts to an array of Status enum.
     * @var Status[] $statuses
     */
    #[CastToArray(Status::class)]
    public readonly array $statuses;
}
```

```
enum Status: string
{
    case pending = 'pending';
    case completed = 'completed';
}
```

```
$order = Order::make([
    'status' => 'pending',
    'statuses' => ['pending', 'completed'],
]);

$order->status; // Status::pending
$order->status->value; // 'pending'

$order->statuses[0]; // Status::pending
$order->statuses[1]->value; // completed
```

Classes
-------

[](#classes)

Sometimes you may want to cast to a class you cannot use the `ServiceModel` trait in.

For a simple cast, simply typehint with the property with the class. This will automatically unpack the array into the constructor of the class.

For a `one-to-many` cast, use the `CastToArray` attribute to cast an array of classes.

#### Simple Class Casting

[](#simple-class-casting)

Simply typehint with the property with the class you want to cast to.

```
use Zerotoprod\ServiceModel\ServiceModel;

class Order
{
    use ServiceModel;

    /**
     * Unpacks the array into the constructor of the type-hinted class.
     */
    public readonly PickupInfo $pickups;
}
```

```
class PickupInfo
{
    public function __construct(public readonly string $location, public readonly string $time)
    {
    }
}
```

```
$order = Order::make([
    'pickups' => [
        'location' => 'Location 1',
        'time' => '2021-01-01 00:00:00',
    ]
]);

$order->pickups; // PickupInfo::class
$order->pickups->location; // Location 1
$order->pickups->time; // 2021-01-01 00:00:00
```

#### Using a Class Method for Parsing

[](#using-a-class-method-for-parsing)

In some cases, you might have a class with a method that accepts a value and returns an instance of the class itself.

The `ServiceModel` package allows you to leverage such methods for parsing values.

You can specify the method to be used for parsing by applying the `CastUsing` attribute to the property in your model. The attribute takes the name of the method as its argument.

Here's an example:

```
use Zerotoprod\ServiceModel\Attributes\CastUsing;
use Zerotoprod\ServiceModel\ServiceModel;

class MyModel
{
    use ServiceModel;

    // The 'set' method of the TimeClass will be used for parsing the value
    #[CastUsing('set')]
    public readonly TimeClass $time;
}
```

In the `TimeClass` below, the `set` method accepts a value, assigns it to the `value` property of a new `TimeClass`instance, and then returns the instance:

```
class TimeClass
{
    public string $value;

    public static function set($value): self
    {
        $self = new self();
        $self->value = $value;

        return $self;
    }
}
```

In this exampleWhen the `ServiceModel` trait processes the `time` property of the `MyModel`, it will invoke the `set` method of the `TimeClass`, passing the value to be parsed. The method will return a `TimeClass` instance, which will then be assigned to the `time` property.

#### One-to-many Class Casting

[](#one-to-many-class-casting)

Sometimes you may want to cast an array of classes you cannot use the `ServiceModel` trait in.

Use the `ArrayOf` attribute to cast an array of classes.

```
use Zerotoprod\ServiceModel\Attributes\ArrayOf;
use Zerotoprod\ServiceModel\ServiceModel;

class Order
{
    use ServiceModel;

    /**
     * Casts to an array of PickupInfo.
     * @var PickupInfo[] $pickups
     */
    #[ArrayOf(PickupInfo::class)]
    public readonly array $pickups;
}
```

```
class PickupInfo
{
    public function __construct(public readonly string $location, public readonly string $time)
    {
    }
}
```

```
$order = Order::make([
    'pickups' => [
        [
            'location' => 'Location 1',
            'time' => '2021-01-01 00:00:00',
        ],
        [
            'location' => 'Location 2',
            'time' => '2021-01-01 00:00:00',
        ],
    ],
]);

$order->pickups[0]->location; // Location 1
$order->pickups[0]->time; // 2021-01-01 00:00:00
```

Value Casting
-------------

[](#value-casting)

Implement the `CanCast` interface to make a custom type.

```
use Zerotoprod\ServiceModel\ServiceModel;

class Order
{
    use ServiceModel;

    /**
     * Transforms the value to a custom instance.
     */
    #[Cast(ToCustomTime::class)]
    public readonly ToCustomTime $ordered_at;

    /**
     * Because Carbon uses the static method `parse`, this will
     * cast the value to a Carbon instance for free.
     */
    public readonly Carbon $created_at;
```

```
use Zerotoprod\ServiceModel\Contracts\CanParse;

class ToCustomTime implements CanParse
{
    public function parse(array $values): Carbon
    {
        return ToCustomTime::parse($values[0]);
    }
}
```

```
$order = Order::make([
    'ordered_at' => '2021-01-01 00:00:00',
    'created_at' => '2021-01-01 00:00:00',
]);

$order->ordered_at; // Carbon::class
$order->ordered_at->toDateTimeString(); // '2021-01-01 00:00:00'

$order->created_at; // Carbon::class
$order->created_at->toDateTimeString(); // '2021-01-01 00:00:00'
```

One-to-many Casting
-------------------

[](#one-to-many-casting)

Use the `CastToArray` attribute to cast an array of classes.

```
use Zerotoprod\ServiceModel\ServiceModel;
use Illuminate\Support\Collection;

class Order
{
    use ServiceModel;

    /**
     * Casts to a Collection containing View classes.
     * @var Collection $views
     */
    #[CollectionOf(View::class)]
    public Collection $views;
}
```

> IMPORTANT: The class name passed in the Attribute (`View::class`) is passed in the constructor of the `CollectionOf` class.

> IMPORTANT: Don't forget to add `#[Attribute]` to the top of your class.

```
use Zerotoprod\ServiceModel\Contracts\CanParse;

#[Attribute]
class CollectionOf implements CanParse
{
    public function __construct(public readonly string $class)
    {
    }

    public function parse(array $values): Collection
    {
        return collect($values)->map(fn(array $item) => $this->class::make($item));
    }
}
```

```
$order = Order::make([
    'views' => [
        [
            'id' => 1,
            'name' => 'View 1'
        ],
        [
            'id' => 2,
            'name' => 'View 2'
        ]
    ],
]);

$order->views->first(); // View::class
$order->views->first()->name; // 'View 1'
```

Plugins
-------

[](#plugins)

You can define your own attributes to extend the functionality of the `ServiceModel` trait.

The property values are passed to the `parse()` method of the attribute.

The Attribute values are passed to the constructor of the attribute.

```
use Zerotoprod\ServiceModel\ServiceModel;

class MyModel
{
    use ServiceModel;

    #[CustomCaster(1)]
    public readonly int $add_one;

    #[CustomValueCaster(1, 2)]
    public readonly int $add_two;
}
```

```
$MyModel = MyModel::make(['add_one' => 1, 'add_two' => 1]);
$MyModel->add_one; // 2
$MyModel->add_two; // 4
```

```
use Attribute;
use Zerotoprod\ServiceModel\Contracts\CanParse;

#[Attribute]
class CustomCaster implements CanParse
{
    public function __construct(public readonly int $attribute_constructor_value)
    {
    }

    public function parse(array $values): int
    {
        return $values[0] + $this->attribute_constructor_value;
    }
}
```

```
use Attribute;
use Zerotoprod\ServiceModel\Contracts\CanParse;

#[Attribute]
class CustomValueCaster implements CanParse
{
    public function __construct(public readonly int $value_1, public readonly int $value_2)
    {
    }

    public function parse(array $values): int
    {
        return $values[0] + $this->value_1 + $this->value_2;
    }
}
```

Factories
---------

[](#factories)

Factories provide a convenient way to generate DTOs with default values.

1. Use the `ServiceModel` and the `HasFactory` trait in your model.
2. Create a class that `extends` the `Factory` class for your factory.
3. Set the `public string $model = ` property in your factory pointing to your model.
4. Set the `public static string $factory = ` property in your model pointing to your factory.
5. Return your default values as an array in the `definition()` method in your factory.

```
use Zerotoprod\ServiceModel\HasFactory;
use Zerotoprod\ServiceModel\ServiceModel;

class Order
{
    use ServiceModel;
    use HasFactory;

    public static string $factory = OrderFactory::class;

    public OrderDetails $details;
    public Status $status;
}
```

```
use Zerotoprod\ServiceModel\Factory;

class OrderFactory extends Factory
{
    public string $model = Order::class;

    public function definition(): array
    {
        return [
            'details' => ['id' => 1, 'name' => 'Order 1'],
            'status' => 'pending',
        ];
    }

    public function setStatus(Status $status): self
    {
        return $this->state(fn() => [
            'status' => $status->value
        ]);
    }
}
```

```
$order = Order::factory()->make();
$order->status; // Status::pending
$order->details->name; // 'Order 1'

$order = Order::factory()->setStatus(Status::completed)->make();
$order->status; // Status::completed
```

Extending the `ServiceModel` Trait
----------------------------------

[](#extending-the-servicemodel-trait)

You can extend the `ServiceModel` trait and add your own functionality to your models.

```
