cuyz/valinor-bundle

Symfony integration of `cuyz/valinor` — a library that helps to map any input into a strongly-typed value object structure.

Fund package maintenance!
romm

Installs: 32 003

Dependents: 1

Suggesters: 0

Security: 0

Stars: 40

Watchers: 3

Forks: 4

Open Issues: 1

Type:symfony-bundle

0.2.3 2024-01-13 16:57 UTC

This package is auto-updated.

Last update: 2024-04-16 15:29:26 UTC


README

Symfony logo Plus Valinor banner

Latest Stable Version PHP Version Require

Symfony integration of Valinor library.

Valinor takes care of the construction and validation of raw inputs (JSON, plain arrays, etc.) into objects, ensuring a perfectly valid state. It allows the objects to be used without having to worry about their integrity during the whole application lifecycle.

The validation system will detect any incorrect value and help the developers by providing precise and human-readable error messages.

The mapper can handle native PHP types as well as other advanced types supported by PHPStan and Psalm like shaped arrays, generics, integer range and more.

Installation

composer require cuyz/valinor-bundle
// config/bundles.php

return [
    // …
    CuyZ\ValinorBundle\ValinorBundle::class => ['all' => true],
];

Mapper injection

A mapper instance can be injected in any autowired service in parameters with the type TreeMapper.

use CuyZ\Valinor\Mapper\TreeMapper;

final class SomeAutowiredService
{
    public function __construct(
        private TreeMapper $mapper,
    ) {}
    
    public function someMethod(): void
    {
        $this->mapper->map(SomeDto::class, /* … */);
        
        // …
    }
}

It can also be manually injected in a service…

…using a PHP file
// config/services.php

use CuyZ\Valinor\Mapper\TreeMapper;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $container): void {
    $container
        ->services()
        ->set(\Acme\SomeService::class)
        ->args([
            service(TreeMapper::class),
        ]);
};
…using a YAML file
services:
    Acme\SomeService:
        arguments:
            - '@CuyZ\Valinor\Mapper\TreeMapper'

Bundle configuration

Global configuration for the bundle can be done in a package configuration file…

…using a PHP file
// config/packages/valinor.php

return static function (Symfony\Config\ValinorConfig $config): void {
    // Date formats that will be supported by the mapper by default.
    $config->mapper()->dateFormatsSupported(['Y-m-d', 'Y-m-d H:i:s']);

    // For security reasons, exceptions thrown in a constructor will not be
    // caught by the mapper unless they are specifically allowed by giving their
    // class names to the configuration below.
    $config->mapper()->allowedExceptions([
        \Webmozart\Assert\InvalidArgumentException::class,
        \App\CustomException::class,
    ]);

    // When a mapping error occurs during a console command, the output will
    // automatically be enhanced to show information about errors. The maximum
    // number of errors that will be displayed can be configured below, or set
    // to 0 to disable this feature entirely.
    $config->console()->mappingErrorsToOutput(15);

    // By default, mapper cache entries are stored in the filesystem. This can
    // be changed by setting the name of a PSR-16 cache service below.
    $config->cache()->service('app.custom_cache');

    // Cache entries representing class definitions won't be cleared when files
    // are modified during development of the application. This can be changed
    // by setting in which environments cache entries will be unvalidated.
    $config->cache()->envWhereFilesAreWatched(['dev', 'custom_env']);
};
…using a YAML file
# config/packages/valinor.yaml

valinor:
    mapper:
        # Date formats that will be supported by the mapper by default.
        date_formats_supported:
            - 'Y-m-d'
            - 'Y-m-d H:i:s'

        # For security reasons, exceptions thrown in a constructor will not be
        # caught by the mapper unless they are specifically allowed by giving
        # their class names to the configuration below.
        allowed_exceptions:
            - \Webmozart\Assert\InvalidArgumentException
            - \App\CustomException,

    console:
        # When a mapping error occurs during a console command, the output will
        # automatically be enhanced to show information about errors. The
        # maximum number of errors that will be displayed can be configured
        # below, or set to 0 to disable this feature entirely.
        mapping_errors_to_output: 15

    cache:
        # By default, mapper cache entries are stored in the filesystem. This
        # can be changed by setting the name of a PSR-16 cache service below.
        service: app.custom_cache

        # Cache entries representing class definitions won't be cleared when
        # files are modified during development of the application. This can be
        # changed by setting in which environments cache entries will be
        # unvalidated.
        env_where_files_are_watched: [ 'dev', 'custom_env' ]

Other features

Customizing mapper builder

A service can customize the mapper builder by implementing the interface MapperBuilderConfigurator.

Note

If this service is autoconfigured, it will automatically be used, otherwise it needs to be tagged with the tag valinor.mapper_builder_configurator.

use CuyZ\Valinor\MapperBuilder;
use CuyZ\ValinorBundle\Configurator\MapperBuilderConfigurator

final class ConstructorRegistrationConfigurator implements MapperBuilderConfigurator
{
    public function configure(MapperBuilder $builder): MapperBuilder
    {
        return $builder
            ->registerConstructor(SomeDTO::create(...))
            ->registerConstructor(SomeOtherDTO::new(...));
    }
}

Configuring mapper behaviour with attributes

Attributes can be used to automatically customize the mapper behaviour.

Warning

This feature is only available for autowired services.

  • EnableFlexibleCasting — changes several behaviours of the mapper concerning type flexibility. For more information, read the documentation.

  • AllowSuperfluousKeys — allows superfluous keys in source arrays, preventing errors when a value is not bound to any object property/parameter or shaped array element. For more information, read the documentation.

  • AllowPermissiveTypes — allows permissive types mixed and object to be used during mapping.

  • SupportDateFormats — configures which date formats will be supported by the mapper.

use CuyZ\Valinor\Mapper\TreeMapper;
use CuyZ\ValinorBundle\Configurator\Attributes\AllowPermissiveTypes;
use CuyZ\ValinorBundle\Configurator\Attributes\AllowSuperfluousKeys;
use CuyZ\ValinorBundle\Configurator\Attributes\EnableFlexibleCasting;
use CuyZ\ValinorBundle\Configurator\Attributes\SupportDateFormats;

final class SomeService
{
    public function __construct(
        #[EnableFlexibleCasting]
        private TreeMapper $mapperWithFlexibleCasting,

        // or…
        #[AllowSuperfluousKeys]
        private TreeMapper $mapperWithSuperfluousKeys,

        // or…
        #[AllowPermissiveTypes]
        private TreeMapper $mapperWithPermissiveTypes,

        // or…
        #[SupportDateFormats('Y-m-d', 'Y/m/d')]
        private TreeMapper $mapperWithCustomDateFormat,
        
        // or a combination of the above…
        #[EnableFlexibleCasting, AllowSuperfluousKeys, …]
        private TreeMapper $mapperWithSeveralAttributes,
    ) {}
}

It is also possible to declare custom configurator attributes by using the interface MapperBuilderConfiguratorAttribute:

use Attribute;
use CuyZ\Valinor\MapperBuilder;
use CuyZ\ValinorBundle\Configurator\Attributes\MapperBuilderConfiguratorAttribute;
 
#[Attribute(Attribute::TARGET_PARAMETER)]
final class SomeCustomConfigurator implements MapperBuilderConfiguratorAttribute
{
    public function configure(MapperBuilder $builder): MapperBuilder
    {
        return $builder
            ->enableFlexibleCasting()
            ->allowSuperfluousKeys()
            ->supportDateFormats('Y/m/d');
    }
}

And then it can be used in a service:

use CuyZ\Valinor\Mapper\TreeMapper;
use CuyZ\ValinorBundle\Configurator\Attributes\SomeCustomConfigurator;
 
final class SomeService
{
   public function __construct(
      #[SomeCustomConfigurator]
     private TreeMapper $mapperWithCustomConfig
  ) {}
}

Mapping errors in console commands

When running a command using Symfony Console, mapping errors will be caught to enhance the output and give a better idea of what went wrong.

Note

The maximum number of errors that will be displayed can be configured in the bundle configuration.

Example of output:

$ bin/console some:command

Mapping errors
--------------

A total of 3 errors were found while trying to map to `Acme\Customer`

 -------- ------------------------------------------------------------------------- 
  path     message                                                                  
 -------- ------------------------------------------------------------------------- 
  id       Value 'John' is not a valid integer.
  name     Value 42 is not a valid string.
  email    Cannot be empty and must be filled with a value matching type `string`.  
 -------- ------------------------------------------------------------------------- 
                                                                                                                        
 [INFO] The above message was generated by the Valinor Bundle, it can be disabled
        in the configuration of the bundle.

Cache warmup

When using Symfony's cache warmup feature — usually bin/console cache:warmup — the mapper cache will be warmed up automatically for all classes that are tagged with the tag valinor.warmup.

This tag can be added manually via service configuration, or automatically for autoconfigured classes using the attribute WarmupForMapper.

#[\CuyZ\ValinorBundle\Cache\WarmupForMapper]
final readonly class ClassThatWillBeWarmedUp
{
    public function __construct(
        public string $foo,
        public int $bar,
    ) {}
}

Note

The WarmupForMapper attribute disables dependency injection autowiring for the class it is assigned to. Although autowiring a class that will be instantiated by a mapper makes little sense in most cases, it may still be needed, in which case the $autowire parameter of the attribute can be set to true.