Code Monkey home page Code Monkey logo

valinor's Introduction

Valinor banner

— From boring old arrays to shiny typed objects —

Latest Stable Version PHP Version Require Total Downloads Mutation testing badge


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 ranges and more.

The library also provides a normalization mechanism that can help transform any input into a data format (JSON, CSV, …), while preserving the original structure.

Installation

composer require cuyz/valinor

📔 Read more on the online documentation

Example

final class Country
{
    public function __construct(
        /** @var non-empty-string */
        public readonly string $name,
        
        /** @var list<City> */
        public readonly array $cities,
    ) {}
}

final class City
{
    public function __construct(
        /** @var non-empty-string */
        public readonly string $name,
        
        public readonly DateTimeZone $timeZone,
    ) {}
}

$json = <<<JSON
    {
        "name": "France",
        "cities": [
            {"name": "Paris", "timeZone": "Europe/Paris"},
            {"name": "Lyon", "timeZone": "Europe/Paris"}
        ]
    }
JSON;

try {
    $country = (new \CuyZ\Valinor\MapperBuilder())
        ->mapper()
        ->map(Country::class, \CuyZ\Valinor\Mapper\Source\Source::json($json));

    echo $country->name; // France 
    echo $country->cities[0]->name; // Paris
} catch (\CuyZ\Valinor\Mapper\MappingError $error) {
    // Handle the error…
}

Documentation

The full documentation is available on valinor.cuyz.io.

Credits & thank you

The development of this library is mainly motivated by the kind words and the help of many people. I am grateful to everyone, especially to the contributors of this repository who directly help to push the project forward.

I also want to thank blackfire-logo Blackfire for providing a license of their awesome tool, leading to notable performance gains when using this library.

valinor's People

Contributors

aboyton avatar agustingomes avatar androlgenhald avatar aurimasniekis avatar bcremer avatar boesing avatar brandonsavage avatar danog avatar davidbadura avatar dependabot[bot] avatar edudobay avatar franmomu avatar fred-jan avatar jdreesen avatar lookyman avatar lucian-olariu avatar magnetik avatar marek-mikula avatar mopolo avatar mtouellette avatar nanosector avatar nickvergessen avatar ocramius avatar robchett avatar romm avatar samsonasik avatar simpod avatar slamdunk avatar szepeviktor avatar timwolla avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

valinor's Issues

"stricter" mode for input types?

Currently, the default behavior of this library is to go through automatic casting, similar to what PHP does with implicit type coercion.

This is probably fine for most use-cases, so this issue is just a question: is it possible to force the library to require exact types as input?

The reason is mostly to avoid issues like floating point -> integer conversion (or the opposite) happening by accident: requiring API clients to provide cleaner data, rather than being "lax" about it.

In following example, all is good:

<?php

namespace My\App;

use CuyZ\Valinor\Attribute\StaticMethodConstructor;
use CuyZ\Valinor\Mapper\MappingError;
use CuyZ\Valinor\MapperBuilder;

require_once __DIR__ . '/Valinor/vendor/autoload.php';

class MyIntWrapper
{
    public int $value;
}

$mapper = (new MapperBuilder())
    ->withCacheDir(__DIR__ . '/cache')
    ->mapper();

var_dump($mapper->map(MyIntWrapper::class, ['value' => 1.0]));
php example.php 
object(My\App\MyIntWrapper)#75 (1) {
  ["value"]=>
  int(1)
}

This is fine, but should there be a rounding error outside what PHP's floating point precision can handle, we would likely have an issue here.

How to not auto-assign null to nullable properties?

I want to differentiate between user not supplying a property, and supplying null.

class Payload {
    // Must always be provided
    public int $id;
    
    // Can be null, or string,
    public string|null $note;
}

The source ['id' => 1] will set Payload::$note to null. But user
did not supply null as value. I want to handle this a MappingError and ask
user to supply either null or string. Is this possible somehow?

Unexpected hydration behavior

I copy-paste example and run it:

$rawJson = <<<TEST
{
    "id": 1337,
    "content": "Do you like potatoes?",
    "date": "1957-07-23 13:37:42",
    "answers": [
        {
            "user": "Ella F.",
            "message": "I like potatoes",
            "date": "1957-07-31 15:28:12"
        },
        {
            "user": "Louis A.",
            "message": "And I like tomatoes",
            "date": "1957-08-13 09:05:24"
        }
    ]
}
TEST;

final class Thread
{
    public function __construct(
        public readonly int $id,
        public readonly string $content,
        public readonly DateTimeInterface $date,
        /** @var Answer[] */
        public readonly array $answers,
    ) {}
}

final class Answer
{
    public function __construct(
        public readonly string $user,
        public readonly string $message,
        public readonly DateTimeInterface $date,
    ) {}
}

$result = (new \CuyZ\Valinor\MapperBuilder())
    ->mapper()
    ->map(
        Thread::class,
        new \CuyZ\Valinor\Mapper\Source\JsonSource($rawJson)
    );

I got following result:
Screenshot 2022-01-28 at 19 19 43

But I expect array of Answer objects in answers property. What am I doing wrong?

P.S. PHP 8.1.1, library version: 0.5.0

Problems with $cacheDir attribute

Hello,

I was having some problems with permissions on my temp folder on my Mac so I decided to provide my own custom cache folder for compiled files.

But I ran into an issue. The cached string is firstly put into temp folder and then 'renamed' (moved into my specified cache folder') so I can't get passed the error with my permissions.

Is there any specific reason why to do this way? Why not just directly put the cached string into the specified cache folder?

Thank you in advance and thank you for this package, it's really cool :)

$tmpFilename = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('', true);

[Question] How it's the estructure of map class files.

I have see the examplen and I have a questión.
I have to write all the clases definition in the same file or I can do this:

<?php 
#ThreadMap.php
namespace App\Mapps;

use App\Mapps\AnswerMap;
final class ThreadMap
{
    public function __construct(
        public readonly int $id,
        public readonly string $content,
        public readonly DateTimeInterface $date,
        /** @var AnswerMap[] */
        public readonly array $answers, 
    ) {}
}
<?php
#AnswerMap.php
namespace App\Mapps;

final class AnswerMap
{
    public function __construct(
        public readonly string $user,
        public readonly string $message,
        public readonly DateTimeInterface $date,
    ) {}
}

feat: allow to declare development environment

Currently, the compilation of a class definition will detect file modification in order to re-run the compilation, see:

public function compileValidation($value): string
{
assert($value instanceof ClassDefinition);
$filename = (Reflection::class($value->name()))->getFileName();
// If the file does not exist it means it's a native class so the
// definition is always valid (for a given PHP version).
if (false === $filename) {
return 'true';
}
$time = filemtime($filename);
return "\\filemtime('$filename') === $time";
}

While this is often needed during a user's development phase (because the value-objects used for the mapping are always changing), this is useless in a production environment, where the PHP files should not change (or during a deployment phase, but the caches should then be cleared).

The issue is that filemtime function is quite heavy and resource-hungry, where this library tries to be as optimized as possible.

Proposal

The MapperBuilder should give access to a new method that would allow to "activate" development mode — this setting would then be used by ClassDefinitionCompiler to handle, or not, the file modification feature.

Feat: Allow providing own PSR-16 cache service

I have been working on making a Symfony bundle for this library and thought it would be great if it was possible to inject my own PSR-16 cache service instead of using a file cache provided from Library.

With the current setup, I think it would be impossible without replicating a whole Container functionality.

Maybe it's better to replicate Container for situations where integrating with framework for e.g. Symfony.

Integer array keys in `array{0: type}` syntax are not recognized correctly

Following works:

<?php

namespace My\App;

use CuyZ\Valinor\Attribute\StaticMethodConstructor;
use CuyZ\Valinor\Mapper\MappingError;
use CuyZ\Valinor\MapperBuilder;

require_once __DIR__ . '/Valinor/vendor/autoload.php';

class MyArrayWrapper
{
    /** @var array{foo: string, bar: int, baz: bool} */
    public array $value;
}

$mapper = (new MapperBuilder())
    ->withCacheDir(__DIR__ . '/cache')
    ->mapper();

var_dump($mapper->map(MyArrayWrapper::class, ['value' => ['foo' => 'a', 'bar' => '1', 'baz' => 'true']]));
 php example.php 
object(My\App\MyArrayWrapper)#88 (1) {
  ["value"]=>
  array(3) {
    ["foo"]=>
    string(1) "a"
    ["bar"]=>
    int(1)
    ["baz"]=>
    bool(true)
  }
}

Doing the same with integer array keys seems to make the tooling choke:

<?php

namespace My\App;

use CuyZ\Valinor\Attribute\StaticMethodConstructor;
use CuyZ\Valinor\Mapper\MappingError;
use CuyZ\Valinor\MapperBuilder;

require_once __DIR__ . '/Valinor/vendor/autoload.php';

class MyArrayWrapper
{
    /** @var array{0: string, 1: int, 2: bool} */
    public array $value;
}

$mapper = (new MapperBuilder())
    ->withCacheDir(__DIR__ . '/cache')
    ->mapper();

var_dump($mapper->map(MyArrayWrapper::class, ['value' => [0 => 'a', 1 => '1', 2 => 'true']]));
php example.php 

Fatal error: Uncaught CuyZ\Valinor\Type\Parser\Exception\Stream\WrongTokenType: Wrong token type `CuyZ\Valinor\Type\Parser\Lexer\Token\ColonToken`, it should be an instance of `CuyZ\Valinor\Type\Parser\Lexer\Token\TraversingToken`. in /app/Valinor/src/Type/Parser/Lexer/TokenStream.php:36
Stack trace:
#0 /app/Valinor/src/Type/Parser/Lexer/Token/ArrayToken.php(131): CuyZ\Valinor\Type\Parser\Lexer\TokenStream->read()
#1 /app/Valinor/src/Type/Parser/Lexer/Token/ArrayToken.php(66): CuyZ\Valinor\Type\Parser\Lexer\Token\ArrayToken->shapedArrayType(Object(CuyZ\Valinor\Type\Parser\Lexer\TokenStream))
#2 /app/Valinor/src/Type/Parser/Lexer/TokenStream.php(39): CuyZ\Valinor\Type\Parser\Lexer\Token\ArrayToken->traverse(Object(CuyZ\Valinor\Type\Parser\Lexer\TokenStream))
#3 /app/Valinor/src/Type/Parser/LexingParser.php(39): CuyZ\Valinor\Type\Parser\Lexer\TokenStream->read()
#4 /app/Valinor/src/Definition/Repository/Reflection/ReflectionPropertyDefinitionBuilder.php(132): CuyZ\Valinor\Type\Parser\LexingParser->parse('array{0: string...')
#5 /app/Valinor/src/Definition/Repository/Reflection/ReflectionPropertyDefinitionBuilder.php(110): CuyZ\Valinor\Definition\Repository\Reflection\ReflectionPropertyDefinitionBuilder->parseType('array{0: string...', Object(ReflectionProperty), Object(CuyZ\Valinor\Type\Parser\LexingParser))
#6 /app/Valinor/src/Definition/Repository/Reflection/ReflectionPropertyDefinitionBuilder.php(70): CuyZ\Valinor\Definition\Repository\Reflection\ReflectionPropertyDefinitionBuilder->typeFromDocBlock(Object(CuyZ\Valinor\Definition\ClassSignature), Object(ReflectionProperty))
#7 /app/Valinor/src/Definition/Repository/Reflection/ReflectionPropertyDefinitionBuilder.php(43): CuyZ\Valinor\Definition\Repository\Reflection\ReflectionPropertyDefinitionBuilder->resolveType(Object(CuyZ\Valinor\Definition\ClassSignature), Object(ReflectionProperty))
#8 /app/Valinor/src/Definition/Repository/Reflection/ReflectionClassDefinitionRepository.php(41): CuyZ\Valinor\Definition\Repository\Reflection\ReflectionPropertyDefinitionBuilder->for(Object(CuyZ\Valinor\Definition\ClassSignature), Object(ReflectionProperty))
#9 [internal function]: CuyZ\Valinor\Definition\Repository\Reflection\ReflectionClassDefinitionRepository->CuyZ\Valinor\Definition\Repository\Reflection\{closure}(Object(ReflectionProperty))
#10 /app/Valinor/src/Definition/Repository/Reflection/ReflectionClassDefinitionRepository.php(42): array_map(Object(Closure), Array)
#11 /app/Valinor/src/Definition/Repository/Cache/CacheClassDefinitionRepository.php(36): CuyZ\Valinor\Definition\Repository\Reflection\ReflectionClassDefinitionRepository->for(Object(CuyZ\Valinor\Definition\ClassSignature))
#12 /app/Valinor/src/Mapper/Tree/Builder/ClassNodeBuilder.php(35): CuyZ\Valinor\Definition\Repository\Cache\CacheClassDefinitionRepository->for(Object(CuyZ\Valinor\Definition\ClassSignature))
#13 /app/Valinor/src/Mapper/Tree/Builder/CasterNodeBuilder.php(35): CuyZ\Valinor\Mapper\Tree\Builder\ClassNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#14 /app/Valinor/src/Mapper/Tree/Builder/VisitorNodeBuilder.php(28): CuyZ\Valinor\Mapper\Tree\Builder\CasterNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#15 /app/Valinor/src/Mapper/Tree/Builder/ValueAlteringNodeBuilder.php(31): CuyZ\Valinor\Mapper\Tree\Builder\VisitorNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#16 /app/Valinor/src/Mapper/Tree/Builder/ShellVisitorNodeBuilder.php(30): CuyZ\Valinor\Mapper\Tree\Builder\ValueAlteringNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#17 /app/Valinor/src/Mapper/Tree/Builder/ErrorCatcherNodeBuilder.php(23): CuyZ\Valinor\Mapper\Tree\Builder\ShellVisitorNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#18 /app/Valinor/src/Mapper/Tree/Builder/RootNodeBuilder.php(21): CuyZ\Valinor\Mapper\Tree\Builder\ErrorCatcherNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#19 /app/Valinor/src/Mapper/TreeMapperContainer.php(57): CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell))
#20 /app/Valinor/src/Mapper/TreeMapperContainer.php(31): CuyZ\Valinor\Mapper\TreeMapperContainer->node('My\\App\\MyArrayW...', Array)
#21 /app/example.php(22): CuyZ\Valinor\Mapper\TreeMapperContainer->map('My\\App\\MyArrayW...', Array)
#22 {main}
  thrown in /app/Valinor/src/Type/Parser/Lexer/TokenStream.php on line 36

Inconsistent results with nullable property

When the source is empty, is the mapper not able to set null to nullable property.

Reproduction

use CuyZ\Valinor\Mapper\MappingError;

require "vendor/autoload.php";

final class Foo
{
    public string|null $note;
}

final class Bar
{
    public string|null $note;

    public string $title;
}

try {
    $foo = (new \CuyZ\Valinor\MapperBuilder())
        ->mapper()
        ->map(Foo::class, []);
} catch (MappingError $e) {
    // Foo: Cannot cast value of type `array` to `string`.
    // Caused by ClassNodeBuilder::transformSource()
    // $source is [] and since the property $note does not exist in $source
    // will it return $source = [$name => $source];

    foreach($e->node()->children() as $child) {
        foreach($child->messages() as $message) {
            echo "Foo: " . $message . "\n";
        }
    }
}

// No issues:
$bar = (new \CuyZ\Valinor\MapperBuilder())
    ->mapper()
    ->map(Bar::class, ['title' => 'Foo']);

$bar->note === null; // true

Expected behavior

Foo::$note should be assigned null as value to be consistent with example Bar, but see my other issue here: #113.

problem saving estructure cache in 0.3.0

In version 0.2.0 there are no problems at all. But when I change version to 0.3.0 appears a problem saving the cache:

**Warning**:  filemtime(): stat failed for [path of my script]/src/Folder.php in /tmp/16848792a685d495037f24b2d638a3219f5c253b.php on line 97
**Warning**:  rename(/tmp/61c4ab23037cf2.75453093,/tmp/16848792a685d495037f24b2d638a3219f5c253b.php): Operation not permitted in [path]/vendor/cuyz/valinor/src/Cache/Compiled/CompiledPhpFileCache.php</b> on line 88

 It looks like for some reason I can not rename files in /tmp/
I can configure other path?

JsonSource and shape

Snippet from example, but replaced with JsonSource

<?php

require "vendor/autoload.php";

try {
    $json = json_encode(['foo' => 'string', 'bar' => 1]);

    $array = (new \CuyZ\Valinor\MapperBuilder())
        ->mapper()
        ->map(
            'array{foo: string, bar: int}',
            new \CuyZ\Valinor\Mapper\Source\JsonSource($json)
        );

    echo $array['foo'];
    echo $array['bar'] * 2;
} catch (\CuyZ\Valinor\Mapper\MappingError $error) {
    // Do something…
}
PHP Fatal error:  Uncaught CuyZ\Valinor\Mapper\MappingError: Could not map type `array{foo: string, bar: int}` with the given source. in /home/einar/projects/Valinor/src/Mapper/TreeMapperContainer.php:32
Stack trace:
#0 /home/einar/projects/Valinor/test.php(11): CuyZ\Valinor\Mapper\TreeMapperContainer->map()
#1 {main}
  thrown in /home/einar/projects/Valinor/src/Mapper/TreeMapperContainer.php on line 32

Replacing $signature with a Dto makes this work.

Is this supposed to work?

Constant scalar types (and compositions thereof) are not currently considered

While experimenting with the library, I noticed that constant scalar types are not currently working:

<?php

namespace My\App;

use CuyZ\Valinor\Attribute\StaticMethodConstructor;
use CuyZ\Valinor\Mapper\MappingError;
use CuyZ\Valinor\MapperBuilder;

require_once __DIR__ . '/Valinor/vendor/autoload.php';

class MyFlagWrapper
{
    /** @var 0|2|4|8|16 */
    public int $value;
}

$mapper = (new MapperBuilder())
    ->withCacheDir(__DIR__ . '/cache')
    ->mapper();

var_dump($mapper->map(MyFlagWrapper::class, ['value' => 2]));

produces:

php example.php 

Fatal error: Uncaught CuyZ\Valinor\Type\Parser\Exception\Stream\WrongTokenType: Wrong token type `CuyZ\Valinor\Type\Parser\Lexer\Token\UnionToken`, it should be an instance of `CuyZ\Valinor\Type\Parser\Lexer\Token\TraversingToken`. in /app/Valinor/src/Type/Parser/Lexer/TokenStream.php:36
Stack trace:
#0 /app/Valinor/src/Type/Parser/LexingParser.php(39): CuyZ\Valinor\Type\Parser\Lexer\TokenStream->read()
#1 /app/Valinor/src/Definition/Repository/Reflection/ReflectionPropertyDefinitionBuilder.php(132): CuyZ\Valinor\Type\Parser\LexingParser->parse('0|2|4|8|16 ')
#2 /app/Valinor/src/Definition/Repository/Reflection/ReflectionPropertyDefinitionBuilder.php(110): CuyZ\Valinor\Definition\Repository\Reflection\ReflectionPropertyDefinitionBuilder->parseType('0|2|4|8|16 ', Object(ReflectionProperty), Object(CuyZ\Valinor\Type\Parser\LexingParser))
#3 /app/Valinor/src/Definition/Repository/Reflection/ReflectionPropertyDefinitionBuilder.php(70): CuyZ\Valinor\Definition\Repository\Reflection\ReflectionPropertyDefinitionBuilder->typeFromDocBlock(Object(CuyZ\Valinor\Definition\ClassSignature), Object(ReflectionProperty))
#4 /app/Valinor/src/Definition/Repository/Reflection/ReflectionPropertyDefinitionBuilder.php(43): CuyZ\Valinor\Definition\Repository\Reflection\ReflectionPropertyDefinitionBuilder->resolveType(Object(CuyZ\Valinor\Definition\ClassSignature), Object(ReflectionProperty))
#5 /app/Valinor/src/Definition/Repository/Reflection/ReflectionClassDefinitionRepository.php(41): CuyZ\Valinor\Definition\Repository\Reflection\ReflectionPropertyDefinitionBuilder->for(Object(CuyZ\Valinor\Definition\ClassSignature), Object(ReflectionProperty))
#6 [internal function]: CuyZ\Valinor\Definition\Repository\Reflection\ReflectionClassDefinitionRepository->CuyZ\Valinor\Definition\Repository\Reflection\{closure}(Object(ReflectionProperty))
#7 /app/Valinor/src/Definition/Repository/Reflection/ReflectionClassDefinitionRepository.php(42): array_map(Object(Closure), Array)
#8 /app/Valinor/src/Definition/Repository/Cache/CacheClassDefinitionRepository.php(36): CuyZ\Valinor\Definition\Repository\Reflection\ReflectionClassDefinitionRepository->for(Object(CuyZ\Valinor\Definition\ClassSignature))
#9 /app/Valinor/src/Mapper/Tree/Builder/ClassNodeBuilder.php(35): CuyZ\Valinor\Definition\Repository\Cache\CacheClassDefinitionRepository->for(Object(CuyZ\Valinor\Definition\ClassSignature))
#10 /app/Valinor/src/Mapper/Tree/Builder/CasterNodeBuilder.php(35): CuyZ\Valinor\Mapper\Tree\Builder\ClassNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#11 /app/Valinor/src/Mapper/Tree/Builder/VisitorNodeBuilder.php(28): CuyZ\Valinor\Mapper\Tree\Builder\CasterNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#12 /app/Valinor/src/Mapper/Tree/Builder/ValueAlteringNodeBuilder.php(31): CuyZ\Valinor\Mapper\Tree\Builder\VisitorNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#13 /app/Valinor/src/Mapper/Tree/Builder/ShellVisitorNodeBuilder.php(30): CuyZ\Valinor\Mapper\Tree\Builder\ValueAlteringNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#14 /app/Valinor/src/Mapper/Tree/Builder/ErrorCatcherNodeBuilder.php(23): CuyZ\Valinor\Mapper\Tree\Builder\ShellVisitorNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#15 /app/Valinor/src/Mapper/Tree/Builder/RootNodeBuilder.php(21): CuyZ\Valinor\Mapper\Tree\Builder\ErrorCatcherNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#16 /app/Valinor/src/Mapper/TreeMapperContainer.php(57): CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell))
#17 /app/Valinor/src/Mapper/TreeMapperContainer.php(31): CuyZ\Valinor\Mapper\TreeMapperContainer->node('My\\App\\MyFlagWr...', Array)
#18 /app/example.php(21): CuyZ\Valinor\Mapper\TreeMapperContainer->map('My\\App\\MyFlagWr...', Array)
#19 {main}
  thrown in /app/Valinor/src/Type/Parser/Lexer/TokenStream.php on line 36

The same union done with string values behaves incorrectly too, but without crash:

<?php

namespace My\App;

use CuyZ\Valinor\Attribute\StaticMethodConstructor;
use CuyZ\Valinor\Mapper\MappingError;
use CuyZ\Valinor\MapperBuilder;

require_once __DIR__ . '/Valinor/vendor/autoload.php';

class MyFlagWrapper
{
    /** @var 'foo'|'bar' */
    public string $value;
}

$mapper = (new MapperBuilder())
    ->withCacheDir(__DIR__ . '/cache')
    ->mapper();

var_dump($mapper->map(MyFlagWrapper::class, ['value' => 2]));

The output should be an exception, but 2 is cast to string.

php example.php 
object(My\App\MyFlagWrapper)#74 (1) {
  ["value"]=>
  string(1) "2"
}

This is probably just a limitation of how the library operates right now

How to apply custom transformations?

Context: as of today, I map my API REST request bodies from json to DTO by using Symfony's Forms. While it works fairly well, Symfony's Form are meant to handle forms. I would like to avoid their inherited complexity and limitations since are not the best fit for my use case, so I'm looking for an alternative and I want to try out Valinor.

Problem: I have cases where in the request I receive things like "productId" and I want my DTO to contain an object of class Product that should be retrieved by doing a query by id using the ORM. Is there an hook/way that I can build upon to fulfil this use case? I was looking for some sort of way to define the transformation/mapping for those custom cases, but I didn't find anything about it in the documentation.
Similarly I have cases where I receive monetary amounts as a string like "12.03" but in my DTO I want a Money object constructed with something like new Money((float) $value), so I would need some sort of custom "type"/"mapping" for this as well.

What are Valinor recommendations in those cases?

`psalm-` and `phpstan-` phpdoc-prefix handling

Hey there,

we still have some legacy code out there which was not yet migrated to PHP 7.4 and thus, contains the following code:

final class WhateverResponse
{
     /**
      * @var string
      * @psalm-var non-empty-string
      */
     public $nonEmptyString;

     /**
      * @param string $nonEmptyString
      * @psalm-param non-empty-string $nonEmptyString
      */
     public function __construct($nonEmptyString)
     {
     }
}

The reason for this is, that we wanted to provide native types for the IDE while also providing psalm-specific types for static analysis.

This also works with @phpstan-var, @phpstan-param.

So PHPStorm did not properly supported non-empty-string for example (it always showed arguments passed to those methods as "invalid") and therefore, we used this way to narrow the type for static analysis while keeping native types for PHPStorm.

I recently tried to use valinor to map a response to that object and it was not throwing a mapping error due to the fact that the NonEmptyString-Type was not detected.


I haven't had a deeper look yet, but I guess it would be quite easy to support both @psalm- and @phpstan- prefixes while parsing docblocks?

Thoughts?

Consider using built-in types from `phpstan/phpstan` or `vimeo/psalm` directly?

The spec about how to declare annotated types changes constantly: this library seems to infer types with its own mechanisms, which is possibly why issues like #1 and #5 arise.

In order to reduce such issues, it may be a good idea to directly build on top of phpstan/phpstan or vimeo/psalm for their internal type definitions (value objects, practically):

By leveraging these, and having a "default" case that throws ("I don't know this type yet"), it would potentially be clear what is and isn't supported yet, at the cost of introducing a runtime dependency.

Support psr/simple-cache 3.0

Hey, I was wondering whether you want to support psr/simple-cache 3.0? It would require dropping support for PHP 7.4 which you might not want to do. I'm happy to submit a PR to drop support for PHP 7.4, and then another to add the return types to your cache implementations and delete stubs/Psr/SimpleCache/CacheInterface.stub and the reference to it in phpstan.neon.dist.

Example from doc not working with PHPStan

PHPStan return error
Method MyClass::getResponse() should return MyClass but returns object.

public static function getResponse(string $rawJson): self
{
        return (new \CuyZ\Valinor\MapperBuilder())
            ->mapper()
            ->map(
                self::class,
                new \CuyZ\Valinor\Mapper\Source\JsonSource($rawJson)
            );
}

Detect unused properties by allowing ArrayAccess as source

I want to detect if a property in source is sent but never ends up being used (for instance a typo). By allowing Source to be ArrayAccess could this be trivial to implement outside of Valinor (register used properties in MySource::offsetGet). I made some very small adjustments to Valinor to make it work (for my very basic DTO), but would need some more testing.

What do you think of the idea?

2022-06-08-200548_334x195_scrot

Example code does not work with library

I am trying to instantiate an object using this library, and using DateTimeInterface type hints as described in the example in the README. However, I get a CannotMapObject exception when I do. Does this library not yet support using a DateTimeInterface type hint for date times? Thanks.

Separate exceptions for field missing and cast fail

Is there a way to differentiate if the field is missing from the input array from the case when it exists, but it can't be cast?
If I'm not missing something, both use cases throw CannotCastToScalarValue exception.

It would be handy if those were separate exceptions.

Support class constants properties

As defined at phpstan here: https://phpstan.org/writing-php-code/phpdoc-types#literals-and-constants

class DtoValues {
    public const FOO = 1;
    public const FOO_BAR = 2;
    public const BAR = 'FooBar!';
}

class FooBar
{
    /**
     * @var DtoValues::FOO*
     */
    public $id;

    /**
     * @var DtoValues::BAR|null
     */
    public $title;
}

Actual use-case

Optional properties in payload.

class DtoValues {
    public const None = 'SomeNoneValueString';
}

class WithOptionalDto
{
    /**
     * @var DtoValues::None|null|int
     */
    public $priority = DtoValues::None;
}

// Payload does not need to contain `priority`, it has a default value (None).
$object = buildDto(WithOptionalDto::class, $source);

if ($object->priority !== DtoValues::None) {
    // Payload actually contains some value, lets use it.
}

Today, the error message is:

CuyZ\Valinor\Definition\Exception\InvalidPropertyDefaultValue: Default value of property WithOptionalDto::$priority is not accepted by DtoValues
image
(Screenshot taken in ReflectionPropertyDefinitionBuilder)

Notice it simply ignores the rest of the types, and is not even an UnionType as expected.

In PHP 8.1 is this a lot easier because we could use a global const which value is an object (const None = new None) and use None as a valid type. Though we are still on 7.4 and must rely on some weird strings - if we decide that this is the best approuch for optional properties.

Require `ocramius/package-versions` instead of `composer/package-versions-deprecated`

composer/package-versions-deprecated is a polyfill package designed for supporting older PHP versions: it is a replacement for ocramius/package-versions, but doesn't declare any of said API (owned by the parent ocramius/package-versions package).

It would be better, at library version, to declare a dependency on ocramius/package-versions:^2 (which runs on php:^7.4.7), rather than including the polyfill itself.

Cache not invalidated after changing parent

Steps to reproduce

<?php

// TopLevel.php

class TopLevel {
    /**
     * @var FooBar2
     */
    protected $foo;
}
<?php

// Child.php

require "TopLevel.php";

class Child extends TopLevel {

    public int $bar;

    public string $title;
}
// test.php

require "vendor/autoload.php";
require "Child.php";

$foo = (new \CuyZ\Valinor\MapperBuilder())
    ->mapper()
    ->map(Child::class, []);
  1. Run php test.php
  2. Error: The type FooBar2 for property TopLevel::$foo could not be resolved: Cannot parse unknown symbol FooBar2
  3. Now solve the issue, for instance by removing $foo from TopLevel
  4. Run php test.php and the same error is thrown, as if the property wasn't removed.

Pre-generation of signature caches

Hey there,

I recently started to use this library.
I was thinking of actually writing a CLI command for CI pipeline which searches for the TreeMapper#map usage and parses the 1st argument (FQCN) from it.

I do want to pass this FQCN to the TypeParser#parse method so that the cacheDirectory entry is being created.

What I found out is, that there is no way to actually get an instance of the TypeParser and I do really would love to avoid instantiating it by myself.

Do you have an idea and/or suggestion on how to retrieve the TypeParser instance with the settings I do pass to the MapperBuilder?

Would love to get some help here so we can pre-generate the cache entries on the filesystem during build-time.

Thanks in advance 🤙🏼

Support `@psalm-type` and `@psalm-import-type` for importing complex types from other symbols?

Background

Usage of @psalm-type and @psalm-import-type allows declaring local and foreign aliases for complex type compositions.

These types ares extremely useful when creating a large trees of array-like structures, where creating a large tree of objects would potentially be worse/wasteful from a development perspective.

Most often, these are useful when attempting to map object structures to array structures that vary slightly, or for supporting array keys that would otherwise be illegal in PHP (such as spaces in array keys, or special languages).

Related issues

This is a follow-up of:

Example / Test Case

Here's a stripped down example with a crash, useful for evaluating whether the feature is "working", if/when we work on it:

<?php

namespace My\App;

use CuyZ\Valinor\MapperBuilder;

require_once __DIR__ . '/Valinor/vendor/autoload.php';

/**
 * @psalm-type ImportedType = array{
 *   importedType: non-empty-string
 * }
 */
class SomeClassThatDeclaresAliases
{
}

/**
 * @psalm-import-type ImportedType from SomeClassThatDeclaresAliases
 *
 * @psalm-type LocalType = array{
 *   localType: int
 * }
 */
class MyArrayWrapper
{
    /** @var array{
     *     0: LocalType
     *     1: ImportedType
     * }
     */
    public array $value;
}

$mapper = (new MapperBuilder())
    ->withCacheDir(__DIR__ . '/cache')
    ->mapper();

var_dump($mapper->map(
    MyArrayWrapper::class,
    ['value' => [
        0 => ['localType' => 123],
        1 => ['importedType' => 'here be dragons']
    ]]
));

Currently crashes due to unknown types:

php example-18-imported-types.php 

Fatal error: Uncaught CuyZ\Valinor\Type\Types\UnresolvableType: The type `array{     0: LocalType     1: ImportedType }` for property `My\App\MyArrayWrapper::$value` could not be resolved: Cannot parse unknown symbol `LocalType`. in /app/Valinor/src/Definition/Repository/Reflection/ReflectionPropertyDefinitionBuilder.php:137
Stack trace:
#0 /app/Valinor/src/Definition/Repository/Reflection/ReflectionPropertyDefinitionBuilder.php(110): CuyZ\Valinor\Definition\Repository\Reflection\ReflectionPropertyDefinitionBuilder->parseType('array{     0: L...', Object(ReflectionProperty), Object(CuyZ\Valinor\Type\Parser\LexingParser))
#1 /app/Valinor/src/Definition/Repository/Reflection/ReflectionPropertyDefinitionBuilder.php(70): CuyZ\Valinor\Definition\Repository\Reflection\ReflectionPropertyDefinitionBuilder->typeFromDocBlock(Object(CuyZ\Valinor\Definition\ClassSignature), Object(ReflectionProperty))
#2 /app/Valinor/src/Definition/Repository/Reflection/ReflectionPropertyDefinitionBuilder.php(43): CuyZ\Valinor\Definition\Repository\Reflection\ReflectionPropertyDefinitionBuilder->resolveType(Object(CuyZ\Valinor\Definition\ClassSignature), Object(ReflectionProperty))
#3 /app/Valinor/src/Definition/Repository/Reflection/ReflectionClassDefinitionRepository.php(41): CuyZ\Valinor\Definition\Repository\Reflection\ReflectionPropertyDefinitionBuilder->for(Object(CuyZ\Valinor\Definition\ClassSignature), Object(ReflectionProperty))
#4 [internal function]: CuyZ\Valinor\Definition\Repository\Reflection\ReflectionClassDefinitionRepository->CuyZ\Valinor\Definition\Repository\Reflection\{closure}(Object(ReflectionProperty))
#5 /app/Valinor/src/Definition/Repository/Reflection/ReflectionClassDefinitionRepository.php(42): array_map(Object(Closure), Array)
#6 /app/Valinor/src/Definition/Repository/Cache/CacheClassDefinitionRepository.php(36): CuyZ\Valinor\Definition\Repository\Reflection\ReflectionClassDefinitionRepository->for(Object(CuyZ\Valinor\Definition\ClassSignature))
#7 /app/Valinor/src/Mapper/Tree/Builder/ClassNodeBuilder.php(35): CuyZ\Valinor\Definition\Repository\Cache\CacheClassDefinitionRepository->for(Object(CuyZ\Valinor\Definition\ClassSignature))
#8 /app/Valinor/src/Mapper/Tree/Builder/CasterNodeBuilder.php(35): CuyZ\Valinor\Mapper\Tree\Builder\ClassNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#9 /app/Valinor/src/Mapper/Tree/Builder/VisitorNodeBuilder.php(28): CuyZ\Valinor\Mapper\Tree\Builder\CasterNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#10 /app/Valinor/src/Mapper/Tree/Builder/ValueAlteringNodeBuilder.php(31): CuyZ\Valinor\Mapper\Tree\Builder\VisitorNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#11 /app/Valinor/src/Mapper/Tree/Builder/ShellVisitorNodeBuilder.php(30): CuyZ\Valinor\Mapper\Tree\Builder\ValueAlteringNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#12 /app/Valinor/src/Mapper/Tree/Builder/ErrorCatcherNodeBuilder.php(23): CuyZ\Valinor\Mapper\Tree\Builder\ShellVisitorNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#13 /app/Valinor/src/Mapper/Tree/Builder/RootNodeBuilder.php(21): CuyZ\Valinor\Mapper\Tree\Builder\ErrorCatcherNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#14 /app/Valinor/src/Mapper/TreeMapperContainer.php(57): CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell))
#15 /app/Valinor/src/Mapper/TreeMapperContainer.php(31): CuyZ\Valinor\Mapper\TreeMapperContainer->node('My\\App\\MyArrayW...', Array)
#16 /app/example-18-imported-types.php(42): CuyZ\Valinor\Mapper\TreeMapperContainer->map('My\\App\\MyArrayW...', Array)
#17 {main}

Next CuyZ\Valinor\Mapper\Tree\Exception\UnresolvableShellType: The type `array{     0: LocalType     1: ImportedType }` for property `My\App\MyArrayWrapper::$value` could not be resolved: Cannot parse unknown symbol `LocalType`. in /app/Valinor/src/Mapper/Tree/Shell.php:40
Stack trace:
#0 /app/Valinor/src/Mapper/Tree/Shell.php(57): CuyZ\Valinor\Mapper\Tree\Shell->__construct(Object(CuyZ\Valinor\Type\Types\UnresolvableType), Array)
#1 /app/Valinor/src/Mapper/Tree/Builder/ClassNodeBuilder.php(41): CuyZ\Valinor\Mapper\Tree\Shell->child('value', Object(CuyZ\Valinor\Type\Types\UnresolvableType), Array, Object(CuyZ\Valinor\Definition\NativeAttributes))
#2 /app/Valinor/src/Mapper/Tree/Builder/CasterNodeBuilder.php(35): CuyZ\Valinor\Mapper\Tree\Builder\ClassNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#3 /app/Valinor/src/Mapper/Tree/Builder/VisitorNodeBuilder.php(28): CuyZ\Valinor\Mapper\Tree\Builder\CasterNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#4 /app/Valinor/src/Mapper/Tree/Builder/ValueAlteringNodeBuilder.php(31): CuyZ\Valinor\Mapper\Tree\Builder\VisitorNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#5 /app/Valinor/src/Mapper/Tree/Builder/ShellVisitorNodeBuilder.php(30): CuyZ\Valinor\Mapper\Tree\Builder\ValueAlteringNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#6 /app/Valinor/src/Mapper/Tree/Builder/ErrorCatcherNodeBuilder.php(23): CuyZ\Valinor\Mapper\Tree\Builder\ShellVisitorNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#7 /app/Valinor/src/Mapper/Tree/Builder/RootNodeBuilder.php(21): CuyZ\Valinor\Mapper\Tree\Builder\ErrorCatcherNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#8 /app/Valinor/src/Mapper/TreeMapperContainer.php(57): CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell))
#9 /app/Valinor/src/Mapper/TreeMapperContainer.php(31): CuyZ\Valinor\Mapper\TreeMapperContainer->node('My\\App\\MyArrayW...', Array)
#10 /app/example-18-imported-types.php(42): CuyZ\Valinor\Mapper\TreeMapperContainer->map('My\\App\\MyArrayW...', Array)
#11 {main}
  thrown in /app/Valinor/src/Mapper/Tree/Shell.php on line 40

Attempting to map constant scalar values leads to `CannotResolveTypeFromUnion` rather than `MappingError`

Taking following example:

<?php

namespace My\App;

use CuyZ\Valinor\Mapper\MappingError;
use CuyZ\Valinor\MapperBuilder;

require_once __DIR__ . '/Valinor/vendor/autoload.php';

class Role
{
    /** @var 'admin'|'user' */
    public string $name;
}


$mapper = (new MapperBuilder())
    ->mapper();

try {
    $r = $mapper
        ->map(
            Role::class,
            [
                'name' => 'wrong'
            ]
        );

    var_dump($r);
} catch (MappingError $e) {
    var_dump(array_map(
        static fn (array $exceptions) => array_map(static fn (\Throwable $e) : array => [
            $e->getMessage(),
            get_class($e),
        ], $exceptions),
        $e->describe()
    ));
}

The expectation is that the MappingError is rendered (as per catch block). The current implementation throws a much deeper exception though:

 php example-error-handling.php 

Fatal error: Uncaught CuyZ\Valinor\Type\Resolver\Exception\CannotResolveTypeFromUnion: Impossible to resolve the type from the union `admin|user` with a value of type `string`. in /app/Valinor/src/Type/Resolver/Union/UnionScalarNarrower.php:40
Stack trace:
#0 /app/Valinor/src/Type/Resolver/Union/UnionNullNarrower.php(41): CuyZ\Valinor\Type\Resolver\Union\UnionScalarNarrower->narrow(Object(CuyZ\Valinor\Type\Types\UnionType), 'wrong')
#1 /app/Valinor/src/Mapper/Tree/Visitor/UnionShellVisitor.php(29): CuyZ\Valinor\Type\Resolver\Union\UnionNullNarrower->narrow(Object(CuyZ\Valinor\Type\Types\UnionType), 'wrong')
#2 /app/Valinor/src/Mapper/Tree/Visitor/AggregateShellVisitor.php(22): CuyZ\Valinor\Mapper\Tree\Visitor\UnionShellVisitor->visit(Object(CuyZ\Valinor\Mapper\Tree\Shell))
#3 /app/Valinor/src/Mapper/Tree/Builder/ShellVisitorNodeBuilder.php(27): CuyZ\Valinor\Mapper\Tree\Visitor\AggregateShellVisitor->visit(Object(CuyZ\Valinor\Mapper\Tree\Shell))
#4 /app/Valinor/src/Mapper/Tree/Builder/ErrorCatcherNodeBuilder.php(23): CuyZ\Valinor\Mapper\Tree\Builder\ShellVisitorNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#5 /app/Valinor/src/Mapper/Tree/Builder/RootNodeBuilder.php(21): CuyZ\Valinor\Mapper\Tree\Builder\ErrorCatcherNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#6 /app/Valinor/src/Mapper/Tree/Builder/ClassNodeBuilder.php(43): CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell))
#7 /app/Valinor/src/Mapper/Tree/Builder/CasterNodeBuilder.php(35): CuyZ\Valinor\Mapper\Tree\Builder\ClassNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#8 /app/Valinor/src/Mapper/Tree/Builder/VisitorNodeBuilder.php(28): CuyZ\Valinor\Mapper\Tree\Builder\CasterNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#9 /app/Valinor/src/Mapper/Tree/Builder/ValueAlteringNodeBuilder.php(31): CuyZ\Valinor\Mapper\Tree\Builder\VisitorNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#10 /app/Valinor/src/Mapper/Tree/Builder/ShellVisitorNodeBuilder.php(30): CuyZ\Valinor\Mapper\Tree\Builder\ValueAlteringNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#11 /app/Valinor/src/Mapper/Tree/Builder/ErrorCatcherNodeBuilder.php(23): CuyZ\Valinor\Mapper\Tree\Builder\ShellVisitorNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#12 /app/Valinor/src/Mapper/Tree/Builder/RootNodeBuilder.php(21): CuyZ\Valinor\Mapper\Tree\Builder\ErrorCatcherNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#13 /app/Valinor/src/Mapper/TreeMapperContainer.php(57): CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell))
#14 /app/Valinor/src/Mapper/TreeMapperContainer.php(31): CuyZ\Valinor\Mapper\TreeMapperContainer->node('My\\App\\Role', Array)
#15 /app/example-error-handling.php(25): CuyZ\Valinor\Mapper\TreeMapperContainer->map('My\\App\\Role', Array)
#16 {main}
  thrown in /app/Valinor/src/Type/Resolver/Union/UnionScalarNarrower.php on line 40

Second execution (from cache) fails

Hi,

I've been testing on master, and while first execution works in my tests, the second one always fails with:

Error : Call to undefined method My\Class::__set_state()
 /tmp/fb00b5114d7e3f185faedd48747f45f62aafcffe.php:138 

I'm running php 8.1

Named constructor support would be handy

Presently, as designed, this library (which is excellent, BTW) only supports direct instantiation of objects through PHP's constructor method. It would be handy to permit the use of named constructors, that map the specific data to a particular static named constructor. This might be an optional third argument to TreeMapper::map()?

How to map generic types?

To try out things, I attempted mapping a generic class, where the generic type parameter has not been provided:

<?php

namespace My\App;

use CuyZ\Valinor\Attribute\StaticMethodConstructor;
use CuyZ\Valinor\Mapper\MappingError;
use CuyZ\Valinor\MapperBuilder;

require_once __DIR__ . '/Valinor/vendor/autoload.php';

/** @template T of mixed */
class MyGenericWrapper
{
    /** @var T */
    public mixed $value;
}

$mapper = (new MapperBuilder())
    ->withCacheDir(__DIR__ . '/cache')
    ->mapper();

var_dump($mapper->map(MyGenericWrapper::class, ['value' => 'who knows?']));

This leads to a clear exception:

php example.php 

Fatal error: Uncaught CuyZ\Valinor\Mapper\Exception\InvalidMappingTypeSignature: Could not parse the type `My\App\MyGenericWrapper` that should be mapped: No generic was assigned to the template(s) `T` for the class `My\App\MyGenericWrapper`. in /app/Valinor/src/Mapper/TreeMapperContainer.php:48
Stack trace:
#0 /app/Valinor/src/Mapper/TreeMapperContainer.php(31): CuyZ\Valinor\Mapper\TreeMapperContainer->node('My\\App\\MyGeneri...', Array)
#1 /app/example.php(23): CuyZ\Valinor\Mapper\TreeMapperContainer->map('My\\App\\MyGeneri...', Array)
#2 {main}
  thrown in /app/Valinor/src/Mapper/TreeMapperContainer.php on line 48

The README.md example is clear about generic types where the type parameter has been set:

Valinor/README.md

Lines 258 to 259 in 396f64a

/** @var SomeCollection<SomeClass> */
private SomeCollection $classWithGeneric,

How would one request a MyGenericWrapper<int> from the mapper, as a root de-serialization target?

Question about modifying values of a source

Hi Romain!
Thank you for your great work!

I have a question about what approach should I take.

We were using version 0.3.0 of Valinor until now, and we created our custom sources for different classes, but as we are migration to 0.7.0 I found the Mapper/Source classes very useful but I want to ask you if there's anyway I can modify a value of a multidimensional array key to a (array) json_decode($value)

CONTEXT

ClientTimeline Class constructor:

/**
     * @param string $identifier
     * @param string $type
     * @param string|null $content
     * @param ClientTimelineMetadata|null $metadata
     * @param \DateTimeInterface $createdAt
     * @param \DateTimeInterface|null $updatedAt
     */
    public function __construct(
        private string $identifier,
        private string $type,
        private ?string $content,
        private ?ClientTimelineMetadata $metadata,
        private \DateTimeInterface $createdAt,
        private ?\DateTimeInterface $updatedAt,
    ) {
    }

$metadata, ClientTimelineMetadata Class constructor:

/**
     * @param bool|null $complete
     * @param DateTimeInterface $date
     */
    public function __construct(
        private ?bool $complete,
        private DateTimeInterface $date,
    ) {
    }

In Valinor 0.3.0 I used this Source:

class ClientTimelineTransformationSource implements \IteratorAggregate
{
    private array $source = [];

    public function __construct(iterable $source)
    {
        $this->source = $this->transform($source);
    }

    public array $keyMap = [
        'timeline_type'  => 'type',
        'created_at' => 'createdAt',
        'updated_at' => 'updatedAt',
    ];

    private function transform(iterable $source): array
    {
        $array = [];

        foreach ($source as $key => $value) {
            if (is_iterable($value)) {
                return $this->transform($value);
            }

            if ($key === 'metadata') {
                $array[$key] = json_decode($value, true);
            } else {
                $array[$this->keyMap[$key] ?? $key] = $value;
            }
        }

        return $array;
    }

    public function getIterator()
    {
        yield from $this->source;
    }

    /**
     * @return array
     */
    public function getSource(): array
    {
        return $this->source;
    }
}

With this source, I mapped a key to another and CamelCase the other keys, but also, for key metadata I use json_decode to decode a json string, so Valinor can Map metadata into the ClientTimeline.

For Valinor 0.7.0 I'm using this:

$returnArray = [];
                foreach ($clientTimelines as $clientTimeline) {
                    // Modify metadata json string to array
                    if (is_string($clientTimeline['metadata'])) {
                        $clientTimeline['metadata'] =  (array) json_decode($clientTimeline['metadata']);
                    }

                    $returnArray[] = (new MapperBuilder())
                        ->mapper()->map(
                            ClientTimeline::class,
                            Source::array($clientTimeline)
                                ->map(
                                    [
                                        'timeline_type' => 'type',
                                    ]
                                )
                                ->camelCaseKeys()
                        );
                }
                return $returnArray;

I was wondering is there any way I can modify the value of the metadata key into (array) json_decode($metadata) with a value modifier or something like it.

Hope this made sense.

Thank you for all your work and for your time.

All the best!

Andrés

Feat: Add naming strategy to allow mixing source/property field names

In many popular denormalizers/mappers it's common to provide a naming strategy to switch between different naming cases for e.g. under_score => camelCase. I have looked a bit inside the library, but it seems with the current setup it is not possible.

@romm maybe you have some suggestions, where I should start with this kind of feature?

Roll a 0.2.1 release containing date format fixes

As presently designed, the example code in the README file does not work as written without the patch introduced in 179ba3d. Its my suggestion that we roll a 0.2.1 release to incorporate that change (you can cherry-pick if you have not-ready features waiting in master). I want to avoid folks coming and trying the library only to discover that the example code doesn't work quite right, and walking away from a beautiful library with a ton of possibility. What do you think?

Multiline type declarations seem to confuse the library overall

Take following example:

<?php

namespace My\App;

use CuyZ\Valinor\Attribute\StaticMethodConstructor;
use CuyZ\Valinor\Mapper\MappingError;
use CuyZ\Valinor\MapperBuilder;

require_once __DIR__ . '/Valinor/vendor/autoload.php';

class MyArrayWrapper
{
    /** @var array{
     *     0: string,
     *     1: int,
     *     2: bool
     * }
     */
    public array $value;
}

$mapper = (new MapperBuilder())
    ->withCacheDir(__DIR__ . '/cache')
    ->mapper();

var_dump($mapper->map(MyArrayWrapper::class, ['value' => [0 => 'a', 1 => '1', 2 => 'true']]));

The type parser does not understand how to proceed here:

php example.php 

Fatal error: Uncaught CuyZ\Valinor\Type\Types\UnresolvableType: The type `array{` for property `My\App\MyArrayWrapper::$value` could not be resolved: Shaped array must define one or more elements, for instance `array{foo: string}`. in /app/cache/8dc7bc5e71167e9e09fdc3de1db078cad5175923.php:20
Stack trace:
#0 /app/Valinor/src/Cache/Compiled/CompiledPhpFileCache.php(70): CuyZ\Valinor\Cache\Compiled\PhpCacheFile@anonymous->value()
#1 /app/Valinor/src/Cache/ChainCache.php(33): CuyZ\Valinor\Cache\Compiled\CompiledPhpFileCache->get('class-definitio...', NULL)
#2 /app/Valinor/src/Cache/VersionedCache.php(34): CuyZ\Valinor\Cache\ChainCache->get('class-definitio...', NULL)
#3 /app/Valinor/src/Definition/Repository/Cache/CacheClassDefinitionRepository.php(33): CuyZ\Valinor\Cache\VersionedCache->get('class-definitio...')
#4 /app/Valinor/src/Mapper/Tree/Builder/ClassNodeBuilder.php(35): CuyZ\Valinor\Definition\Repository\Cache\CacheClassDefinitionRepository->for(Object(CuyZ\Valinor\Definition\ClassSignature))
#5 /app/Valinor/src/Mapper/Tree/Builder/CasterNodeBuilder.php(35): CuyZ\Valinor\Mapper\Tree\Builder\ClassNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#6 /app/Valinor/src/Mapper/Tree/Builder/VisitorNodeBuilder.php(28): CuyZ\Valinor\Mapper\Tree\Builder\CasterNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#7 /app/Valinor/src/Mapper/Tree/Builder/ValueAlteringNodeBuilder.php(31): CuyZ\Valinor\Mapper\Tree\Builder\VisitorNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#8 /app/Valinor/src/Mapper/Tree/Builder/ShellVisitorNodeBuilder.php(30): CuyZ\Valinor\Mapper\Tree\Builder\ValueAlteringNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#9 /app/Valinor/src/Mapper/Tree/Builder/ErrorCatcherNodeBuilder.php(23): CuyZ\Valinor\Mapper\Tree\Builder\ShellVisitorNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#10 /app/Valinor/src/Mapper/Tree/Builder/RootNodeBuilder.php(21): CuyZ\Valinor\Mapper\Tree\Builder\ErrorCatcherNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#11 /app/Valinor/src/Mapper/TreeMapperContainer.php(57): CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell))
#12 /app/Valinor/src/Mapper/TreeMapperContainer.php(31): CuyZ\Valinor\Mapper\TreeMapperContainer->node('My\\App\\MyArrayW...', Array)
#13 /app/example.php(26): CuyZ\Valinor\Mapper\TreeMapperContainer->map('My\\App\\MyArrayW...', Array)
#14 {main}

Next CuyZ\Valinor\Mapper\Tree\Exception\UnresolvableShellType: The type `array{` for property `My\App\MyArrayWrapper::$value` could not be resolved: Shaped array must define one or more elements, for instance `array{foo: string}`. in /app/Valinor/src/Mapper/Tree/Shell.php:40
Stack trace:
#0 /app/Valinor/src/Mapper/Tree/Shell.php(57): CuyZ\Valinor\Mapper\Tree\Shell->__construct(Object(CuyZ\Valinor\Type\Types\UnresolvableType), Array)
#1 /app/Valinor/src/Mapper/Tree/Builder/ClassNodeBuilder.php(41): CuyZ\Valinor\Mapper\Tree\Shell->child('value', Object(CuyZ\Valinor\Type\Types\UnresolvableType), Array, Object(CuyZ\Valinor\Definition\EmptyAttributes))
#2 /app/Valinor/src/Mapper/Tree/Builder/CasterNodeBuilder.php(35): CuyZ\Valinor\Mapper\Tree\Builder\ClassNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#3 /app/Valinor/src/Mapper/Tree/Builder/VisitorNodeBuilder.php(28): CuyZ\Valinor\Mapper\Tree\Builder\CasterNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#4 /app/Valinor/src/Mapper/Tree/Builder/ValueAlteringNodeBuilder.php(31): CuyZ\Valinor\Mapper\Tree\Builder\VisitorNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#5 /app/Valinor/src/Mapper/Tree/Builder/ShellVisitorNodeBuilder.php(30): CuyZ\Valinor\Mapper\Tree\Builder\ValueAlteringNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#6 /app/Valinor/src/Mapper/Tree/Builder/ErrorCatcherNodeBuilder.php(23): CuyZ\Valinor\Mapper\Tree\Builder\ShellVisitorNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#7 /app/Valinor/src/Mapper/Tree/Builder/RootNodeBuilder.php(21): CuyZ\Valinor\Mapper\Tree\Builder\ErrorCatcherNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell), Object(CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder))
#8 /app/Valinor/src/Mapper/TreeMapperContainer.php(57): CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder->build(Object(CuyZ\Valinor\Mapper\Tree\Shell))
#9 /app/Valinor/src/Mapper/TreeMapperContainer.php(31): CuyZ\Valinor\Mapper\TreeMapperContainer->node('My\\App\\MyArrayW...', Array)
#10 /app/example.php(26): CuyZ\Valinor\Mapper\TreeMapperContainer->map('My\\App\\MyArrayW...', Array)
#11 {main}
  thrown in /app/Valinor/src/Mapper/Tree/Shell.php on line 40

Note: this is just #5, but with different formatting.

One of the most complex types from one of customer projects (could be useful for future expansion of type parsing) :

/**
 * @psalm-immutable
 *
 * @psalm-import-type VariantRawArrayType from Variant
 * @psalm-import-type AssortmentRawArrayType from Assortment
 * @psalm-import-type AttachmentRawArrayType from Attachment
 * @psalm-import-type AwardRawArrayType from Award
 * @psalm-import-type BrandRawArrayType from Brand
 * @psalm-import-type PriceRawArrayType from Price
 * @psalm-import-type TaxonomyRawArrayType from Taxonomy
 *
 * @psalm-type ProductRawArrayType = array{
 *    id: non-empty-string,
 *    isActive: bool,
 *    sku: string,
 *    name: string,
 *    description: ?string,
 *    shortDescription: ?string,
 *    synonyms: string,
 *    clientFields: array<non-empty-string, mixed>,
 *    brand: array<non-empty-string, mixed>,
 *    hazardStatements: list<string>,
 *    precautionaryStatements: list<string>,
 *    mainTaxonomy: TaxonomyRawArrayType,
 *    taxonomies: list<TaxonomyRawArrayType>,
 *    taxonomyListingIds: list<non-empty-string>,
 *    attachments: list<AttachmentRawArrayType>,
 *    assortments: list<AssortmentRawArrayType>,
 *    variants: list<VariantRawArrayType>,
 *    mainVariant: VariantRawArrayType,
 *    relatedProductIds: array<string, list<non-empty-string>>,
 *    variationName: ?string,
 *    skuProvidedBySupplier: ?string,
 *    awards?: ?list<AwardRawArrayType>,
 *    created: string,
 *    updated: string,
 *    prices: list<PriceRawArrayType>
 * }
 */
final class Product
{
    // <snip>

    /**
     * @psalm-param array{
     *     _score: float,
     *     _source: ProductRawArrayType
     * } $data
     */
    public static function fromArray(array $data): self
    {
        // <snip>
    }
}

misc: change attribute arguments compilation

Instead of serializing all arguments, only those with an object value should be serialized: scalar values should be var_export'ed.

Reference:

if (count($arguments) > 0) {
$arguments = serialize($arguments);
$arguments = 'unserialize(' . var_export($arguments, true) . ')';
return "new $name(...$arguments)";
}

Should look like:

private function defaultValue(ParameterDefinition $parameter): string
{
$defaultValue = $parameter->defaultValue();
return is_scalar($defaultValue)
? var_export($parameter->defaultValue(), true)
: 'unserialize(' . var_export(serialize($defaultValue), true) . ')';
}

License in composer.json

According to the LICENSE file this repository is MIT licensed, however in composer.json it says "GPL".

Allow inferring of classes with children classes

As stated in #136 (comment), the following should work:

try {
    $result = (new \CuyZ\Valinor\MapperBuilder())
        ->infer(DateTime::class, fn() => SomeClassThatInheritsDateTime::class)
        ->mapper()
        ->map(SomeClass::class, [/* … */]);
} catch (MappingError $error) {
    // …
}

Strengthen tests for datetime mapping

As stated by @keichinger in #40 (comment), the test-cases below can lead to false-positive, mainly because every case is mapping to the exact same datetime.

public function test_datetime_properties_are_converted_properly(): void
{
$dateTimeInterface = new DateTimeImmutable('@1356097062');
$dateTimeImmutable = new DateTimeImmutable('@1356097062');
$dateTimeFromTimestamp = 1356097062;
$dateTimeFromTimestampWithFormat = [
'datetime' => 1356097062,
'format' => 'U',
];
$dateTimeFromAtomFormat = '2012-12-21T13:37:42+00:00';
$dateTimeFromArray = [
'datetime' => '2012-12-21 13:37:42',
'format' => 'Y-m-d H:i:s',
];
$mysqlDate = '2012-12-21 13:37:42';
$pgsqlDate = '2012-12-21 13:37:42.123456';
try {
$result = $this->mapperBuilder->mapper()->map(AllDateTimeValues::class, [
'dateTimeInterface' => $dateTimeInterface,
'dateTimeImmutable' => $dateTimeImmutable,
'dateTimeFromTimestamp' => $dateTimeFromTimestamp,
'dateTimeFromTimestampWithFormat' => $dateTimeFromTimestampWithFormat,
'dateTimeFromAtomFormat' => $dateTimeFromAtomFormat,
'dateTimeFromArray' => $dateTimeFromArray,
'mysqlDate' => $mysqlDate,
'pgsqlDate' => $pgsqlDate,
]);
} catch (MappingError $error) {
$this->mappingFail($error);
}
self::assertInstanceOf(DateTimeImmutable::class, $result->dateTimeInterface);
self::assertEquals($dateTimeInterface, $result->dateTimeInterface);
self::assertEquals($dateTimeImmutable, $result->dateTimeImmutable);
self::assertEquals(new DateTimeImmutable("@$dateTimeFromTimestamp"), $result->dateTimeFromTimestamp);
self::assertEquals(new DateTimeImmutable("@{$dateTimeFromTimestampWithFormat['datetime']}"), $result->dateTimeFromTimestampWithFormat);
self::assertEquals(DateTimeImmutable::createFromFormat(DATE_ATOM, $dateTimeFromAtomFormat), $result->dateTimeFromAtomFormat);
self::assertEquals(DateTimeImmutable::createFromFormat($dateTimeFromArray['format'], $dateTimeFromArray['datetime']), $result->dateTimeFromArray);
self::assertEquals(DateTimeImmutable::createFromFormat(DateTimeObjectBuilder::DATE_MYSQL, $mysqlDate), $result->mysqlDate);
self::assertEquals(DateTimeImmutable::createFromFormat(DateTimeObjectBuilder::DATE_PGSQL, $pgsqlDate), $result->pgsqlDate);
}

Changing each case to a different datetime everytime would strengthen the tests and ensure the expected behaviour is correct.

Do you supports default property value for readonly properties?

Example:

class Acme {
  public function __construct(
      #[DefaultValue('bar')]
      private readonly string $foo,
      private readonly string $baz,
  ) {}
}

Let's map it:

map(Acme::class, [
    'baz' => 'test'
]);

Expected result:

[
  'foo' => 'bar',
  'baz' => 'test'
]

m?

Consider making most internal API `@internal` before moving to `1.0.0`

Currently, most of the classes in the package are public (no @internal declared), but there are loads of details inside the Definition, Library, Mapper, Type and Utility namespaces that are likely fit for being marked @internal from the start.

While that will indeed reduce the API that third-parties can rely upon, it will give you more runway to change the API if/when needed, and then to relax this boundary when you call a component stable enough for others to tinker with it.

[0.7] More specific registered constructor is not called

Hi,

I have hierarchy of objects that look like this one:

class A {
  private InnerClass $inner;
}

class InnerClass  {
  private int $property1;
  private int $property 2;
  
  public function __construct(int $property1) {
    $this->property1 = $property;
    $this->property2 = 42;
  }

  public static function customConstructor(int $property1, int $property2): self {
    $result = new self($property1);
    $result->property2 = $property;
    return $result;
  }
}

With 0.7, I register my constructor trough $mapperBuilder->registerConstructor(B::customConstructor(...))

Then I try to map A, but the native constructor is always called and not the custom one even though it's more specific (that was working on 0.6).

default values in the constructor

HI

I tried to create object with default value

final class SomeClass
{
public function __construct(private string $var='aa')
{

}

}

with empty input data

(new \CuyZ\Valinor\MapperBuilder())
->mapper()
->map(
SomeClass::class,
[]
);

and got CannotMapObject Exception

It seems to me that in this case have to be assignment of a default value not exception. How do You Think?

Classes with arrays of classes dont work in PHP7.4

I have a series of classes with multiples properties, when its a string or other class there are no problem.
But when a class have an array of other classes the mapper ignores that

Example:
Raw Json: '{"urls":[{"expanded_url":"https://example.com"}]}'
Class to be mapped:

<?php

namespace App\Api\Twitter\Hint;

final class Entities
{
    /** @var Url[] $urls */
    public array $urls;

    public function __construct(
        /** @var Url[] $urls */
        array $urls
    )
    {
        /** @var Url[] $urls */
        $this->urls=$urls;
    }

    /**
     * @return Url[]
     */
    public function getUrls(): array
    {
        return $this->urls;
    }
}

And

<?php

namespace App\Api\Twitter\Hint;

class Url
{
    public string $expanded_url;

    public function __construct(
        string $expanded_url
    )
    {
        $this->expanded_url=$expanded_url;
    }

    /**
     * @return string
     */
    public function getExpandedUrl(): string
    {
        return $this->expanded_url;
    }
}

property urls converts in a normal array with a normal array inside. I tested the class Url and work without problem. So, the problem appears when used with an array of classes.

Named constructor that received input as array

Hi,

I've an object which deserialization can only be done in a constructor that receive all sub elements as an array.

Something like

{
  "foo": {
     "test": true,
     "other": "yes", 
     "something_else": false
   }
}

That is handled by :

class Foo {
  public static function fromArray(array $data) {
    $result = new self();
    $result->somethingSuperStrange = $data['test'] ?? $data['something_else']
    return $result;
  }
}

Don't mind the something super strange as this is not my real use case, I really need to handle some weird data.

Can something like this be done with valinor ?

Expand API of `CuyZ\Valinor\Mapper\Tree\Message\Message` to allow for i18n and API customization?

Currently, Message is only an instance of Stringable:

interface Message extends Stringable
{
}

When using this library, you can expect the most common usage to be (pseudo-code):

try {
    parse_validate_and_map($request);
} catch (MappingError $failed) {
    response_code(422);
    mapping_error_to_failure_response($failed);
}

In order to effectively write mapping_error_to_failure_response(), especially considering concerns like translation, i18n, and general re-shaping of the response, it would be helpful to have:

  • the internal message (like now), valid for loggers and such
  • a declared code (currently, Exception#getCode() is used, but unsure if it is stable long-term?)
  • the original value (which failed mapping - at the level we're at) - this is useful for interpolating in error messages
  • the wished type (to which we failed to cast/map towards) - this is useful for interpolating in error messages
  • the logical path of the value, not the string as key in MappingError, useful for processing/filtering errors later on

In practice, the use-case is mostly around i18n of error messages, plus providing a bit more information on each Message.

A good example of such serialization is https://datatracker.ietf.org/doc/html/rfc7807

How can I add the path to a Message

I'd like to add the subject of the issue so that catching the MappingError I can compose an API response similar to

{
    "issues": [
        {
            "subject": "my_subject",
            "message": "Want is wrong with the subject"
        }
    ]
}

I'm throwing a custom Exception like this one in the mapping signature Object constructor

use CuyZ\Valinor\Mapper\Tree\Message\Message;

use function implode;
use function sprintf;

class InvalidEnumValue extends \DomainException implements Message
{
    /**
     * @param list<Enum> $enumValues
     */
    public static function forEnum(array $enumValues, string $value): self
    {
        return new self(sprintf('Value \'%s\' is not valid, valid values are %s', $value, '\''
            . implode('\', \'', $enumValues) . '\''));
    }
}

Do you have any suggestion?

By the way the reason why I need to throw a custom exception here is because the mapper isn't able to map a string to an \MyCLabs\Enum\Enum. The issue seems to be the current Psalm annotations inside \MyCLabs\Enum\Enum.
Are Psalm annotations used in some way to map the object?

Provide generic template value for registerConstructor function

Sorry for all the issues, I really love the library! 😅

Might be related to #8, though that was when @template was used at root.

<?php

require "vendor/autoload.php";

class FooBar {
    public function __construct(public int $id) {}
}

/**
 * @template T
 */
class Bar
{
    /**
     * @var T
     */
    public $obj;

    /**
     * @param T $obj
     */
    public function __construct($obj) {
        $this->obj = $obj;
    }
}

class Foo
{
    /**
     * @var Bar<FooBar>
     */
    public Bar $bar;
}

$mapper = (new \CuyZ\Valinor\MapperBuilder())
    ->registerConstructor(

        /**
         * @template T
         * @param class-string<T> $className
         * 
         * @return Bar<T>
         */
        #[\CuyZ\Valinor\Mapper\Object\DynamicConstructor]
        function ($className, $id): Bar {
            $obj = new $className($id);
            return new Bar($obj);
        }
    )
    ->mapper();

$foo = $mapper->map(Foo::class, [
    'bar' => 2
]);

print $foo->bar->obj->id;
  • This is not a real-life example

Compiled PhpCacheFile fails with attributes which has new initializers

I was playing with this library today making a bundle for Symfony applications and discovered that if the property has an attribute that has nested attributes/objects will generate invalid cached PHP file code.

Here is how to reproduce:

Click to expand!
<?php

use CuyZ\Valinor\MapperBuilder;

require_once __DIR__ . '/vendor/autoload.php';

#[Attribute]
class Foo {
    public function __construct(public array $objects)
    {
    }
}

class Bar {
    public function __construct(public readonly string $value)
    {

    }
}


class DtoObject {
    #[Foo([
        new Bar('aaa'),
        new Bar('bbb'),
        new Bar('ccc'),
    ])]
    public string $field;
}

$result = (new MapperBuilder())
    ->mapper()
    ->map(DtoObject::class, ['field' => 'value']);

var_dump($result);

Here is generated PhpCacheFile:

Click to expand!
<?php // Generated by CuyZ\Valinor\Cache\Compiled\CompiledPhpFileCache
return new class($this->compiler instanceof \CuyZ\Valinor\Cache\Compiled\HasArguments ? $this->compiler->arguments() : []) implements \CuyZ\Valinor\Cache\Compiled\PhpCacheFile {
    /** @var array<string, mixed> */
    private array $arguments;
    
    public function __construct(array $arguments)
    {
        $this->arguments = $arguments;
    }

    public function value()
    {
        return new \CuyZ\Valinor\Definition\ClassDefinition(
    'DtoObject',
    'DtoObject',
    CuyZ\Valinor\Definition\EmptyAttributes::get(),
    new \CuyZ\Valinor\Definition\Properties(new \CuyZ\Valinor\Definition\PropertyDefinition(
    'field',
    'DtoObject::$field',
    CuyZ\Valinor\Type\Types\NativeStringType::get(),
    false,
    NULL,
    true,
    new \CuyZ\Valinor\Definition\AttributesContainer(...[new Foo(...array (
  0 => 
  array (
    0 => 
    Bar::__set_state(array(
       'value' => 'aaa',
    )),
    1 => 
    Bar::__set_state(array(
       'value' => 'bbb',
    )),
    2 => 
    Bar::__set_state(array(
       'value' => 'ccc',
    )),
  ),
))])
)),
    new \CuyZ\Valinor\Definition\Methods()
);
    }

    public function isValid(): bool
    {
        return (\filemtime('/tmp/a.php') === 1640628842);
    }
};

At first, I was trying to figure out from where the heck this __set_state method was coming when I found that var_export somehow generates this kind of code, so right now I am not really having any idea how to fix this...

https://www.php.net/manual/en/function.var-export.php#refsect1-function.var-export-notes

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.