-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[JsonStreamer] Provide current object to value transformers #61006
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
base: 7.4
Are you sure you want to change the base?
Conversation
Indeed this is really a nice-to-have functionality. When generating Hypermedia (or probably any JSON) while streaming, it's quite useful to have the current object being serialized to change parts of the data. To illustrate, we use it to generate the IRI of a given object during serialization: <?php
declare(strict_types=1);
namespace ApiPlatform\JsonLd\JsonStreamer\ValueTransformer;
use ApiPlatform\Hydra\Collection;
use ApiPlatform\Metadata\CollectionOperationInterface;
use ApiPlatform\Metadata\IriConverterInterface;
use ApiPlatform\Metadata\UrlGeneratorInterface;
use Symfony\Component\JsonStreamer\ValueTransformer\ValueTransformerInterface;
use Symfony\Component\TypeInfo\Type;
final class IriValueTransformer implements ValueTransformerInterface
{
public function __construct(
private readonly IriConverterInterface $iriConverter,
) {
}
public function transform(mixed $value, array $options = []): mixed
{
if ($options['_current_object'] instanceof Collection) {
return $this->iriConverter->getIriFromResource($options['operation']->getClass(), UrlGeneratorInterface::ABS_PATH, $options['operation']);
}
return $this->iriConverter->getIriFromResource(
$options['_current_object'],
UrlGeneratorInterface::ABS_PATH,
$options['operation'] instanceof CollectionOperationInterface ? null : $options['operation'],
);
}
public static function getStreamValueType(): Type
{
return Type::string();
}
} Without this functionality we're quite locked-out when it comes to transformations like this one: it's recursive so we're using only the node being serialized, as opposed to |
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 it be $option + [...] or [...] + $options?
should we document the key in a phpdoc on ValueTransformerInterface?
@@ -6,13 +6,13 @@ | |||
return static function (mixed $data, \Psr\Container\ContainerInterface $valueTransformers, array $options): \Traversable { | |||
try { | |||
yield '{"id":'; | |||
yield \json_encode($valueTransformers->get('Symfony\Component\JsonStreamer\Tests\Fixtures\ValueTransformer\DoubleIntAndCastToStringValueTransformer')->transform($data->id, $options), \JSON_THROW_ON_ERROR, 511); | |||
yield \json_encode($valueTransformers->get('Symfony\Component\JsonStreamer\Tests\Fixtures\ValueTransformer\DoubleIntAndCastToStringValueTransformer')->transform($data->id, $options + ['_current_object' => $data]), \JSON_THROW_ON_ERROR, 511); |
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.
can the addition be done earlier so that it's done once?
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'm not sure that it's the best way to go here.
Indeed, storing the current object earlier will do variable assignments for nothing if the is no value transformer. For example, for this object with nested objects, it'll do 4 variable assignment for "nothing" (and it'll be even worse if it is a collection of that object):
/**
* @param Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithOtherDummies $data
*/
return static function (mixed $data, \Psr\Container\ContainerInterface $valueTransformers, array $options): \Traversable {
try {
$options['_current_object'] = $data;
yield '{"name":';
yield \json_encode($data->name, \JSON_THROW_ON_ERROR, 511);
$options['_current_object'] = $data->otherDummyOne;
yield ',"otherDummyOne":{"@id":';
yield \json_encode($data->otherDummyOne->id, \JSON_THROW_ON_ERROR, 510);
yield ',"name":';
yield \json_encode($data->otherDummyOne->name, \JSON_THROW_ON_ERROR, 510);
$options['_current_object'] = $data->otherDummyTwo;
yield '},"otherDummyTwo":{"id":';
yield \json_encode($data->otherDummyTwo->id, \JSON_THROW_ON_ERROR, 510);
yield ',"name":';
yield \json_encode($data->otherDummyTwo->name, \JSON_THROW_ON_ERROR, 510);
$options['_current_object'] = $data;
yield '},"friendly":';
yield $data->friendly ? 'true' : 'false';
yield '}';
} catch (\JsonException $e) {
throw new \Symfony\Component\JsonStreamer\Exception\NotEncodableValueException($e->getMessage(), 0, $e);
}
};
Plus, it kind of introduce "state" that is a bit risky to me. With the variable assignment, we have more chance to mess up and forget to set/reset the current object, while the array addition is 100% sure.
WDYT?
6a6b9ad
to
e9dcd42
Compare
Indeed, I changed Good idea for the doc block, I added the comment. |
PR title reads weird 😅 |
Indeed 😅. I updated it, does it look better to you? |
A new attempt to #59281.
As discussed with @soyuka, it is really important to have access to the current object during stream writing.
Without that, it'll for example be impossible for API Platform to generate resource IRIs (https://github.com/api-platform/core/blob/main/src/Metadata/IriConverterInterface.php#L46).
This PR makes is available in value transformers'
$context
under the_current_object
key.