cuyz / valinor Goto Github PK
View Code? Open in Web Editor NEWPHP library that helps to map any input into a strongly-typed value object structure.
Home Page: https://valinor.cuyz.io
License: MIT License
PHP library that helps to map any input into a strongly-typed value object structure.
Home Page: https://valinor.cuyz.io
License: MIT License
When the source is empty, is the mapper not able to set null
to nullable property.
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
Foo::$note
should be assigned null
as value to be consistent with example Bar
, but see my other issue here: #113.
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.
According to the LICENSE file this repository is MIT licensed, however in composer.json it says "GPL".
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?
Hi,
It seems that having two infer registered do not work.
I've done a "as simple as possible" repro code here : https://github.com/magnetik/valinor-repro
When I register A before B I have an error on B, and when I register B before A, I have an error on A.
Is there something I'm missing?
<?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, []);
php test.php
FooBar2
for property TopLevel::$foo
could not be resolved: Cannot parse unknown symbol FooBar2
$foo
from TopLevel
php test.php
and the same error is thrown, as if the property wasn't removed.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.
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
As revealed in #114, circular references should be handled better, especially to prevent infinite loop.
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).
This is a follow-up of:
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
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.
Valinor/tests/Integration/Mapping/Type/DateTimeMappingTest.php
Lines 18 to 60 in f12ef39
Changing each case to a different datetime everytime would strengthen the tests and ensure the expected behaviour is correct.
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?
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
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?
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 :)
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,
) {}
}
Instead of serializing all arguments, only those with an object value should be serialized: scalar values should be var_export
'ed.
Reference:
Should look like:
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
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) {
// …
}
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;
}
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 byDtoValues
(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.
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?
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?
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)
);
}
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?
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 🤙🏼
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 ?
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?
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()
?
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>
}
}
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.
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).
Psalm supports syntax like this one:
/**
* @psalm-type Foo = array{b: int}
* @psalm-type Bar = array{a: int}
* @return Foo&Bar
*/
function foo() {
return ['a' => 1, 'b' => 2];
}
This library should be able to support it as well.
Reference: https://twitter.com/orklah/status/1478027768105480194
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.
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:
<?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:
<?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
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?
Currently, the compilation of a class definition will detect file modification in order to re-run the compilation, see:
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.
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.
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?
Currently, Message
is only an instance of Stringable
:
Valinor/src/Mapper/Tree/Message/Message.php
Lines 9 to 11 in 396f64a
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:
Exception#getCode()
is used, but unsure if it is stable long-term?)string
as key in MappingError
, useful for processing/filtering errors later onIn 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
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
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.
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.
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
.
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
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:
Lines 258 to 259 in 396f64a
How would one request a MyGenericWrapper<int>
from the mapper, as a root de-serialization target?
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?
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?
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.
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;
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)
);
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
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.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.