-
-
Notifications
You must be signed in to change notification settings - Fork 76
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WIP: feat: introduce normalizer #423
Conversation
As someone who worked quite a bit with PHP's native |
} | ||
|
||
if ($object instanceof DateTimeInterface) { | ||
return fn () => $object->format('Y-m-d\\TH:i:sP'); // RFC 3339 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should microseconds be preserved here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's go for it, if that's a problem for some people, they will tell us and we will manage then.
return $value; | ||
} | ||
|
||
if (is_iterable($value)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prioritizing this over is_object()
makes it impossible to add custom handling for classes implementing IteratorAggregate
. I'd also argue that the default behavior is a little unexpected in general, because only the children will be returned by default and properties by the IteratorAggregate
will be ignored.
Example:
<?php
require('vendor/autoload.php');
final class SomeObject implements IteratorAggregate
{
public function __construct(
public readonly string $key,
public readonly string $value,
) {
}
public function getIterator(): Traversable
{
yield from [];
}
}
var_dump(
(new \CuyZ\Valinor\NormalizerBuilder())
->addHandler(fn (SomeObject $object) => [$object->key, $object->value])
->normalizer()
->normalize(new SomeObject('key', 'value'))
);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch! What do you think about the new behaviour?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't really have a strong preference, as I'm not the target audience of this feature. (Who would have expected after my Mastodon comments and comments in the GH discussion 😁). The new behavior appears to be reasonable, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haha yeah, thank you for your participation though!
Right. And after all, this is very easy to simulate the same behaviour with something like: (new \CuyZ\Valinor\NormalizerBuilder())
->addHandler(function (object $object, callable $next) {
return method_exists($object, 'normalize')
? $object->normalize()
: $next();
}); // … Thanks for the suggestion. |
Another gotcha that is likely not fixable, but should perhaps be stated as a limitation: Circular data structures cannot be serialized. <?php
require('vendor/autoload.php');
final class Node
{
public function __construct(
/** @param list<Node> $neighbors */
public array $neighbors = []
) {
}
}
$n = new Node();
$n2 = new Node();
$n->neighbors[] = $n2;
$n2->neighbors[] = $n;
var_dump(
(new \CuyZ\Valinor\NormalizerBuilder())
->normalizer()
->normalize($n)
); Note that PHP's internal |
Indeed, currently it will go in an infinite loop. There are two solutions I can think of right now:
I'm not sure I could find a way to provide a solution that would work in every case. WDYT? |
I would consider telling folks “don't do that” to be reasonable. Detecting cycles would likely require additional memory usage for something that shouldn't happen. Limiting the maximum depth would probably be fine if an error is emitted that the maximum depth is exceeded instead of returning partly serialized data. |
Overall, this looks good. Although this is a bit out of scope, you've mentioned this in your post so I'll reply here:
Is there a way I, as an end-user of Valinor, can access reflection/attributes information in Again, to be clear, I'm not proposing/pushing attributes into Valinor itself; I'll be more than happy to simply implement them on my side. However, I don't see a mechanism that would allow me to do so; especially allow me to do so efficiently, i.e. without calling reflection / instantiating attributes on every serialization/deserialization. Would this also be something that Valinor can introduce or has the decision been made? |
2d00ffa
to
ee212db
Compare
private function doNormalize(mixed $value, array $references): mixed | ||
{ | ||
if (is_object($value)) { | ||
$id = spl_object_id($value); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You might consider using a WeakMap<object, true>
instead. I feel this would be a little more explicit, especially since spl_object_id()
might be reused in case the original object dies (for whatever reason) during normalization.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, thanks!
@oprypkhantc I've just pushed a new commit that makes it possible to use attributes. Documentation on it coming soon! |
a0f89e5
to
abba5eb
Compare
Co-authored-by: Tim Düsterhus <[email protected]>
eec221c
to
e8f1f37
Compare
Better architecture for upcoming JSON normalizer
3d2a1e1
to
7963b2e
Compare
Hi @TimWolla, @oprypkhantc, thank you for your participation on this work! I'm going to prepare a release for it. 🙂 |
This introduces a new feature — a normalizer, which aims to do the opposite of the mapper: recursively transform a structure of objects/others to a nested array of scalar values, that can then easily be encoded to JSON or other basic data-format.
For further reference, please read discussion #420.
The main idea behind this normalizer is a bit different than what can be found in other normalization/serialization libraries, in the way that this library — Valinor — will not leak any customization attribute/interface/trait in the objects being serialized. This is one of the most important goals, as it allows the developers to remain the owners of all business logic rules.
Instead of attribute/interface/trait, the
NormalizerBuilder
will be customized with “handlers”: these are callables that can be chained to customize the normalization result for a given object. A handler must specify at least one argument in its callback: the type of the object it will be handling — note that this type can be the native typeobject
so that the handler will be applied on every object. A second argument can be specified, acallable
that allows the handler to call the next handler in the queue, to get its result and apply custom modifications on it.To make this more understandable, below are adaptations of some customization features that can be found in other libraries (often driven by attributes):
Note
The default normalized representation of an object will be done by accessing all its properties — no matter the visibility (private/protected/public).
For better understanding, take a look at the major integration test.
Basic object custom normalization️
Sometimes, the normalized representation of an object can require some tiny adjustments; the object below needs to append/prepend underscores to its keys for some business reason.
The object just needs to implement the native— behavior changed, see #423 (comment)__serialize()
method and add the needed logic in it.A global handler is registered to detect if an object defines a
normalize()
method, in which case the result will be used by the normalizer.Transforming all keys to snake_case️
This is a classic one: JSON-encoded API often require keys to be in the snake_case format whereas PHP's classes' properties are often written using the camelCase format. In the example below, the transformation is done globally and recursively on every object during normalization. This could of course be adapted to match one's needs if necessary.
Ignoring properties during normalization️
To ignore properties, the
__serialize()
method could easily be used. But an alternative here is to provide an interfaceIgnoresValuesOnNormalization
that can be implemented by any object. This interface is then detected during the normalization to do the job of actually un-setting the values that should be ignored.Adding prefix to key️
Same kind of example as above: an interface is used by objects to specify when a prefix should be added to the all keys during normalization. If each key should have a different prefix/suffix, the use of
__serialize()
method would probably be a better choice.API versioning️
This is a more complex example, but it shows how the normalizer can be used to handle API versioning. The idea here is that an API can evolve over time, and sometimes the normalized representation of an object in the API response can change.
Dates format️
This is a very common use-case: dates are often represented as strings in data-format like JSON. By default, the normalizer will format any date using the RFC 3339. This can be customized by adding a handler that will be applied on every
DateTimeInterface
object.Note that other implementations could be imagined to offer a more precise control over the format.
I really want to emphasize the fact that, in the examples above, the library does not leak in the objects normalization rules: the logic is not coupled to the normalizer and does (should) stay in the domain-layer of the application.
In some cases, it can seem cumbersome to implement the logic, but I strongly believe that in most cases this is trivial work that can be done quickly. More importantly, it can be adapted/improved directly by the developers of the application, following the constantly-changing needs of the business rules, without being tied up to a third-party library release-cycle.
I'd be glad to have your feedback on this feature, and I'm open to any suggestion.
Please don't hesitate to checkout the branch and try it out.