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.
Via Composer
composer require cerbero/dto
- 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
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.
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.
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.
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');
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
.
The flag Cerbero\Dto\NONE
is simply a placeholder and doesn't alter the behaviour of a DTO in any way.
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.
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'])
.
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.
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.
The flag Cerbero\Dto\CAMEL_CASE_ARRAY
lets all DTO properties preserve their camel case names when a DTO is converted into an array.
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
.
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.
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';
});
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');
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);
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);
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).
Please see CHANGELOG for more information on what has changed recently.
$ composer test
Please see CONTRIBUTING and CODE_OF_CONDUCT for details.
If you discover any security related issues, please email [email protected] instead of using the issue tracker.
The MIT License (MIT). Please see License File for more information.