A data transfer object inspired by Rust's serde

Overview

Data Transfer Object

Want to deserialize an object with data on the fly? Go for it by using the From trait.


How is this package any different from spaties popular data-transfer-object, you may ask? Well, it's not meant to be a replacement by any means. But while using it I've often come across some things I've missed since I knew them from serde, like renaming and ignoring properties, something that spatie's data-transfer-object might not get in the near future. So there it is, my own little DTO package :) I hope it helps someone, as it helps me in my daily work. Feel free to open issues or pull requests - any help is greatly appreciated!

Requirements

This package is designed for PHP ≥ 8.0 only since it's using PHP 8.0 Attributes.

Attributes

Name

You get a parameter which is not named as the parameter in your class? #[Name(...)] to the rescue - just specify the name from the Request:

use Dgame\DataTransferObject\Annotation\Name;
use Dgame\DataTransferObject\From;

final class Limit
{
    use From;

    public int $offset;
    #[Name('size')]
    public int $limit;
}

Now the key size will be mapped to the property $limit - but keep in mind: the name limit is no longer known since you overwrote it with size. If that is not your intention, take a look at the Alias Attribute.

Alias

You get a parameter which is not always named as the parameter in your class? #[Alias(...)] can help you - just specify the alias from the Request:

use Dgame\DataTransferObject\Annotation\Alias;
use Dgame\DataTransferObject\From;

final class Limit
{
    use From;

    public int $offset;
    #[Alias('size')]
    public int $limit;
}

Now the keys size and limit will be mapped to the property $limit. You can mix #[Name(...)] and #[Alias(...)] as you want:

use Dgame\DataTransferObject\Annotation\Alias;
use Dgame\DataTransferObject\Annotation\Name;
use Dgame\DataTransferObject\From;

final class Foo
{
    use From;

    #[Name('a')]
    #[Alias('z')]
    public int $id;
}

The keys a and z are mapped to the property id - but not the key id since you overwrote it with a. But the following

use Dgame\DataTransferObject\Annotation\Alias;
use Dgame\DataTransferObject\From;

final class Foo
{
    use From;

    #[Alias('a')]
    #[Alias('z')]
    public int $id;
}

will accept the keys a, z and id.

Call

You want to call a function or method before the value is assigned? No problem with #[Call(<method>, <class>)]. If you don't specify a method but just a class, the __invoke method is the default.

use Dgame\DataTransferObject\Annotation\Call;
use Dgame\DataTransferObject\From;

final class Foo
{
    use From;

    #[Call(class: self::class, method: 'toInt')]
    public int $id;

    public static function toInt(string|int|float|bool $value): int
    {
        return (int) $value;
    }
}

$foo = Foo::from(['id' => '43']);

Validation

You want to validate the value before it is assigned? We can do that. There are a few pre-defined validations prepared, but you can easily write your own by implementing the Validation-interface.

Min

use Dgame\DataTransferObject\Annotation\Min;
use Dgame\DataTransferObject\From;

final class Limit
{
    use From;
    
    #[Min(0)]
    public int $offset;
    #[Min(0)]
    public int $limit;
}

Both $offset and $limit must be at least have the value 0 (so they must be positive-integers). If not, an exception is thrown. You can configure the message of the exception by specifying the message parameter:

use Dgame\DataTransferObject\Annotation\Min;
use Dgame\DataTransferObject\From;

final class Limit
{
    use From;
    
    #[Min(0, message: 'Offset must be positive!')]
    public int $offset;
    #[Min(0, message: 'Limit must be positive!')]
    public int $limit;
}

Max

use Dgame\DataTransferObject\Annotation\Max;
use Dgame\DataTransferObject\From;

final class Limit
{
    use From;
    
    #[Max(1000)]
    public int $offset;
    #[Max(1000)]
    public int $limit;
}

Both $offset and $limit may not exceed 1000. If they do, an exception is thrown. You can configure the message of the exception by specifying the message parameter:

use Dgame\DataTransferObject\Annotation\Max;
use Dgame\DataTransferObject\From;

final class Limit
{
    use From;
    
    #[Max(1000, message: 'Offset may not be larger than 1000')]
    public int $offset;
    #[Max(1000, message: 'Limit may not be larger than 1000')]
    public int $limit;
}

Instance

Do you want to make sure that a property is an instance of a certain class or that each item in an array is an instance of that said class?

use Dgame\DataTransferObject\Annotation\Instance;

final class Collection
{
    #[Instance(class: Entity::class, message: 'We need an array of Entities!')]
    private array $entities;
}

Type

If you are trying to cover objects or other class instances, you should probably take a look at Instance.

As long as you specify a type for your properties, the Type validation is automatically added to ensure that the specified values can be assigned to the specified types. If not, a validation exception will be thrown. Without this validation, a TypeError would be thrown, which may not be desirable.

So this code

final class Foo
{
    private ?int $id;
}

is actually seen as this:

use Dgame\DataTransferObject\Annotation\Type;

final class Foo
{
    #[Type(name: '?int')]
    private ?int $id;
}

The following snippets are equivalent to the snippet above:

use Dgame\DataTransferObject\Annotation\Type;

final class Foo
{
    #[Type(name: 'int|null')]
    private ?int $id;
}
use Dgame\DataTransferObject\Annotation\Type;

final class Foo
{
    #[Type(name: 'int', allowsNull: true)]
    private ?int $id;
}

If you want to change the exception message, you can do so using the message parameter:

use Dgame\DataTransferObject\Annotation\Type;

final class Foo
{
    #[Type(name: '?int', message: 'id is expected to be int or null')]
    private ?int $id;
}

Custom

Do you want your own Validation? Just implement the Validation-interface:

use Dgame\DataTransferObject\Annotation\Validation;
use Dgame\DataTransferObject\From;

#[Attribute(Attribute::TARGET_PROPERTY)]
final class NumberBetween implements Validation
{
    public function __construct(private int|float $min, private int|float $max)
    {
    }

    public function validate(mixed $value): void
    {
        if (!is_numeric($value)) {
            throw new InvalidArgumentException(var_export($value, true) . ' must be a numeric value');
        }

        if ($value < $this->min) {
            throw new InvalidArgumentException(var_export($value, true) . ' must be >= ' . $this->min);
        }

        if ($value > $this->max) {
            throw new InvalidArgumentException(var_export($value, true) . ' must be <= ' . $this->max);
        }
    }
}

final class ValidationStub
{
    use From;

    #[NumberBetween(18, 125)]
    private int $age;

    public function getAge(): int
    {
        return $this->age;
    }
}

Ignore

You don't want a specific key-value to override your property? Just ignore it:

use Dgame\DataTransferObject\Annotation\Ignore;
use Dgame\DataTransferObject\From;

final class Foo
{
    use From;

    #[Ignore]
    public string $uuid = 'abc';
    public int $id = 0;
}

$foo = Foo::from(['uuid' => 'xyz', 'id' => 42]);
echo $foo->id; // 42
echo $foo->uuid; // abc

Reject

You want to go one step further than simply ignoring a value? Then Reject it:

use Dgame\DataTransferObject\Annotation\Reject;
use Dgame\DataTransferObject\From;

final class Foo
{
    use From;

    #[Reject(reason: 'The attribute "uuid" is not supposed to be set')]
    public string $uuid = 'abc';
}

$foo = Foo::from(['id' => 42]); // Works fine
echo $foo->id; // 42
echo $foo->uuid; // abc

$foo = Foo::from(['uuid' => 'xyz', 'id' => 42]); // throws 'The attribute "uuid" is not supposed to be set'

Required

Normally, a nullable-property or a property with a provided default value is treated with said default-value or null if the property cannot be assigned from the provided data. If no default-value is provided and the property is not nullable, an error is thrown in case the property was not found. But in some cases you might want to specify the reason, why the property is required or even want to require an otherwise default-able property. You can do that by using Required:

use Dgame\DataTransferObject\Annotation\Required;
use Dgame\DataTransferObject\From;

final class Foo
{
    use From;

    #[Required(reason: 'We need an "id"')]
    public ?int $id;
    
    #[Required(reason: 'We need a "name"')]
    public string $name;
}

Foo::from(['id' => 42, 'name' => 'abc']); // Works
Foo::from(['name' => 'abc']); // Fails but would work without the `Required`-Attribute since $id is nullable
Foo::from(['id' => 42]); // Fails and would fail regardless of the `Required`-Attribute since $name is not nullable and has no default-value - but the reason why it is required is now more clear.

Property promotion

In the above examples, property promotion is not used because it is more readable that way, but property promotion is supported. So the following example

use Dgame\DataTransferObject\Annotation\Min;
use Dgame\DataTransferObject\From;

final class Limit
{
    use From;
    
    #[Min(0)]
    public int $offset;
    #[Min(0)]
    public int $limit;
}

can be rewritten as shown below

use Dgame\DataTransferObject\Annotation\Min;
use Dgame\DataTransferObject\From;

final class Limit
{
    use From;

    public function __construct(
        #[Min(0)] public int $offset,
        #[Min(0)] public int $limit
    ) { }
}

and it still works.

Nested object detection

You have nested objects and want to deserialize them all at once? That is a given:

use Dgame\DataTransferObject\From;

final class Bar
{
    public int $id;
}

final class Foo
{
    use From;
    
    public Bar $bar;
}

$foo = Foo::from(['bar' => ['id' => 42]]);
echo $foo->bar->id; // 42

Have you noticed the missing From in Bar? From is just a little wrapper for the actual DTO. So your nested classes don't need to use it at all.

There is no limit to the depth of nesting, the responsibility is yours! :)

You might also like...
Developer-friendly framework heavily inspired by Laravel and based on Timber and Wpemerge solutions for WordPress themes development with Bedrock folder structure

Lightweight Brocooly (Brocket) Improved Controllers, Middleware and Routers, but slightly less powerful Container Open beta. Package in development Cr

this starter kite inspired by laravel & Geo and mvc pattern. it's wrap for Wordpress built in classes.

WordpressStarterKite Introduction Built With Prerequisite Directory Structure Guidelines Getting Started Authors Introduction this starter kite inspir

jq as a service. Inspired by a tweet.

jqaas jq as a service. Inspired by a tweet. usage For when you have to do some JSON mangling and you want a web server to do it for you. Supports both

A news website inspired from IEEE and MIT CSAIL's homepage

News-Website I made this website as a side project in April 2022. The goal was to learn PHP and implement backend features to a scientific news websit

A simple ByteBuffer implementation for PHP (Node.js inspired)

Bytebuffer A simple ByteBuffer implementation for PHP (Node.js inspired) Installation composer require labalityowo/bytebuffer:dev-stable Why? I made t

Laravel style FormRequests for Symfony; inspired by adamsafr/form-request-bundle

Somnambulist Form Request Bundle An implementation of form requests from Laravel for Symfony based on the original work by Adam Sapraliev. Requirement

High performance view templating API for PHP applications using tags & expressions inspired by Java JSTL and C compiler

View Language API Table of contents: About Expressions Tags Configuration Compilation Installation Unit Tests Examples Reference Guide About This API

Psl is a standard library for PHP, inspired by hhvm/hsl
Psl is a standard library for PHP, inspired by hhvm/hsl

📚 PHP Standard Library - a modern, consistent, centralized, well-typed, non-blocking set of APIs for PHP programmers

Data visualization for NASA's DSNNow public data
Data visualization for NASA's DSNNow public data

DSN Monitor Data visualization for NASA's DSNNow public data. A live version of the project can be accessed at http://dsnmonitor.ddns.net. Description

Comments
  • Drop In replacement for spatie/data-transfer-object

    Drop In replacement for spatie/data-transfer-object

    As the spatie package is deprecated do you think you could modify your package to be used as drop in replacement?

    I really like your approach but I am missing some functionality as all, except, only, toArray etc.

    BR wucherpfennig

    opened by wucherpfennig 2
Releases(v0.4.0)
Owner
Randy Schütt
Randy Schütt
Creating data transfer objects with the power of php objects. No php attributes, no reflection api, and no other under the hook work.

Super Simple DTO Creating data transfer objects with the power of php objects. No php attributes, no reflection api, and no other under the hook work.

Mohammed Manssour 8 Jun 8, 2023
An article about alternative solution for convert object into a JSON Object for your api.

Do we really need a serializer for our JSON API? The last years I did build a lot of JSON APIs but personally was never happy about the magic of using

Alexander Schranz 1 Feb 1, 2022
Your alter ego object. Takes the best of object and array worlds.

Supporting Opensource formapro\values is an MIT-licensed open source project with its ongoing development made possible entirely by the support of com

FormaPro 31 Jun 25, 2021
Configure Magento 2 to send email using Google App, Gmail, Amazon Simple Email Service (SES), Microsoft Office365 and many other SMTP (Simple Mail Transfer Protocol) servers

Magento 2 SMTP Extension - Gmail, G Suite, Amazon SES, Office 365, Mailgun, SendGrid, Mandrill and other SMTP servers. For Magento 2.0.x, 2.1.x, 2.2.x

MagePal :: Magento Extensions 303 Oct 7, 2022
Melek Berita Backend is a service for crawling data from various websites and processing the data to be used for news data needs.

About Laravel Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experie

Chacha Nurholis 2 Oct 9, 2022
provides a nested object property based user interface for accessing this configuration data within application code

laminas-config This package is considered feature-complete, and is now in security-only maintenance mode, following a decision by the Technical Steeri

Laminas Project 43 Dec 26, 2022
A small, modern, PSR-7 compatible PSR-17 and PSR-18 network library for PHP, inspired by Go's net package.

Net A small, modern, PSR-7 compatible PSR-17 and PSR-18 network library for PHP, inspired by Go's net package. Features: No hard dependencies; Favours

Minibase 16 Jun 7, 2022
A small, modern, PSR-7 compatible PSR-17 and PSR-18 network library for PHP, inspired by Go's net package.

Net A small, modern, PSR-7 compatible PSR-17 and PSR-18 network library for PHP, inspired by Go's net package. Features: No hard dependencies; Favours

Minibase 16 Jun 7, 2022
Deeper is a easy way to compare if 2 objects is equal based on values in these objects. This library is heavily inspired in Golang's reflect.DeepEqual().

Deeper Deeper is a easy way to compare if 2 objects is equal based on values in these objects. This library is heavily inspired in Golang's reflect.De

Joubert RedRat 4 Feb 12, 2022
Ied plugin composer - Inspired Plugin Composer: Create, publish and edit plugins from within Textpattern CMS.

ied_plugin_composer Create, publish and edit plugins from within Textpattern CMS. Creates a new page under the Extensions tab where you can edit and e

Stef Dawson 8 Oct 3, 2020