orisai / object-mapper Goto Github PK
View Code? Open in Web Editor NEWRaw data mapping to validated objects
License: Mozilla Public License 2.0
Raw data mapping to validated objects
License: Mozilla Public License 2.0
During metadata validation, check object can be constructed via ObjectCreator
https://wiki.php.net/rfc/fibers
Processor and rules array of and list of can be asynchronous. And if not with fibers, swoole will help :)
https://wiki.php.net/rfc/enumerations
Support native enumerations (only backed, non backed will throw exception)
Types are representation of whole VO structure and are also able to track exact keys which are invalid. In order to check which keys were invalid, user has to compare invalid keys with sent data and that's impractical.
All the Type methods which mark value invalid should be required to also add that value to Type so it could be rendered together with invalid key by Formatter.
In this stage we should resolve the problem to satisfaction just in Type
. Optimal rendering in ErrorFormatter
could be quite complex and for now only minimal implementation in VisualErrorFormatter
should be enough to verify solution - something like key: string (received int(123) instead)
Add trimming string values to support error-prone user-given form inputs
StringValue()
- parameter trim=bool
, default false
BoolValue() castBoolLike
, FloatValue() castNumericString
, IntValue() castNumericString
, NullValue() castEmptyString
Options
dynamic context to change all of above dynamically in runtime. e.g. StringDynamicContext
with trim=true
would enable all processed StringValue()
to trim values. trim=false
would disable all. Differentiating between implicit and explicit parameters is probably not needed.cc @mrceperka
No response
Since MappedObject is just interface, annotations/attributes don't have to exist and we have CreateWithoutConstructor, any mapped object can be used directly, without using object mapper. To make object mapper fully optional dependency for such object, we should extract MappedObject interface into separate repository
Split ObjectCreator into two methods - one for instantion, second for reflection check
DefaultObjectCreator should also check if constructor is public, if used by object mapper. Other implementations may allow other ways.
orisai/openapi currently fails on creation of whole Type structure of the OpenAPI class if all nodes are allowed, because it creates more than ~10k objects and PHP fails with Exit Code: 139(Segmentation violation)
While it could be fixed by PHP, this whole structure is unreasonable. Instead, we could do this:
It may be useful to support pseudo-tpyes like numeric-string
, so constructions like string&&int<acceptNumericString>
are not needed.
e.g. IntRule
can replace castNumericString=true/false
option with numericString=require/optionally/never
.
TODO - list all rules which should be modified
Reasoning: #10 (comment)
During metadata validation, check whether default values are valid according to validation rules
Needs:
Any errors should raise only warning, because things like options context may still break the defaults
Objects can be written as default values via promoted properties or #[DefaultValue]
. We should make sure they are properly supported. For each mapped object, default object should be different instance.
DefaultValuesPrinter should print object
class Example extends MappedObject
{
/** @var list<User> */
#[ArrayOf(
new EntityFromId('user', User::class, new IntValue())
)]
public array $users;
}
Callbacks overriding should work the same way as methods usually work
e.g. BackedEnumRule should force structure to re-validate when class changes (may be deleted, not backed, not enum, ...)
Args resolvers are callbacks/fields/docs/modifiers
For apps without DIC and simpler tests setup
$setup = new ProcessorSetup();
$setup->withRuleManager(...)
->withObjectCreator(...);
$processor = $setup->build();
So metadata could be validated accross all object in project, instead of when object is first used via processor
Newest phpstorm supports basic @template annotations. Let's drop custom .phpstorm.meta and @phpstan-template for supported cases.
Thanks to new in initializers, compound types will be possible to be written as attributes, we should support them then https://wiki.php.net/rfc/new_in_initializers
Same classes as for annotations can be used, we just have to modify metadata reading and write test case for differences :)
Auto-initialization of objects may lead to infinite loops in case of self-references. Needs test for verification.
Metadata-based detection is not possible - even object with all properties required may be still initializable due to before class callback. If no solution is found, feature may be just removed.
Solving it in runtime may not be viable - creating first object in structure and omiting same one deeper in structure is not deterministic behavior.
object-mapper/src/Processing/DefaultProcessor.php
Lines 428 to 442 in 29a0d58
@MappedObjectValue(Input::class, tryAutoInit=true)
Support manual creation of objects along with auto mapping
#[CreateWithoutConstructor]
final class ExampleInput extends MappedObject
{
#[StringValue]
public string $example;
public function __construct(string $example)
{
$this->example = $example;
}
}
$input = $processor->process(['example' => 'string'], ExampleInput::class);
$input = new ExampleInput('string');
Map fields from flat structure to objects hierarchy
Field names of root and all levels of proxied objects must be checked for name collisions
Proxy should not be allowed to be contained in other rules?
namespace Example\Orisai\ObjectMapper;
use Orisai\ObjectMapper\Attributes\Expect\MappedObjectValue;
use Orisai\ObjectMapper\Attributes\Expect\StringValue;
use Orisai\ObjectMapper\Attributes\Modifiers\FieldName;
use Orisai\ObjectMapper\MappedObject;
use Orisai\ObjectMapper\Processing\Processor;
final class Example extends MappedObject
{
#[MappedObjectValue(Proxied::class, proxy: true)]
public Proxied $proxy;
#[MappedObjectValue(Proxied::class, proxy: true, proxySeparator: '@')]
#[FieldName('changed-name')]
public Proxied $anotherProxy;
}
final class Proxied extends MappedObject
{
#[StringValue]
public string $example;
}
$processor;
assert($processor instanceof Processor);
$processor->process([
'proxy-example' => 'foo',
'changed-name@example' => 'foo',
], Example::class);
Currently, Type is available in second parameter of callback via Context.
Currently only requirement for meta validation to pass is for object to implement MappedObject. But since we support inheritance, interfaces and traits, not only class implementing MappedObject may use object mapper meta.
We should check if meta is defined in:
e.g. following case is against LSP and should be disallowed. Class A does not implement MappedObject but can be mapped when B which extends it and implements MappedObject is used.
#[After("method")]
class A {}
class B extends A implements MappedObject {}
Just '1.234'
, no fancy stuff likes spaces and commas. Because it's format used for numeric-string in all languages anyway.
Validate if each of them is defined above valid target
While annotations and attributes validate their targets, we should have an independent way, because:
Currently are private callbacks and properties supported only in final classes. To support them on any classes, following must be resolved:
This will be better as part of a separate package for value objects
Move array cache from MetaLoader to DefaultProcessor and scope it for single processor run
Properties are invariant (and mapped fields should be also) and as such should not be changed to prevent compatibility breaks
final class Example extends MappedObject {
/** @var array{foo: string, bar: string} */
#[AllOf([
new ArrayShape([
'foo' => new StringValue(),
'bar' => new MixedValue()
]),
new ArrayShape([
'bar' => new StringValue(),
], allowExtraProps: true),
])]
public array $merged;
/** @var array{foo: string, bar: string} */
#[AllOf([
new ArrayShape(FullObject::class),
new ArrayShape(PartialObject::class, allowExtraProps: true),
])]
public array $merged2;
}
$processor->process([
'merged' => [
'foo' => 'string',
'bar' => 'string',
],
'merged2' => [
'foo' => 'string',
'bar' => 'string',
],
], Example::class);
(new Date()).toISOString()
'2022-11-31T11:00:00.000Z'
$d = DateTimeImmutable::createFromFormat('Y-m-d\TH:i:s.v\Z', '2022-10-31T11:00:00.000Z', new \DateTimeZone('UTC'));
var_dump($d);
$d = $d->setTimezone(new \DateTimeZone('Europe/Prague'));
var_dump($d);
PHP 8.1 is comming with readonly properties which are perfect for ValueObject usecase. Full support needs some changes:
@Default()
annotation/attribute should be introduced
It would be nice to have a guide how to best integrate object-mapper within slim app.
For REST APIs is useful to be able distinguish whether value was not sent or was sent and is null in order to support PATCH requests. That's currently achieved by setting $options->setRequiredFields($options::REQUIRE_NONE);
which makes all the fields optional, unsets defaults from object and uses isset($object->fieldName)
to check difference between null
and uninitialized
.
Problem is the isset($object->propertyName)
part - while it's convenient to use language construct for that, isset()
should return false
for null - IDEs and static analysis tools use that presumption and we shouldn't ignore it. Also possibility is that PHP 8.2 will introduce isInitialized($object->propertyName)
and removal of current behavior may be too complicated by that time.
For now, $object->isInitialized('propertyName')
should be enough. For static analysis may be custom rule or pseudo-type property-string
introduced.
Implementation should also remove magic from __set
, __get
and __isset
and replace it with ObjectHelpers::strict*
methods.
Related PHP feature php/php-src#7029
Continue of #10
We should not need to track whole data structure in case it does not match our rules.
Any object can reference, directly or indirectly, itself. At bare minimum, metadata loading should work.
Each validation path should have their own options and types instances
Currently if processor is given an output from json_decode($value)
, an stdClass
is casted to array
in case it's in place of a MappedObjectValue
. But stdClass
makes sense to handle also in case of an ArrayOf
and a ListOf
.
Handling should be also available in Value
in case printer decides to print an invalid value.
We should also consider that fix on user side is as simple as json_decode($value, true)
and that it may work only for some formats:
stdClass
and adds an extra properties.SimpleXMLElement
and we have no way of casting it to an array, handling attributes or printing errors while maintaining the original xml structureNo response
No response
For value objects support - like email, url, phone number, credit card number.
Value objects may be whole different library, with object mapper being an optional feature.
items
key accepts only values of items
(expects the value of field to be given directly, not in array of ['field' => 'value']
#[SingleFieldObject]
final class SingleFieldInput implements MappedObject
{
#[ListOf(item: new MappedObjectValue(ItemInput::class))]
public array $items;
}
If parent object supports to be created without object mapper, child should not break this promise. Opposite applies as well. One object in hierarchy cannot request valdiation dependencies in constructor while other expects already valid values.
If any class, interface or trait defines __construct() and uses CreateWithoutConstructor:
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.