GithubHelp home page GithubHelp logo

orisai / object-mapper Goto Github PK

View Code? Open in Web Editor NEW
9.0 9.0 1.0 1.08 MB

Raw data mapping to validated objects

License: Mozilla Public License 2.0

PHP 99.59% Makefile 0.41%
api array conversion hydrator mapper mapping object orisai parser parsing php schema validation validator

object-mapper's People

Contributors

mabar avatar mrceperka avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

Forkers

mrceperka

object-mapper's Issues

Support alternative meta sources

  • json source - so libs like orisai/openapi can provide rules without annotations being installed
  • modify compound rules to accept arrays as well as inner objects - ExamplesDoc, LinkDoc, CompoundRule, ArrayOfRule, ListOfRule, EntityFromIdRule

Types - invalid value tracking

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)

Trimming empty string values

Description

Add trimming string values to support error-prone user-given form inputs

  • StringValue() - parameter trim=bool, default false
  • Rules casting from string should also trim value, when casting is enabled (without any additional option). Specifically 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.

Addition information

cc @mrceperka

Related issues

No response

Object mapper contracts

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

  • Create orisai/object-mapper-contracts
  • Move MappedObject interface

PHP is crashing hard on large Type structures

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:

  • MappedObjectType should be replaced with a lazy implementation - only when MappedObjectType is used, its inner objects are created. This would keep inicialized structure only in two levels of mapped objects at a time.
  • When processing Type structure (mostly in Printer), types must be dereferenced as soon as we obtain all their inner types, so PHP could destroy them
  • In Printer, write only first level of MappedObjectType on error, so we don't have to print stupidly large structures

Better support for pseudo-types

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)

Validate default values

During metadata validation, check whether default values are valid according to validation rules

Needs:

  • #39 resolved
  • processor dependency
  • "all is optional" validation mode set

Any errors should raise only warning, because things like options context may still break the defaults

Objects as default values

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

Efficient validation of multiple queried values rule

  • EfficientMultiValueValidationRule interface (implemented by EntityFromIdRule)
  • for n+1 query problem solution
  • e.g. for input of list resulting into list querying in a single query
  • support in ArrayRule keys and values and ListRule values
class Example extends MappedObject
{

	/** @var list<User> */
	#[ArrayOf(
		new EntityFromId('user', User::class, new IntValue())
	)]
	public array $users;

}

Invert CreateWithoutConstructor behavior

Callbacks covariance and contravariance

Callbacks overriding should work the same way as methods usually work

  • Changing meta may be impossible, same as in #55
  • Input parameters can be less specific (callback-specific rules may apply)
  • Return type may be more specific (callback-specific rules may apply)
  • Callbacks should be validated in scope of all classes which define or change them

Add cache dependencies via args resolvers

  • Each args resolver should be able to provide list for metadata invalidation
  • If possible, referenced constants fro other classes should result into referenced class being included too (e.g. from ArrayEnumRule)

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

Benchmark improvements

  • Drop PHPUnit TestCase usage - replace with #8
  • Use persistent cache to measure difference between first and other iterations

ProcessorSetup class

For apps without DIC and simpler tests setup

$setup = new ProcessorSetup();
$setup->withRuleManager(...)
	->withObjectCreator(...);
$processor = $setup->build();

Check DefaultProcessor::handleMissingFields() for infinite loops

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.

// Try to initialize object from empty array when no data given
// Mapped object in compound type is not supported (allOf, anyOf)
// Used only in default mode - if all or none values are required then we need differentiate whether user sent value or not
$mappedObjectArgs = $propertyMeta->getRule()->getArgs();
assert($mappedObjectArgs instanceof MappedObjectArgs);
try {
$data[$missingField] = $initializeObjects
? $this->process([], $mappedObjectArgs->type, $options)
: $this->processWithoutMapping([], $mappedObjectArgs->type, $options);
} catch (InvalidData $exception) {
$type->overwriteInvalidField(
$missingField,
InvalidData::create($exception->getType(), Value::none()),
);
}

Create without constructor

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');

Proxy objects

Map fields from flat structure to objects hierarchy

Field names of root and all levels of proxied objects must be checked for name collisions

  • forbid cyclic references? inner object names must be resolved before root object
  • cache must be invalidated for root object when proxied object is changed (only the shared references and names cache)

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);

Specific Type implementation for callbacks

Currently, Type is available in second parameter of callback via Context.

  • It may be moved to separate parameter, to be checked by object mapper for specific implementation
  • Types could be generic and checked by phpstan - useful for anyof, allof, arrayof a listof rules
  • User can decide to use different Type in callback than the default one, Type checks should allow them. Specific Type is only needed for code completition for user, object mapper does not need them

Forbid meta outside of context of MappedObject (LSP-compatibility)

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:

  • Class which implements MappedObject
  • Trait used in a class which implements MappedObject
  • Interface which extends MappedObject

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 {}

Normalize casting to numeric types

Just '1.234', no fancy stuff likes spaces and commas. Because it's format used for numeric-string in all languages anyway.

  • in int rule
  • in float rule

Meta targets - class, property, method, ...

Validate if each of them is defined above valid target

  • Rules - only above property
  • Callbacks - above class or property (all of them)
  • Modifiers - above class or property (each individually)
  • Docs - above class or property (each individually)

While annotations and attributes validate their targets, we should have an independent way, because:

  • each annotation/attribute has to set target individually
  • possible other meta sources do not have such option

Private callbacks and properties

Follow up of #30 and #31

Currently are private callbacks and properties supported only in final classes. To support them on any classes, following must be resolved:

  • meta validation must distinguish between classes in hierarchy
  • - for each method/property must be used declaring class, not current class
  • - multiple private methods/properties with same name must work (binding needs classname for private methods/properties)
  • - public and protected methods must be validated in context of both defining and overriding class (to find source of error as well as ensuring overriding method e.g. changing parameters count from 1 to 2 is not broken) - continues in #56
  • - public and protected properties should not allow to be redefined (or at least their rules cannot be changed) - continues in #55
  • - private properties with same must enforce different field name to avoid collisions
  • before call must be method/property binded to context of class which defines it
  • ! ensure metadata specify exact source - it matters whether annotation/attribute was defined on parent or child class !

Drop URL rule

This will be better as part of a separate package for value objects

Single run-scoped array cache

Move array cache from MetaLoader to DefaultProcessor and scope it for single processor run

  • recursive calls of processor for initialization of inner objects must use the same array cache (otherwise would be this cache useless)
  • should help cleaning up memory after processing is finished

Fields invariance

Properties are invariant (and mapped fields should be also) and as such should not be changed to prevent compatibility breaks

  • Public and protected properties cannot have their meta overriden by child class
  • Meta source cannot override nor change meta for property from different meta source meta sources currently replace each other completely

Array shape

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);

readonly properties and default annotation/attribute support

PHP 8.1 is comming with readonly properties which are perfect for ValueObject usecase. Full support needs some changes:

  • check whether property was already (wrongly) written into by a callback and inform it cannot be overwritten
  • because defaults are not possible, a @Default() annotation/attribute should be introduced
    • not allowed in combination with real default value
    • default value will be stored in metadata, same way as the real default value is now

VO - less magical is initialized checks

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

Cyclical references

Any object can reference, directly or indirectly, itself. At bare minimum, metadata loading should work.

Clone type context and options properly

Each validation path should have their own options and types instances

  • Prerequirement for dynamic contexts for callbacks and run-scoped rule options
  • Possible prerequirement for run-only array cache - #48
  • Properly solves #43 - types printing is still uneficient
  • Test cloning and adding processed classes is done properly in every validation path node - array keys and items, list items, any of nodes, all of nodes?, inner objects

protected/private mapped properties

  • find different way to write into objects
  • protected properties can be defined on any object (but can't be overriden)
  • private properties are supported only on final classes (to prevent ambiguity with private parent property)

Handling stdClass and other datasource objects

Description

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:

  • neon may use an entity which extends stdClass and adds an extra properties.
  • xml may be returned as e.g. a SimpleXMLElement and we have no way of casting it to an array, handling attributes or printing errors while maintaining the original xml structure

Addition information

No response

Related issues

No response

Single field object

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.

  • instead of items key accepts only values of items (expects the value of field to be given directly, not in array of ['field' => 'value']
  • allows single mapped property
  • callbacks should still work the same
  • types should be modified for the different structure
    • but still allow field-independent errors?
  • FieldName modifier is forbidden
#[SingleFieldObject]
final class SingleFieldInput implements MappedObject
{

	#[ListOf(item: new MappedObjectValue(ItemInput::class))]
	public array $items;

}

Ensure inheritance and dependencies invalidation is properly supported

  • annotations/attributes should define source class, if it is a parent and not the current class (prerequirement of #35)
  • each mapped object metadata should define list of dependencies to check for metadata invalidation (any classes used in rules, used traits, implemented interfaces and extended classes) - continues in #47
  • referenced constants from other classes should ideally result in checking their class, but that may be not possible) - same as previous, #47
  • cache in variable for performance in development mode (in production metadata are cached permanently) (already works partially, continues in #48)
  • each (mapped object) class and trait in hierarchy should allow mapped properties
  • each (mapped object) class and trait in hierarchy should allow callbacks - continues in #52
  • each (mapped object) class in hierarchy should allow docs annotations (traits probably not, some doc annotations are unique, some are not) - same as previous, #52

CreateWithoutConstructor must be defined above all classes

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:

  • Each class with __construct() must use CreateWithoutConstructor
  • Each trait with __construct() must use CreateWithoutConstructor
  • Each interface with __construct() must use CreateWithoutConstructor

protected/private callbacks

  • find different way to call them on objects
  • protected methods can be defined on any object (and can be overriden)
  • private methods are supported only on final classes (calling any private methods properly is more complex)

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.