v5.0 2024-12-12 05:28 UTC

README

This library is mainly for Symfony, but you can also use it in native PHP. How is this library different from others? The library can automatically collect objects according to the given rules from the data that you specify, basically this is the data from the Request

Install

$ composer require codememory/dto

What will be covered in this documentation?

  • How to use DTO?
  • How to validate DTO with symfony/validator?
  • How to use decorators?
  • What is Context in decorators?
  • How to create your own decorators?
  • What is a collector and how to create your own collector ?
  • How to create a context factory?
  • How to create your own key naming strategy?
  • How to create your own DTO property provider?

[ ! ] Please note that in the DataTransfer, all properties that we process must have the access modifier "public"

Usage examples

<?php

use Codememory\Dto\Collectors\BaseCollector;
use Codememory\Dto\Decorators as DD;
use Codememory\Dto\Factory\ConfigurationFactory;
use Codememory\Dto\DecoratorHandlerRegistrar;
use Codememory\Reflection\ReflectorManager;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Codememory\Dto\DataTransferObjectManager;
use Codememory\Dto\Factory\ExecutionContextFactory;

enum StatusEnum
{
    case ACTIVATED;
    case NOT_ACTIVATED;
}

#[DD\ToType]
final class UserDto extends DataTransferObjectManager
{
    public ?string $name = null;
    public ?string $surname = null;
    public ?int $age = null;
    
    #[DD\ToEnum]
    public ?StatusEnum $status = null;
}

$userDto = new UserDto(
    new BaseCollector(), 
    new ConfigurationFactory(),
    new ExecutionContextFactory(),
    new DecoratorHandlerRegistrar(),
    new ReflectorManager(new FilesystemAdapter('dto', '/var/cache/codememory'))
);

// We start the assembly of DTO based on the transferred data
$userDto->collect([
    'name' => 'My Name',
    'surname' => 'My Surname',
    'age' => 80,
    'status' => 'ACTIVATED'
]);

// Result dump UserDto
/** 
  name    -> My Name
  surname -> My Surname
  age     -> 80
  status  -> StatusEnum::ACTIVATED (object)
*/

Validate DTO with symfony/validator

use Symfony\Component\Validator\Validation;
use Symfony\Component\Validator\Constraints as Assert;
use Codememory\Reflection\ReflectorManager;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Codememory\Dto\DataTransferObjectManager;
use Codememory\Dto\Factory\ConfigurationFactory;
use Codememory\Dto\DecoratorHandlerRegistrar;
use Codememory\Dto\Factory\ExecutionContextFactory;

final ProductDto extends DataTransferObjectManager
{
    #[DD\Validation([
        new Assert\NotBlank(message: 'Name is required'),
        new Assert\Length(max: 5, maxMessage: 'Name must not exceed 5 characters')
    ])]
    public ?string $name = null;
}


$productDto = new ProductDto(
    new BaseCollector(),
    new ConfigurationFactory()
    new ExecutionContextFactory(),
    new DecoratorHandlerRegistrar(),
    new ReflectorManager(new FilesystemAdapter('dto', '/var/cache/codememory'))
);

$productDto->collect(['name' => 'Super name']);

// Validate
$validator = Validation::createValidatorBuilder()
    ->enableAnnotationMapping()
    ->getValidator();

$errors = $validator->validate($productDto);

foreach ($errors as $error) {
    echo $error->getMessage(); // Name must not exceed 5 characters
}

Use decorators

There are 2 types of decorators, one points to the DTO class, the other to the DTO properties

The difference between the decorator which the class has is that the given decorator will be executed for all DTO properties and the given decorator will be executed first

use Codememory\Dto\Decorators as DD

// Decorator for class
#[DD\ToType] // This decorator will cast all DTO properties to the type specified by the property
final class OneDto extends AbstractDataTransferObject 
{
    public ?int $number = null;
    public array $list = [];
}

// Decorators for properties
final class TestDto extends AbstractDataTransferObject 
{
    #[DD\NestedDTO(OneDto::class)]
    public ?OneDto $one = null;
    
   
    // Multiple decorators
    // Priority works here, first ToEnum will be executed, and then IgnoreSetterCallForHarvestableObject
    #[DD\ToEnum]
    #[DD\IgnoreSetterCallForHarvestableObject]
    public ?StatusEnum $status = null;
}

List of decorators

  • IgnoreSetterCallForHarvestableObject - Ignore the setter call on the harvestable object

  • PrefixSetterMethodForHarvestableObject - Changes the prefix of the called method to set the value in the harvestable object

    • $prefix - Prefix name, for example "set"
  • SetterMethodForHarvestableObject - Change the full name of the method through which it will be possible to set the value in the harvestable object

    • $name - Method name, for example "setName"
  • NestedDTO - Nested DataTransfer, nest in DataTransfer property

    • $dto - DataTransfer namespace
    • $object (default: null) - The namespace of the object to be collected. If the value is not passed, the property on which this decorator is attached will ignore the setter call on the collected object
    • $thenCallback (default: null) - The name of the callback method, which should return a bool value indicating whether it is worth checking out or not
  • ToEnum - Translates a value from collect data to an enum object

    • $byValue (default: false) - Search for case in Enum by its value, by default it searches by its name
  • ToEnumList - Similar to the ToEnum decorator, except that this decorator expects an array and will try to convert each element of the value into an Enum

    • $unique (default: true) - This is a new argument that will filter the input array for uniqueness
  • ToType - Converts a value from collect data to a specific type

    • $type (default: auto) - The name of the PHP type or Interface DateTime. By default works on the type from the property
    • $onyData (default: false) - Force cast to type, only value at collect data level
  • Validation - Add symfony assert constraint to validation queue

    • $assert - Array of validation rules if this property will be processed
  • XSS - Protecting input strings or strings in an array from XSS attack

  • ExpectArray - Expects a normal array

    • $expectKeys - Array of pending keys, the rest will be removed
  • ExpectMultiArray - Expects a normal array

    • $expectKeys - Array of pending keys, the rest will be removed
    • $itemKeyAsNumber (default: true) - Converts all item keys to numeric order
  • ExpectOneDimensionalArray - Expects a one-dimensional array

    • $types (default: any) - Array of skipped value types

List of data key naming strategies

  • Codememory\Dto\DataKeyNamingStrategy\DataKeyNamingStrategyCamelCase - Translates property name to camelCase for lookup in data
  • Codememory\Dto\DataKeyNamingStrategy\DataKeyNamingStrategySnakeCase - Translates property name to snake_case for lookup in data
  • Codememory\Dto\DataKeyNamingStrategy\DataKeyNamingStrategyUpperCase - Translates property name to UPPER_CASE for lookup in data

List of property providers

  • Codememory\Dto\Provider\DataTransferObjectPrivatePropertyProvider - Allows only private properties ignoring AbstractDataTransferObject properties
  • Codememory\Dto\Provider\DataTransferObjectProtectedPropertyProvider - Allows only protected properties ignoring AbstractDataTransferObject properties
  • Codememory\Dto\Provider\DataTransferObjectPublicPropertyProvider - Allows only public properties ignoring AbstractDataTransferObject properties

Parsing Context

This is an API class that comes inside a decorator to manage the state or values of the dto, the object being collected, and the value from collect data

Methods:

  • getDataTransferObject - Returns the current data transfer object that contains the property being processed.
  • getProperty - Returns the currently processed property.
  • getData - Returns the input data that will be used to collect the dto and the object.
  • getDataValue - Returns a value from data (which was passed during data transfer build).
  • getDataTransferObjectValue - Returns the value that was set to the data transfer property.
  • getValueForHarvestableObject - Returns the value that was set to the object being collected.
  • getDataKey - Returns a key that can be used to get a value from data.
  • getNameSetterMethodForHarvestableObject - Returns the name of the setter method for the object being collected.
  • isIgnoredSetterCallForHarvestableObject - Whether the setter method call on the harvestable object is ignored.
  • isSkippedThisProperty - Whether to skip processing the current property.

Many of these methods have setters.

DataTransferObject Methods:

  • getCollector - Returns the collector with which the dto will be collected
  • getConfiguration - Returns the dto configuration
  • getExecutionContextFactory - Returns the factory with which the decorator context was created
  • getReflectorManager - Returns the reflection manager, more details in the codememory/reflection library
  • getClassReflector - Returns a reflection of the current dto
  • getHarvestableObject - Returns the collectable object
  • setHarvestableObject - Set build object
  • addDataTransferObjectPropertyConstraintsCollection - Add DTO Property Validation Collection
  • getListDataTransferObjectPropertyConstrainsCollection - Get a list of all dto property validation collections
  • getDataTransferObjectPropertyConstrainsCollection - Get collection validation property of specific dto
  • collect - Starts the entire build process
  • recollectHarvestableObject - Rebuilds the harvestable object into the new passed object

Creating your own decorator

use Attribute;
use Codememory\Dto\Interfaces\DecoratorInterface;
use Codememory\Dto\Interfaces\DecoratorHandlerInterface;
use Codememory\Dto\Interfaces\ExecutionContextInterface;
use Codememory\Reflection\ReflectorManager;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Codememory\Dto\DataTransferObjectManager;
use Codememory\Dto\Factory\ExecutionContextFactory;
use Codememory\Dto\Factory\ConfigurationFactory;
use Codememory\Dto\DecoratorHandlerRegistrar;

// Let's create a decorator that will combine the value of all properties and separate it with a certain character
#[Attribute(Attribute::TARGET_PROPERTY)] // Will only apply to properties
final class PropertyConcatenation implements DecoratorInterface 
{
    public function __construct(
        public readonly string $propertyNames,
        public readonly string $separator
    ) {}
    
    public function getHandler() : string
    {
        return PropertyConcatenationHandler::class;
    }
}

// Create decorator Handler
final class PropertyConcatenationHandler implements DecoratorHandlerInterface 
{
    /**
     * @param PropertyConcatenation $decorator
     */
    public function handle(ConstraintInterface $decorator, ExecutionContextInterface $context) : void
    {
        // Get the values of all passed properties
        $assignedValues = array_map(static function (string $property) use ($context) {
            return $context->getDataTransferObject()->$property;
        }, $decorator->propertyNames);
        
        // Update the current value by concatenating multiple values separating them with $separator
        $context->setDataTransferObjectValue(implode($decorator->separator, $assignedValues));
        $context->setValueForHarvestableObject($context->getDataTransferObject());
    }
}

// Let's test our decorator
final class TestDto extends DataTransferObjectManager
{
    public ?string $name = null;
    public ?string $surname = null;
    
    #[PropertyConcatenation(['name', 'surname'], '+')]
    public ?string $fullName = null;
}

$testDto = new TestDto(
    new BaseCollector(),
    new ConfigurationFactory(),
    new ExecutionContextFactory(),
    new DecoratorHandlerRegistrar(),
    new ReflectorManager(new FilesystemAdapter('dto', '/var/cache/codememory'))
);

// To register this decorator when you create a new instance by passing the configuration as the second argument to it, you must first register the decorator through this configuration
$testDto->getDecoratorHandlerRegistrar()->register(new PropertyConcatenationHandler());

$testDto->collect([
    'name' => 'Code',
    'surname' => 'Memory',
    'full_name' => 'test_full_name' // Our decorator will override this value
]);

echo $testDto->fullName // Code+Memory

Creating Your Own Collector

Collector - is a DTO collector, it plays a major role in working with each DTO property

use Codememory\Dto\Interfaces\CollectorInterface;
use Codememory\Dto\Interfaces\ExecutionContextInterface;
use Codememory\Dto\Interfaces\DecoratorInterface;

final class MyCollector implements CollectorInterface
{
    public function collect(ExecutionContextInterface $context) : void
    {
        // Here all processing begins on each property, this method is called every iteration of the properties
        
        // Example get property attributes
        foreach ($context->getProperty()->getAttributes() as $attribute) {
            $attributeInstance = $attribute->newInstance();
        
            if ($attributeInstance instanceof DecoratorInterface) {
                $decoratorHandler = $context->getDataTransferObject()->getDecoratorHandlerRegistrar()->getHandler($attributeInstance->getHandler());
                
                // ....
            }
        }
        
        // ....
    }
}

How to create a context factory?

use Codememory\Dto\Interfaces\ExecutionContextFactoryInterface;
use Codememory\Dto\Interfaces\ExecutionContextInterface;
use Codememory\Dto\Interfaces\DataTransferObjectInterface;
use Codememory\Reflection\Reflectors\PropertyReflector;
use Codememory\Dto\Interfaces\ExecutionContextInterface;
use Codememory\Dto\Factory\ConfigurationFactory;

// Create a context
final class MyContext implements ExecutionContextInterface
{
    // Implementing Interface Methods...
}

// Creating a context factory
final class MyContextFactory implements ExecutionContextFactoryInterface
{
    public function createExecutionContext(DataTransferObjectInterface $dataTransferObject, PropertyReflector $property, array $data) : ExecutionContextInterface
    {
        $context = new MyContext();
        // ...
        
        return $context;
    }
}

// When creating a DTO instance, we pass this context factory
// Example:
new MyDto(new BaseCollector(), new ConfigurationFactory(), new MyContextFactory(), ...);

How to create your own key naming strategy?

This strategy will look for values in data which was passed to collect as "_{dto property name}"

use Codememory\Dto\Interfaces\DataKeyNamingStrategyInterface;
use Codememory\Dto\Factory\ConfigurationFactory;

final class MyStrategyName implements DataKeyNamingStrategyInterface
{
    private ?\Closure $extension = null;

    public function convert(string $propertyName) : string
    {
        $name =  "_$propertyName";
        
        if (null !== $this->extension) {
            return call_user_func($this->extension, $name);
        }
        
        return $name;
    }
    
    // With this method, you need to give the opportunity to extend the convert method
    public function setExtension(callable $callback) : DataTransferObjectPropertyProviderInterface
    {
        $this->extension = $callback;
        
        return $this;
    }
}

$myDto = new MyDTO(new BaseCollector(), new ConfigurationFactory(), ...);

// To use this strategy, you need to change the configuration
$myDto->getConfiguration()->setDataKeyNamingStrategy(new MyStrategyName());

How to create your own DTO property provider?

The provider must return the dto properties that are allowed to be processed by the collector! Don't forget to ignore AbstractDataTransferObject properties, otherwise these properties will be processed too

use Codememory\Dto\Interfaces\DataTransferObjectPropertyProviderInterface;
use Codememory\Reflection\Reflectors\ClassReflector;
use Codememory\Dto\Factory\ConfigurationFactory;

// The provider will say that only private properties need to be processed
final class MyPropertyProvider implements DataTransferObjectPropertyProviderInterface
{
    private ?\Closure $extension = null;

    public function getProperties(ClassReflector $classReflector) : array
    {
        $properties = $classReflector->getPrivateProperties();
        
        if (null !== $this->extension) {
            return call_user_func($this->extension, $properties);
        }
        
        return $properties;
    }
    
    // With this method, you need to give the opportunity to extend the getProperties method
    public function setExtension(callable $callback) : DataTransferObjectPropertyProviderInterface
    {
        $this->extension = $callback;
        
        return $this;
    }
}

$myDto = new MyDTO(new BaseCollector(), new ConfigurationFactory(), ...);

// Change the provider in the configuration
$myDto->getConfiguration()->setDataTransferObjectPropertyProvider(new MyPropertyProvider());