Skip to content

[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

Open
wants to merge 1 commit into
base: 7.4
Choose a base branch
from

Conversation

mtarld
Copy link
Contributor

@mtarld mtarld commented Jul 1, 2025

Q A
Branch? 7.4
Bug fix? no
New feature? yes
Deprecations? no
Issues
License MIT

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.

@soyuka
Copy link
Contributor

soyuka commented Jul 1, 2025

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 $value.
With @mtarld we thought of multiple approaches to this problem but many would've impacted performances more then we like for this component.

Copy link
Member

@nicolas-grekas nicolas-grekas left a 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);
Copy link
Member

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?

Copy link
Contributor Author

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?

@mtarld mtarld force-pushed the feat/json-streamer-current-object branch from 6a6b9ad to e9dcd42 Compare July 7, 2025 09:47
@mtarld
Copy link
Contributor Author

mtarld commented Jul 7, 2025

Indeed, I changed $options + ['_current_object' => $data] to ['_current_object' => $data] + $options, as it'll override any user-defined value (and therefore prevent its usage to do "hacky" stuff).

Good idea for the doc block, I added the comment.

@OskarStark
Copy link
Contributor

PR title reads weird 😅

@mtarld mtarld changed the title [JsonStreamer] Allow to access to current object when writing [JsonStreamer] Provide current object to value transformers Jul 16, 2025
@mtarld
Copy link
Contributor Author

mtarld commented Jul 16, 2025

Indeed 😅. I updated it, does it look better to you?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants