cerbero / dto
Data Transfer Object (DTO)
Fund package maintenance!
cerbero90
Installs: 104 732
Dependents: 2
Suggesters: 0
Security: 0
Stars: 16
Watchers: 3
Forks: 5
Open Issues: 1
Requires
- php: ^7.1||^8.0
Requires (Dev)
- phpunit/phpunit: >=7.0
- squizlabs/php_codesniffer: ^3.0
README
This package was inspired by Lachlan Krautz' excellent data-transfer-object.
A data transfer object (DTO) is an object that carries data between processes. DTO does not have any behaviour except for storage, retrieval, serialization and deserialization of its own data. DTOs are simple objects that should not contain any business logic but rather be used for transferring data.
Install
Via Composer
composer require cerbero/dto
Usage
- Instantiate a DTO
- Declare properties
- Default values
- Interact with values
- Available flags
- Default flags
- Interact with flags
- Manipulate properties
- Interact with properties
- Convert into array
- Listen to events
- Convert into string
Instantiate a DTO
DTOs can be instantiated like normal classes or via the factory method make()
. The parameters are optional and include the data to carry and the flags that dictate how the DTO should behave:
use const Cerbero\Dto\PARTIAL; $data = [ 'name' => 'John', 'address' => [ 'street' => 'King Street', ], ]; $dto = new SampleDto($data, PARTIAL); $dto = SampleDto::make($data, PARTIAL);
In the example above, $data
is an array containing the properties declared in the DTO and PARTIAL
is a flag that let the DTO be instantiated even though it doesn't have all its properties set (we will see flags in more detail later).
Keys in the array $data
can be either snake case or camel case, the proper case is automatically detected to match DTO properties.
Declare properties
Properties can be declared in a DTO by using doc comment tags:
use Cerbero\Dto\Dto; use Sample\Dtos\AddressDto; /** * A sample user DTO. * * @property string $name * @property bool $isAdmin * @property mixed $something * @property \DateTime|null $birthday * @property UserDto[] $friends * @property AddressDto $address */ class UserDto extends Dto { // }
Either @property
or @property-read
can be used, followed by the expected data type and the desired property name. When expecting more than one type, we can separate them with a pipe |
character, e.g. \DateTime|null
.
A collection of types can be declared by adding the suffix []
to the data type, e.g. UserDto[]
. It's important to declare the fully qualified name of classes, either in the doc comment or as a use
statement.
Primitive types can be specified too, e.g. string
, bool
, int
, array
, etc. The pseudo-type mixed
allow any type.
Default values
While values can be set when instatiating a DTO, default values can also be defined in the DTO class:
use Cerbero\Dto\Dto; /** * A sample user DTO. * * @property string $name */ class UserDto extends Dto { protected static $defaultValues = [ 'name' => 'John', ]; } // $user1->name will return: John $user1 = new UserDto(); // $user2->name will return: Jack $user2 = new UserDto(['name' => 'Jack']);
Please note that in the above example default values are overridden by the values passed during the DTO creation.
Interact with values
DTO property values can be accessed in several ways, but a Cerbero\Dto\Exceptions\UnknownDtoPropertyException
is thrown if a requested property is not set:
// as an object $user->address->street; // as an array $user['address']['street']; // via dot notation $user->get('address.street'); // via nested DTO $user->address->get('street');
To check whether properties have a value, the following methods can be called:
// as an object isset($user->address->street); // as an array isset($user['address']['street']); // via dot notation $user->has('address.street'); // via nested DTO $user->address->has('street');
Please note that the above methods will return FALSE also if the property value is set to NULL (just like the default PHP behaviour). To check whether a property has actually been set, we can call $user->hasProperty('address.street')
(we will see properties in more details later).
The outcome of setting a value depends on the flags set in a DTO. DTOs are immutable by default, so a new instance gets created when setting a value. Values can be changed in the same DTO instance only if a MUTABLE
flag is set:
// throw Cerbero\Dto\Exceptions\ImmutableDtoException if immutable $user->address->street = 'King Street'; // throw Cerbero\Dto\Exceptions\ImmutableDtoException if immutable $user['address']['street'] = 'King Street'; // set the new value in the same instance if mutable or in a new instance if immutable $user->set('address.street', 'King Street'); // set the new value in the same instance if mutable or in a new instance if immutable $user->address->set('street', 'King Street');
Same applies when unsetting a value but only PARTIAL
DTOs can have values unset, otherwise a Cerbero\Dto\Exceptions\UnsetDtoPropertyException
is thrown:
// throw Cerbero\Dto\Exceptions\ImmutableDtoException if immutable unset($user->address->street); // throw Cerbero\Dto\Exceptions\ImmutableDtoException if immutable unset($user['address']['street']); // unset the new value in the same instance if mutable or in a new instance if immutable $user->unset('address.street'); // unset the new value in the same instance if mutable or in a new instance if immutable $user->address->unset('street');
Available flags
Flags determine how a DTO behaves and can be set when instantiating a new DTO. They support bitwise operations, so we can combine multiple behaviours via PARTIAL | MUTABLE
.
NONE
The flag Cerbero\Dto\NONE
is simply a placeholder and doesn't alter the behaviour of a DTO in any way.
IGNORE_UNKNOWN_PROPERTIES
The flag Cerbero\Dto\IGNORE_UNKNOWN_PROPERTIES
lets a DTO ignore extra data that is not part of its properties. If this flag is not provided, a Cerbero\Dto\Exceptions\UnknownDtoPropertyException
is thrown when trying to set a property that is not declared.
MUTABLE
The flag Cerbero\Dto\MUTABLE
lets a DTO override its property values without creating a new DTO instance, as DTOs are immutable by default. If not provided, a Cerbero\Dto\Exceptions\ImmutableDtoException
is thrown when trying to alter a property without calling set()
or unset()
, e.g. $dto->property = 'foo'
or unset($dto['property'])
.
PARTIAL
The flag Cerbero\Dto\PARTIAL
lets a DTO be instantiated without some properties. If not provided, a Cerbero\Dto\Exceptions\MissingValueException
is thrown when properties are missing or when unsetting a property.
CAST_PRIMITIVES
The flag Cerbero\Dto\CAST_PRIMITIVES
lets a DTO cast property values if they don't match the expected primitive type. If not provided, a Cerbero\Dto\Exceptions\UnexpectedValueException
is thrown when trying to set a value with a wrong primitive type.
CAMEL_CASE_ARRAY
The flag Cerbero\Dto\CAMEL_CASE_ARRAY
lets all DTO properties preserve their camel case names when a DTO is converted into an array.
Default flags
While flags can be set when instatiating a DTO, default flags can also be defined in the DTO class:
use Cerbero\Dto\Dto; use const Cerbero\Dto\PARTIAL; use const Cerbero\Dto\IGNORE_UNKNOWN_PROPERTIES; use const Cerbero\Dto\MUTABLE; /** * A sample user DTO. * * @property string $name */ class UserDto extends Dto { protected static $defaultFlags = PARTIAL | IGNORE_UNKNOWN_PROPERTIES; } // $user->getFlags() will return: PARTIAL | IGNORE_UNKNOWN_PROPERTIES | MUTABLE $user = UserDto::make($data, MUTABLE);
Default flags are combined with the flags passed during the DTO creation, which means that in the code above $user
has the following flags set: PARTIAL
, IGNORE_UNKNOWN_PROPERTIES
and MUTABLE
.
Interact with flags
Default flags in a DTO can be retrieved by calling the static method getDefaultFlags()
, whilst flags belonging to a DTO instance can be read via getFlags()
:
// PARTIAL | IGNORE_UNKNOWN_PROPERTIES UserDto::getDefaultFlags(); // PARTIAL | IGNORE_UNKNOWN_PROPERTIES | MUTABLE $user->getFlags();
To determine whether a DTO has one or more flag set, we can call hasFlags()
:
$user->hasFlags(PARTIAL); // true $user->hasFlags(PARTIAL | MUTABLE); // true $user->hasFlags(PARTIAL | NULLABLE); // false
DTO flags can be set again by calling the method setFlags()
. If the DTO is mutable the flags are set against the current instance, otherwise a new instance of the DTO is created with the given flags:
$user = $user->setFlags(PARTIAL | NULLABLE);
In case we want to add one or more flags to the already set ones, we can call addFlags()
. If the DTO is mutable the flags are added to the current instance, otherwise they are added to a new instance:
$user = $user->addFlags(CAMEL_CASE_ARRAY | CAST_PRIMITIVES);
Finally to remove flags, we can call removeFlags()
. If the DTO is mutable the flags are removed from the current instance, otherwise they are removed from a new instance:
$user = $user->removeFlags(IGNORE_UNKNOWN_PROPERTIES | MUTABLE);
Please note that when flags are added, removed or set and affect DTO values, properties are re-mapped to apply the effects of the new flags.
Manipulate properties
Along with set()
there are other methods that can be called to manipulate a DTO properties. The method merge()
joins the properties of a DTO with another DTO or anything iterable, e.g. an array:
$user1 = UserDto::make([ 'name' => 'John', 'address' => [ 'street' => 'King Street', ], ], PARTIAL | IGNORE_UNKNOWN_PROPERTIES); $user2 = UserDto::make([ 'name' => 'Anna', 'address' => [ 'unit' => 10, ], ], PARTIAL | CAMEL_CASE_ARRAY); // [ // 'name' => 'Anna', // 'address' => [ // 'street' => 'King Street', // 'unit' => 10, // ], // ] $mergedDto = $user1->merge($user2); // PARTIAL | IGNORE_UNKNOWN_PROPERTIES | CAMEL_CASE_ARRAY $mergedDto->getFlags();
In the example above, the two DTOs are immutable, so another DTO will be created after they merge. If $user1
was mutable, its own properties would have changed without creating a new DTO instance. Please also note that even DTO flags are merged.
In order to let a DTO carry only some specific properties, we can call the only()
method and pass a list of properties to keep:
$result = $user->only(['name', 'address'], CAST_PRIMITIVES);
Any optional flag passed as second parameter will be merged with the existing flags of the DTO. The changes will be applied to a new instance if the DTO is immutable or to the same instance if it is mutable.
The only()
method has also an opposite method called except
that keeps all the DTO properties except for the ones excluded:
$result = $user->except(['name', 'address'], CAST_PRIMITIVES);
Sometimes we may need to quickly alter the data of an immutable DTO. In order to do that while preserving the immutability of the DTO after the altering process, we can call the mutate()
method:
$user->mutate(function (UserData $user) { $user->name = 'Jack'; });
Interact with properties
During the creation of a DTO, properties are internally mapped from the data provided. The properties map is an associative array containing the property names as keys and instances of Cerbero\Dto\DtoProperty
as values. To retrieve such map (maybe for inspection), we can call the getPropertiesMap()
method:
// ['name' => Cerbero\Dto\DtoProperty, ...] $map = $user->getPropertiesMap();
There are also methods to retrieve property names, all the DtoProperty
instances, a singular DtoProperty
instance and finally a method to determine if a property is set at all (useful for example to avoid false negatives when a property value is NULL):
// ['name', 'isAdmin', ...] $names = $user->getPropertyNames(); // [Cerbero\Dto\DtoProperty, Cerbero\Dto\DtoProperty, ...] $properties = $user->getProperties(); // Cerbero\Dto\DtoProperty instance for the property "name" $nameProperty = $user->getProperty('name'); // TRUE as long as the property "name" is set (even if its value is NULL) $hasName = $user->hasProperty('name');
Convert into array
As shown above, DTOs can behave like arrays, their values can be set and retrieved in an array fashion. DTO itself is iterable, hence can be used in a loop:
foreach($dto as $propertyName => $propertyValue) { // ... }
We can call the method toArray()
to get an array representation of a DTO and its nested DTOs. The resulting array will have keys in snake case by default, unless the DTO has the CAMEL_CASE_ARRAY
flag:
// [ // 'name' => 'Anna', // 'is_admin' => true, // 'address' => [ // 'street' => 'King Street', // 'unit' => 10, // ], // ] $user->toArray();
Sometimes we may want a value to be converted when a DTO turns into an array. To do so we can register value converters in the ArrayConverter
:
use Cerbero\Dto\Manipulators\ArrayConverter; use Cerbero\Dto\Manipulators\ValueConverter; class DateTimeConverter implements ValueConverter { public function fromDto($value) { return $value->format('Y-m-d'); } public function toDto($value) { return new DateTime($value); } } ArrayConverter::instance()->setConversions([ DateTime::class => DateTimeConverter::class, ]); $user = UserDto::make(['birthday' => '01/01/2000']); $user->birthday; // instance of DateTime $user->toArray(); // ['birthday' => '01/01/2000']
Please note that conversions registered in ArrayConverter
will apply to all DTOs, whenever they are turned into arrays. In order to transform values only for a specific DTO, read below about the Listener
class.
Singular conversions can also be added or removed with the methods addConversion()
and removeConversion()
:
ArrayConverter::instance()->addConversion(DateTime::class, DateTimeConverter::class); ArrayConverter::instance()->removeConversion(DateTime::class);
Listen to events
Whenever a DTO sets or gets one of its property values, a listener may intercept the event and alter the outcome. Every DTO can have one listener associated that can be registered via the Listener
class:
use Cerbero\Dto\Manipulators\Listener; class UserDtoListener { public function setName($value) { return ucwords($value); } public function getSomething($value) { return $value === null ? rand() : $value; } } Listener::instance()->listen([ UserDto::class => UserDtoListener::class, ]); $user = UserDto::make(['name' => 'john doe', 'something' => null]); $user->name; // John Doe $user->something; // random integer
In the example above, UserDtoListener
listens every time a UserDto
property is set or accessed and calls the related method if existing. The convention behind listeners method names is concatenating the event (set
or get
) to the listened property name in camel case, e.g. setName
or getIsAdmin
.
Values returned by listener methods override the actual property values. Listeners are not only meant to alter values but also to run arbitrary logic when a DTO property is read or set.
Singular listeners can also be added or removed with the methods addListener()
and removeListener()
:
Listener::instance()->addListener(UserDto::class, UserDtoListener::class); Listener::instance()->removeListener(UserDto::class);
Convert into string
Finally DTOs can be casted into strings. When that happens, their JSON representation is returned:
// {"name":"John Doe"} (string) $user;
A more explicit way to turn a DTO into a JSON is calling the method toJson()
, which has the same effect of encoding a DTO via json_encode()
:
$user->toJson(); json_encode($user);
If some DTO values need a special transformation when encoded into JSON, such transformation can be defined in ArrayConverter
(see the section Convert into array for more details).
Change log
Please see CHANGELOG for more information on what has changed recently.
Testing
$ composer test
Contributing
Please see CONTRIBUTING and CODE_OF_CONDUCT for details.
Security
If you discover any security related issues, please email andrea.marco.sartori@gmail.com instead of using the issue tracker.
Credits
License
The MIT License (MIT). Please see License File for more information.