diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
index 5296bda25513e..9aec80d8a96c4 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
@@ -237,6 +237,7 @@ class FrameworkExtension extends Extension
private bool $mailerConfigEnabled = false;
private bool $httpClientConfigEnabled = false;
private bool $notifierConfigEnabled = false;
+ private bool $serializerConfigEnabled = false;
private bool $propertyAccessConfigEnabled = false;
private static bool $lockConfigEnabled = false;
@@ -386,7 +387,7 @@ public function load(array $configs, ContainerBuilder $container)
$container->getDefinition('exception_listener')->replaceArgument(3, $config['exceptions']);
- if ($this->isConfigEnabled($container, $config['serializer'])) {
+ if ($this->serializerConfigEnabled = $this->isConfigEnabled($container, $config['serializer'])) {
if (!class_exists(\Symfony\Component\Serializer\Serializer::class)) {
throw new LogicException('Serializer support cannot be enabled as the Serializer component is not installed. Try running "composer require symfony/serializer-pack".');
}
@@ -516,7 +517,7 @@ public function load(array $configs, ContainerBuilder $container)
$this->registerNotifierConfiguration($config['notifier'], $container, $loader);
}
- // profiler depends on form, validation, translation, messenger, mailer, http-client, notifier being registered
+ // profiler depends on form, validation, translation, messenger, mailer, http-client, notifier, serializer being registered
$this->registerProfilerConfiguration($config['profiler'], $container, $loader);
$this->addAnnotatedClassesToCompile([
@@ -798,6 +799,10 @@ private function registerProfilerConfiguration(array $config, ContainerBuilder $
$loader->load('notifier_debug.php');
}
+ if ($this->serializerConfigEnabled) {
+ $loader->load('serializer_debug.php');
+ }
+
$container->setParameter('profiler_listener.only_exceptions', $config['only_exceptions']);
$container->setParameter('profiler_listener.only_main_requests', $config['only_main_requests']);
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer_debug.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer_debug.php
new file mode 100644
index 0000000000000..28fc4ea48c514
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer_debug.php
@@ -0,0 +1,32 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\DependencyInjection\Loader\Configurator;
+
+use Symfony\Component\Serializer\DataCollector\SerializerDataCollector;
+use Symfony\Component\Serializer\Debug\TraceableSerializer;
+
+return static function (ContainerConfigurator $container) {
+ $container->services()
+ ->set('debug.serializer', TraceableSerializer::class)
+ ->decorate('serializer', null, 255)
+ ->args([
+ service('debug.serializer.inner'),
+ service('serializer.data_collector'),
+ ])
+
+ ->set('serializer.data_collector', SerializerDataCollector::class)
+ ->tag('data_collector', [
+ 'template' => '@WebProfiler/Collector/serializer.html.twig',
+ 'id' => 'serializer',
+ ])
+ ;
+};
diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json
index 512512b4612a3..f915572ea730e 100644
--- a/src/Symfony/Bundle/FrameworkBundle/composer.json
+++ b/src/Symfony/Bundle/FrameworkBundle/composer.json
@@ -85,7 +85,7 @@
"symfony/mime": "<5.4",
"symfony/property-info": "<5.4",
"symfony/property-access": "<5.4",
- "symfony/serializer": "<5.4",
+ "symfony/serializer": "<6.1",
"symfony/security-csrf": "<5.4",
"symfony/security-core": "<5.4",
"symfony/stopwatch": "<5.4",
diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/serializer.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/serializer.html.twig
new file mode 100644
index 0000000000000..c9197742f065a
--- /dev/null
+++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/serializer.html.twig
@@ -0,0 +1,228 @@
+{% extends '@WebProfiler/Profiler/layout.html.twig' %}
+
+{% import _self as helper %}
+
+{% block menu %}
+
+ {{ include('@WebProfiler/Icon/validator.svg') }}
+ Serializer
+
+{% endblock %}
+
+{% block panel %}
+
Serializer
+ {% if not collector.handledCount %}
+
+
Nothing was handled by the serializer for this request.
+
+ {% else %}
+
+
+ {{ collector.handledCount }}
+ Handled
+
+
+
+ {{ '%.2f'|format(collector.totalTime * 1000) }} ms
+ Total time
+
+
+
+
+ {{ helper.render_serialize_tab(collector.data, true) }}
+ {{ helper.render_serialize_tab(collector.data, false) }}
+
+ {{ helper.render_normalize_tab(collector.data, true) }}
+ {{ helper.render_normalize_tab(collector.data, false) }}
+
+ {{ helper.render_encode_tab(collector.data, true) }}
+ {{ helper.render_encode_tab(collector.data, false) }}
+
+ {% endif %}
+{% endblock %}
+
+{% macro render_serialize_tab(collectorData, serialize) %}
+ {% set data = serialize ? collectorData.serialize : collectorData.deserialize %}
+ {% set cellPrefix = serialize ? 'serialize' : 'deserialize' %}
+
+
+
{{ serialize ? 'serialize' : 'deserialize' }} {{ data|length }}
+
+ {% if not data|length %}
+
+
Nothing was {{ serialize ? 'serialized' : 'deserialized' }}.
+
+ {% else %}
+
+
+
+ Data |
+ Context |
+ Normalizer |
+ Encoder |
+ Time |
+
+
+
+ {% for item in data %}
+
+ {{ helper.render_data_cell(item, loop.index, cellPrefix) }} |
+ {{ helper.render_context_cell(item, loop.index, cellPrefix) }} |
+ {{ helper.render_normalizer_cell(item, loop.index, cellPrefix) }} |
+ {{ helper.render_encoder_cell(item, loop.index, cellPrefix) }} |
+ {{ helper.render_time_cell(item) }} |
+
+ {% endfor %}
+
+
+ {% endif %}
+
+
+{% endmacro %}
+
+{% macro render_normalize_tab(collectorData, normalize) %}
+ {% set data = normalize ? collectorData.normalize : collectorData.denormalize %}
+ {% set cellPrefix = normalize ? 'normalize' : 'denormalize' %}
+
+
+
{{ normalize ? 'normalize' : 'denormalize' }} {{ data|length }}
+
+ {% if not data|length %}
+
+
Nothing was {{ normalize ? 'normalized' : 'denormalized' }}.
+
+ {% else %}
+
+
+
+ Data |
+ Context |
+ Normalizer |
+ Time |
+
+
+
+ {% for item in data %}
+
+ {{ helper.render_data_cell(item, loop.index, cellPrefix) }} |
+ {{ helper.render_context_cell(item, loop.index, cellPrefix) }} |
+ {{ helper.render_normalizer_cell(item, loop.index, cellPrefix) }} |
+ {{ helper.render_time_cell(item) }} |
+
+ {% endfor %}
+
+
+ {% endif %}
+
+
+{% endmacro %}
+
+{% macro render_encode_tab(collectorData, encode) %}
+ {% set data = encode ? collectorData.encode : collectorData.decode %}
+ {% set cellPrefix = encode ? 'encode' : 'decode' %}
+
+
+
{{ encode ? 'encode' : 'decode' }} {{ data|length }}
+
+ {% if not data|length %}
+
+
Nothing was {{ encode ? 'encoded' : 'decoded' }}.
+
+ {% else %}
+
+
+
+ Data |
+ Context |
+ Encoder |
+ Time |
+
+
+
+ {% for item in data %}
+
+ {{ helper.render_data_cell(item, loop.index, cellPrefix) }} |
+ {{ helper.render_context_cell(item, loop.index, cellPrefix) }} |
+ {{ helper.render_encoder_cell(item, loop.index, cellPrefix) }} |
+ {{ helper.render_time_cell(item) }} |
+
+ {% endfor %}
+
+
+ {% endif %}
+
+
+{% endmacro %}
+
+{% macro render_data_cell(item, index, method) %}
+ {% set data_id = 'data-' ~ method ~ '-' ~ index %}
+
+ {{ item.dataType }}
+
+
+{% endmacro %}
+
+{% macro render_context_cell(item, index, method) %}
+ {% set context_id = 'context-' ~ method ~ '-' ~ index %}
+
+ {% if item.type %}
+ Type: {{ item.type }}
+ Format: {{ item.format ? item.format : 'none' }}
+ {% else %}
+ Format: {{ item.format ? item.format : 'none' }}
+ {% endif %}
+
+
+{% endmacro %}
+
+{% macro render_normalizer_cell(item, index, method) %}
+ {% set nested_normalizers_id = 'nested-normalizers-' ~ method ~ '-' ~ index %}
+
+ {{ item.normalizer.class }} ({{ '%.2f'|format(item.normalizer.time * 1000) }} ms)
+
+ {% if item.normalization|length > 1 %}
+
+ {% endif %}
+{% endmacro %}
+
+{% macro render_encoder_cell(item, index, method) %}
+ {% set nested_encoders_id = 'nested-encoders-' ~ method ~ '-' ~ index %}
+
+ {{ item.encoder.class }} ({{ '%.2f'|format(item.encoder.time * 1000) }} ms)
+
+ {% if item.encoding|length > 1 %}
+
+ {% endif %}
+{% endmacro %}
+
+{% macro render_time_cell(item) %}
+ {{ '%.2f'|format(item.time * 1000) }} ms
+{% endmacro %}
diff --git a/src/Symfony/Component/Serializer/CHANGELOG.md b/src/Symfony/Component/Serializer/CHANGELOG.md
index 28b194edb8fca..9bee69dbd4fbb 100644
--- a/src/Symfony/Component/Serializer/CHANGELOG.md
+++ b/src/Symfony/Component/Serializer/CHANGELOG.md
@@ -4,6 +4,7 @@ CHANGELOG
6.1
---
+ * Add `TraceableSerializer`, `TraceableNormalizer`, `TraceableEncoder` and `SerializerDataCollector` to integrate with the web profiler
* Add the ability to create contexts using context builders
* Set `Context` annotation as not final
* Deprecate `ContextAwareNormalizerInterface`, use `NormalizerInterface` instead
diff --git a/src/Symfony/Component/Serializer/DataCollector/SerializerDataCollector.php b/src/Symfony/Component/Serializer/DataCollector/SerializerDataCollector.php
new file mode 100644
index 0000000000000..57a8aecab02be
--- /dev/null
+++ b/src/Symfony/Component/Serializer/DataCollector/SerializerDataCollector.php
@@ -0,0 +1,239 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Serializer\DataCollector;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\DataCollector\DataCollector;
+use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
+use Symfony\Component\Serializer\Debug\TraceableSerializer;
+use Symfony\Component\VarDumper\Cloner\Data;
+
+/**
+ * @author Mathias Arlaud
+ *
+ * @final
+ *
+ * @internal
+ */
+class SerializerDataCollector extends DataCollector implements LateDataCollectorInterface
+{
+ private array $collected = [];
+
+ public function reset(): void
+ {
+ $this->data = [];
+ $this->collected = [];
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function collect(Request $request, Response $response, \Throwable $exception = null): void
+ {
+ // Everything is collected during the request, and formatted on kernel terminate.
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getName(): string
+ {
+ return 'serializer';
+ }
+
+ public function getData(): Data|array
+ {
+ return $this->data;
+ }
+
+ public function getHandledCount(): int
+ {
+ return array_sum(array_map('count', $this->data));
+ }
+
+ public function getTotalTime(): float
+ {
+ $totalTime = 0;
+
+ foreach ($this->data as $handled) {
+ $totalTime += array_sum(array_map(fn (array $el): float => $el['time'], $handled));
+ }
+
+ return $totalTime;
+ }
+
+ public function collectSerialize(string $traceId, mixed $data, string $format, array $context, float $time): void
+ {
+ unset($context[TraceableSerializer::DEBUG_TRACE_ID]);
+
+ $this->collected[$traceId] = array_merge(
+ $this->collected[$traceId] ?? [],
+ compact('data', 'format', 'context', 'time'),
+ ['method' => 'serialize'],
+ );
+ }
+
+ public function collectDeserialize(string $traceId, mixed $data, string $type, string $format, array $context, float $time): void
+ {
+ unset($context[TraceableSerializer::DEBUG_TRACE_ID]);
+
+ $this->collected[$traceId] = array_merge(
+ $this->collected[$traceId] ?? [],
+ compact('data', 'format', 'type', 'context', 'time'),
+ ['method' => 'deserialize'],
+ );
+ }
+
+ public function collectNormalize(string $traceId, mixed $data, ?string $format, array $context, float $time): void
+ {
+ unset($context[TraceableSerializer::DEBUG_TRACE_ID]);
+
+ $this->collected[$traceId] = array_merge(
+ $this->collected[$traceId] ?? [],
+ compact('data', 'format', 'context', 'time'),
+ ['method' => 'normalize'],
+ );
+ }
+
+ public function collectDenormalize(string $traceId, mixed $data, string $type, ?string $format, array $context, float $time): void
+ {
+ unset($context[TraceableSerializer::DEBUG_TRACE_ID]);
+
+ $this->collected[$traceId] = array_merge(
+ $this->collected[$traceId] ?? [],
+ compact('data', 'format', 'type', 'context', 'time'),
+ ['method' => 'denormalize'],
+ );
+ }
+
+ public function collectEncode(string $traceId, mixed $data, ?string $format, array $context, float $time): void
+ {
+ unset($context[TraceableSerializer::DEBUG_TRACE_ID]);
+
+ $this->collected[$traceId] = array_merge(
+ $this->collected[$traceId] ?? [],
+ compact('data', 'format', 'context', 'time'),
+ ['method' => 'encode'],
+ );
+ }
+
+ public function collectDecode(string $traceId, mixed $data, ?string $format, array $context, float $time): void
+ {
+ unset($context[TraceableSerializer::DEBUG_TRACE_ID]);
+
+ $this->collected[$traceId] = array_merge(
+ $this->collected[$traceId] ?? [],
+ compact('data', 'format', 'context', 'time'),
+ ['method' => 'decode'],
+ );
+ }
+
+ public function collectNormalization(string $traceId, string $normalizer, float $time): void
+ {
+ $method = 'normalize';
+
+ $this->collected[$traceId]['normalization'][] = compact('normalizer', 'method', 'time');
+ }
+
+ public function collectDenormalization(string $traceId, string $normalizer, float $time): void
+ {
+ $method = 'denormalize';
+
+ $this->collected[$traceId]['normalization'][] = compact('normalizer', 'method', 'time');
+ }
+
+ public function collectEncoding(string $traceId, string $encoder, float $time): void
+ {
+ $method = 'encode';
+
+ $this->collected[$traceId]['encoding'][] = compact('encoder', 'method', 'time');
+ }
+
+ public function collectDecoding(string $traceId, string $encoder, float $time): void
+ {
+ $method = 'decode';
+
+ $this->collected[$traceId]['encoding'][] = compact('encoder', 'method', 'time');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function lateCollect(): void
+ {
+ $this->data = [
+ 'serialize' => [],
+ 'deserialize' => [],
+ 'normalize' => [],
+ 'denormalize' => [],
+ 'encode' => [],
+ 'decode' => [],
+ ];
+
+ foreach ($this->collected as $collected) {
+ $data = [
+ 'data' => $this->cloneVar($collected['data']),
+ 'dataType' => get_debug_type($collected['data']),
+ 'type' => $collected['type'] ?? null,
+ 'format' => $collected['format'],
+ 'time' => $collected['time'],
+ 'context' => $this->cloneVar($collected['context']),
+ 'normalization' => [],
+ 'encoding' => [],
+ ];
+
+ if (isset($collected['normalization'])) {
+ $mainNormalization = array_pop($collected['normalization']);
+
+ $data['normalizer'] = ['time' => $mainNormalization['time']] + $this->getMethodLocation($mainNormalization['normalizer'], $mainNormalization['method']);
+
+ foreach ($collected['normalization'] as $normalization) {
+ if (!isset($data['normalization'][$normalization['normalizer']])) {
+ $data['normalization'][$normalization['normalizer']] = ['time' => 0, 'calls' => 0] + $this->getMethodLocation($normalization['normalizer'], $normalization['method']);
+ }
+
+ ++$data['normalization'][$normalization['normalizer']]['calls'];
+ $data['normalization'][$normalization['normalizer']]['time'] += $normalization['time'];
+ }
+ }
+
+ if (isset($collected['encoding'])) {
+ $mainEncoding = array_pop($collected['encoding']);
+
+ $data['encoder'] = ['time' => $mainEncoding['time']] + $this->getMethodLocation($mainEncoding['encoder'], $mainEncoding['method']);
+
+ foreach ($collected['encoding'] as $encoding) {
+ if (!isset($data['encoding'][$encoding['encoder']])) {
+ $data['encoding'][$encoding['encoder']] = ['time' => 0, 'calls' => 0] + $this->getMethodLocation($encoding['encoder'], $encoding['method']);
+ }
+
+ ++$data['encoding'][$encoding['encoder']]['calls'];
+ $data['encoding'][$encoding['encoder']]['time'] += $encoding['time'];
+ }
+ }
+
+ $this->data[$collected['method']][] = $data;
+ }
+ }
+
+ private function getMethodLocation(string $class, string $method): array
+ {
+ $reflection = new \ReflectionClass($class);
+
+ return [
+ 'class' => $reflection->getShortName(),
+ 'file' => $reflection->getFileName(),
+ 'line' => $reflection->getMethod($method)->getStartLine(),
+ ];
+ }
+}
diff --git a/src/Symfony/Component/Serializer/Debug/TraceableEncoder.php b/src/Symfony/Component/Serializer/Debug/TraceableEncoder.php
new file mode 100644
index 0000000000000..cd4a351c6804a
--- /dev/null
+++ b/src/Symfony/Component/Serializer/Debug/TraceableEncoder.php
@@ -0,0 +1,126 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Serializer\Debug;
+
+use Symfony\Component\Serializer\DataCollector\SerializerDataCollector;
+use Symfony\Component\Serializer\Encoder\DecoderInterface;
+use Symfony\Component\Serializer\Encoder\EncoderInterface;
+use Symfony\Component\Serializer\SerializerAwareInterface;
+use Symfony\Component\Serializer\SerializerInterface;
+use Symfony\Component\Serializer\Tests\Encoder\NormalizationAwareEncoder;
+
+/**
+ * Collects some data about encoding.
+ *
+ * @author Mathias Arlaud
+ *
+ * @final
+ *
+ * @internal
+ */
+class TraceableEncoder implements EncoderInterface, DecoderInterface, SerializerAwareInterface
+{
+ public function __construct(
+ private EncoderInterface|DecoderInterface $encoder,
+ private SerializerDataCollector $dataCollector,
+ ) {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function encode(mixed $data, string $format, array $context = []): string
+ {
+ if (!$this->encoder instanceof EncoderInterface) {
+ throw new \BadMethodCallException(sprintf('The "%s()" method cannot be called as nested encoder doesn\'t implements "%s".', __METHOD__, EncoderInterface::class));
+ }
+
+ $startTime = microtime(true);
+ $encoded = $this->encoder->encode($data, $format, $context);
+ $time = microtime(true) - $startTime;
+
+ if ($traceId = ($context[TraceableSerializer::DEBUG_TRACE_ID] ?? null)) {
+ $this->dataCollector->collectEncoding($traceId, \get_class($this->encoder), $time);
+ }
+
+ return $encoded;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @param array $context
+ */
+ public function supportsEncoding(string $format /*, array $context = [] */): bool
+ {
+ if (!$this->encoder instanceof EncoderInterface) {
+ return false;
+ }
+
+ $context = \func_num_args() > 1 ? func_get_arg(1) : [];
+
+ return $this->encoder->supportsEncoding($format, $context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function decode(string $data, string $format, array $context = []): mixed
+ {
+ if (!$this->encoder instanceof DecoderInterface) {
+ throw new \BadMethodCallException(sprintf('The "%s()" method cannot be called as nested encoder doesn\'t implements "%s".', __METHOD__, DecoderInterface::class));
+ }
+
+ $startTime = microtime(true);
+ $encoded = $this->encoder->decode($data, $format, $context);
+ $time = microtime(true) - $startTime;
+
+ if ($traceId = ($context[TraceableSerializer::DEBUG_TRACE_ID] ?? null)) {
+ $this->dataCollector->collectDecoding($traceId, \get_class($this->encoder), $time);
+ }
+
+ return $encoded;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @param array $context
+ */
+ public function supportsDecoding(string $format /*, array $context = [] */): bool
+ {
+ if (!$this->encoder instanceof DecoderInterface) {
+ return false;
+ }
+
+ $context = \func_num_args() > 1 ? func_get_arg(1) : [];
+
+ return $this->encoder->supportsDecoding($format, $context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function setSerializer(SerializerInterface $serializer)
+ {
+ if (!$this->encoder instanceof SerializerAwareInterface) {
+ return;
+ }
+
+ $this->encoder->setSerializer($serializer);
+ }
+
+ public function needsNormalization(): bool
+ {
+ return !$this->encoder instanceof NormalizationAwareEncoder;
+ }
+}
diff --git a/src/Symfony/Component/Serializer/Debug/TraceableNormalizer.php b/src/Symfony/Component/Serializer/Debug/TraceableNormalizer.php
new file mode 100644
index 0000000000000..e32475e3cd859
--- /dev/null
+++ b/src/Symfony/Component/Serializer/Debug/TraceableNormalizer.php
@@ -0,0 +1,155 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Serializer\Debug;
+
+use Symfony\Component\Serializer\DataCollector\SerializerDataCollector;
+use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
+use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
+use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
+use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
+use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
+use Symfony\Component\Serializer\SerializerAwareInterface;
+use Symfony\Component\Serializer\SerializerInterface;
+
+/**
+ * Collects some data about normalization.
+ *
+ * @author Mathias Arlaud
+ *
+ * @final
+ *
+ * @internal
+ */
+class TraceableNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface, NormalizerAwareInterface, DenormalizerAwareInterface, CacheableSupportsMethodInterface
+{
+ public function __construct(
+ private NormalizerInterface|DenormalizerInterface $normalizer,
+ private SerializerDataCollector $dataCollector,
+ ) {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null
+ {
+ if (!$this->normalizer instanceof NormalizerInterface) {
+ throw new \BadMethodCallException(sprintf('The "%s()" method cannot be called as nested normalizer doesn\'t implements "%s".', __METHOD__, NormalizerInterface::class));
+ }
+
+ $startTime = microtime(true);
+ $normalized = $this->normalizer->normalize($object, $format, $context);
+ $time = microtime(true) - $startTime;
+
+ if ($traceId = ($context[TraceableSerializer::DEBUG_TRACE_ID] ?? null)) {
+ $this->dataCollector->collectNormalization($traceId, \get_class($this->normalizer), $time);
+ }
+
+ return $normalized;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @param array $context
+ */
+ public function supportsNormalization(mixed $data, string $format = null /*, array $context = [] */): bool
+ {
+ if (!$this->normalizer instanceof NormalizerInterface) {
+ return false;
+ }
+
+ $context = \func_num_args() > 2 ? func_get_arg(2) : [];
+
+ return $this->normalizer->supportsNormalization($data, $format, $context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function denormalize(mixed $data, string $type, string $format = null, array $context = []): mixed
+ {
+ if (!$this->normalizer instanceof DenormalizerInterface) {
+ throw new \BadMethodCallException(sprintf('The "%s()" method cannot be called as nested normalizer doesn\'t implements "%s".', __METHOD__, DenormalizerInterface::class));
+ }
+
+ $startTime = microtime(true);
+ $denormalized = $this->normalizer->denormalize($data, $type, $format, $context);
+ $time = microtime(true) - $startTime;
+
+ if ($traceId = ($context[TraceableSerializer::DEBUG_TRACE_ID] ?? null)) {
+ $this->dataCollector->collectDenormalization($traceId, \get_class($this->normalizer), $time);
+ }
+
+ return $denormalized;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @param array $context
+ */
+ public function supportsDenormalization(mixed $data, string $type, string $format = null /*, array $context = [] */): bool
+ {
+ if (!$this->normalizer instanceof DenormalizerInterface) {
+ return false;
+ }
+
+ $context = \func_num_args() > 3 ? func_get_arg(3) : [];
+
+ return $this->normalizer->supportsDenormalization($data, $type, $format, $context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function setSerializer(SerializerInterface $serializer)
+ {
+ if (!$this->normalizer instanceof SerializerAwareInterface) {
+ return;
+ }
+
+ $this->normalizer->setSerializer($serializer);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function setNormalizer(NormalizerInterface $normalizer)
+ {
+ if (!$this->normalizer instanceof NormalizerAwareInterface) {
+ return;
+ }
+
+ $this->normalizer->setNormalizer($normalizer);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function setDenormalizer(DenormalizerInterface $denormalizer)
+ {
+ if (!$this->normalizer instanceof DenormalizerAwareInterface) {
+ return;
+ }
+
+ $this->normalizer->setDenormalizer($denormalizer);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function hasCacheableSupportsMethod(): bool
+ {
+ return $this->normalizer instanceof CacheableSupportsMethodInterface && $this->normalizer->hasCacheableSupportsMethod();
+ }
+}
diff --git a/src/Symfony/Component/Serializer/Debug/TraceableSerializer.php b/src/Symfony/Component/Serializer/Debug/TraceableSerializer.php
new file mode 100644
index 0000000000000..d98d0017b69fc
--- /dev/null
+++ b/src/Symfony/Component/Serializer/Debug/TraceableSerializer.php
@@ -0,0 +1,182 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Serializer\Debug;
+
+use Symfony\Component\Serializer\DataCollector\SerializerDataCollector;
+use Symfony\Component\Serializer\Encoder\DecoderInterface;
+use Symfony\Component\Serializer\Encoder\EncoderInterface;
+use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
+use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
+use Symfony\Component\Serializer\SerializerInterface;
+
+/**
+ * Collects some data about serialization.
+ *
+ * @author Mathias Arlaud
+ *
+ * @final
+ * @internal
+ */
+class TraceableSerializer implements SerializerInterface, NormalizerInterface, DenormalizerInterface, EncoderInterface, DecoderInterface
+{
+ public const DEBUG_TRACE_ID = 'debug_trace_id';
+
+ public function __construct(
+ private SerializerInterface&NormalizerInterface&DenormalizerInterface&EncoderInterface&DecoderInterface $serializer,
+ private SerializerDataCollector $dataCollector,
+ ) {
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ final public function serialize(mixed $data, string $format, array $context = []): string
+ {
+ $context[self::DEBUG_TRACE_ID] = $traceId = uniqid();
+
+ $startTime = microtime(true);
+ $result = $this->serializer->serialize($data, $format, $context);
+ $time = microtime(true) - $startTime;
+
+ $this->dataCollector->collectSerialize($traceId, $data, $format, $context, $time);
+
+ return $result;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ final public function deserialize(mixed $data, string $type, string $format, array $context = []): mixed
+ {
+ $context[self::DEBUG_TRACE_ID] = $traceId = uniqid();
+
+ $startTime = microtime(true);
+ $result = $this->serializer->deserialize($data, $type, $format, $context);
+ $time = microtime(true) - $startTime;
+
+ $this->dataCollector->collectDeserialize($traceId, $data, $type, $format, $context, $time);
+
+ return $result;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ final public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null
+ {
+ $context[self::DEBUG_TRACE_ID] = $traceId = uniqid();
+
+ $startTime = microtime(true);
+ $result = $this->serializer->normalize($object, $format, $context);
+ $time = microtime(true) - $startTime;
+
+ $this->dataCollector->collectNormalize($traceId, $object, $format, $context, $time);
+
+ return $result;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ final public function denormalize(mixed $data, string $type, string $format = null, array $context = []): mixed
+ {
+ $context[self::DEBUG_TRACE_ID] = $traceId = uniqid();
+
+ $startTime = microtime(true);
+ $result = $this->serializer->denormalize($data, $type, $format, $context);
+ $time = microtime(true) - $startTime;
+
+ $this->dataCollector->collectDenormalize($traceId, $data, $type, $format, $context, $time);
+
+ return $result;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ final public function encode(mixed $data, string $format, array $context = []): string
+ {
+ $context[self::DEBUG_TRACE_ID] = $traceId = uniqid();
+
+ $startTime = microtime(true);
+ $result = $this->serializer->encode($data, $format, $context);
+ $time = microtime(true) - $startTime;
+
+ $this->dataCollector->collectEncode($traceId, $data, $format, $context, $time);
+
+ return $result;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ final public function decode(string $data, string $format, array $context = []): mixed
+ {
+ $context[self::DEBUG_TRACE_ID] = $traceId = uniqid();
+
+ $startTime = microtime(true);
+ $result = $this->serializer->decode($data, $format, $context);
+ $time = microtime(true) - $startTime;
+
+ $this->dataCollector->collectDecode($traceId, $data, $format, $context, $time);
+
+ return $result;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @param array $context
+ */
+ final public function supportsNormalization(mixed $data, string $format = null /*, array $context = [] */): bool
+ {
+ $context = \func_num_args() > 2 ? \func_get_arg(2) : [];
+
+ return $this->serializer->supportsNormalization($data, $format, $context);
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @param array $context
+ */
+ final public function supportsDenormalization(mixed $data, string $type, string $format = null /*, array $context = [] */): bool
+ {
+ $context = \func_num_args() > 3 ? \func_get_arg(3) : [];
+
+ return $this->serializer->supportsDenormalization($data, $type, $format, $context);
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @param array $context
+ */
+ final public function supportsEncoding(string $format /*, array $context = [] */): bool
+ {
+ $context = \func_num_args() > 1 ? \func_get_arg(1) : [];
+
+ return $this->serializer->supportsEncoding($format, $context);
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @param array $context
+ */
+ final public function supportsDecoding(string $format /*, array $context = [] */): bool
+ {
+ $context = \func_num_args() > 1 ? \func_get_arg(1) : [];
+
+ return $this->serializer->supportsDecoding($format, $context);
+ }
+}
diff --git a/src/Symfony/Component/Serializer/DependencyInjection/SerializerPass.php b/src/Symfony/Component/Serializer/DependencyInjection/SerializerPass.php
index 58ade72fe8e48..00bd0eea030f2 100644
--- a/src/Symfony/Component/Serializer/DependencyInjection/SerializerPass.php
+++ b/src/Symfony/Component/Serializer/DependencyInjection/SerializerPass.php
@@ -16,6 +16,9 @@
use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
+use Symfony\Component\DependencyInjection\Reference;
+use Symfony\Component\Serializer\Debug\TraceableEncoder;
+use Symfony\Component\Serializer\Debug\TraceableNormalizer;
/**
* Adds all services with the tags "serializer.encoder" and "serializer.normalizer" as
@@ -34,18 +37,34 @@ public function process(ContainerBuilder $container)
return;
}
- if (!$normalizers = $this->findAndSortTaggedServices('serializer.normalizer', $container)) {
+ if (!$normalizers = $container->findTaggedServiceIds('serializer.normalizer')) {
throw new RuntimeException('You must tag at least one service as "serializer.normalizer" to use the "serializer" service.');
}
+ if ($container->getParameter('kernel.debug') && $container->hasDefinition('serializer.data_collector')) {
+ foreach (array_keys($normalizers) as $normalizer) {
+ $container->register('debug.'.$normalizer, TraceableNormalizer::class)
+ ->setDecoratedService($normalizer, null, 255)
+ ->setArguments([new Reference('debug.'.$normalizer.'.inner'), new Reference('serializer.data_collector')]);
+ }
+ }
+
$serializerDefinition = $container->getDefinition('serializer');
- $serializerDefinition->replaceArgument(0, $normalizers);
+ $serializerDefinition->replaceArgument(0, $this->findAndSortTaggedServices('serializer.normalizer', $container));
- if (!$encoders = $this->findAndSortTaggedServices('serializer.encoder', $container)) {
+ if (!$encoders = $container->findTaggedServiceIds('serializer.encoder')) {
throw new RuntimeException('You must tag at least one service as "serializer.encoder" to use the "serializer" service.');
}
- $serializerDefinition->replaceArgument(1, $encoders);
+ if ($container->getParameter('kernel.debug') && $container->hasDefinition('serializer.data_collector')) {
+ foreach (array_keys($encoders) as $encoder) {
+ $container->register('debug.'.$encoder, TraceableEncoder::class)
+ ->setDecoratedService($encoder, null, 255)
+ ->setArguments([new Reference('debug.'.$encoder.'.inner'), new Reference('serializer.data_collector')]);
+ }
+ }
+
+ $serializerDefinition->replaceArgument(1, $this->findAndSortTaggedServices('serializer.encoder', $container));
if (!$container->hasParameter('serializer.default_context')) {
return;
diff --git a/src/Symfony/Component/Serializer/Encoder/ChainEncoder.php b/src/Symfony/Component/Serializer/Encoder/ChainEncoder.php
index dec57e4945d52..70d7c9fd10645 100644
--- a/src/Symfony/Component/Serializer/Encoder/ChainEncoder.php
+++ b/src/Symfony/Component/Serializer/Encoder/ChainEncoder.php
@@ -11,6 +11,7 @@
namespace Symfony\Component\Serializer\Encoder;
+use Symfony\Component\Serializer\Debug\TraceableEncoder;
use Symfony\Component\Serializer\Exception\RuntimeException;
/**
@@ -61,6 +62,10 @@ public function needsNormalization(string $format, array $context = []): bool
{
$encoder = $this->getEncoder($format, $context);
+ if ($encoder instanceof TraceableEncoder) {
+ return $encoder->needsNormalization();
+ }
+
if (!$encoder instanceof NormalizationAwareInterface) {
return true;
}
diff --git a/src/Symfony/Component/Serializer/Tests/DataCollector/SerializerDataCollectorTest.php b/src/Symfony/Component/Serializer/Tests/DataCollector/SerializerDataCollectorTest.php
new file mode 100644
index 0000000000000..7b22b6064e0d2
--- /dev/null
+++ b/src/Symfony/Component/Serializer/Tests/DataCollector/SerializerDataCollectorTest.php
@@ -0,0 +1,293 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Serializer\Tests\DataCollector;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Serializer\DataCollector\SerializerDataCollector;
+use Symfony\Component\Serializer\Encoder\CsvEncoder;
+use Symfony\Component\Serializer\Encoder\JsonEncoder;
+use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
+use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
+
+class SerializerDataCollectorTest extends TestCase
+{
+ public function testCollectSerialize()
+ {
+ $dataCollector = new SerializerDataCollector();
+
+ $dataCollector->collectSerialize('traceIdOne', 'data', 'format', ['foo' => 'bar'], 1.0);
+ $dataCollector->collectDeserialize('traceIdTwo', 'data', 'type', 'format', ['foo' => 'bar'], 1.0);
+
+ $dataCollector->lateCollect();
+ $collectedData = $this->castCollectedData($dataCollector->getData());
+
+ $this->assertSame([[
+ 'data' => 'data',
+ 'dataType' => 'string',
+ 'type' => null,
+ 'format' => 'format',
+ 'time' => 1.0,
+ 'context' => ['foo' => 'bar'],
+ 'normalization' => [],
+ 'encoding' => [],
+ ]], $collectedData['serialize']);
+
+ $this->assertSame([[
+ 'data' => 'data',
+ 'dataType' => 'string',
+ 'type' => 'type',
+ 'format' => 'format',
+ 'time' => 1.0,
+ 'context' => ['foo' => 'bar'],
+ 'normalization' => [],
+ 'encoding' => [],
+ ]], $collectedData['deserialize']);
+ }
+
+ public function testCollectNormalize()
+ {
+ $dataCollector = new SerializerDataCollector();
+
+ $dataCollector->collectNormalize('traceIdOne', 'data', 'format', ['foo' => 'bar'], 1.0);
+ $dataCollector->collectDenormalize('traceIdTwo', 'data', 'type', 'format', ['foo' => 'bar'], 1.0);
+
+ $dataCollector->lateCollect();
+ $collectedData = $this->castCollectedData($dataCollector->getData());
+
+ $this->assertSame([[
+ 'data' => 'data',
+ 'dataType' => 'string',
+ 'type' => null,
+ 'format' => 'format',
+ 'time' => 1.0,
+ 'context' => ['foo' => 'bar'],
+ 'normalization' => [],
+ 'encoding' => [],
+ ]], $collectedData['normalize']);
+
+ $this->assertSame([[
+ 'data' => 'data',
+ 'dataType' => 'string',
+ 'type' => 'type',
+ 'format' => 'format',
+ 'time' => 1.0,
+ 'context' => ['foo' => 'bar'],
+ 'normalization' => [],
+ 'encoding' => [],
+ ]], $collectedData['denormalize']);
+ }
+
+ public function testCollectEncode()
+ {
+ $dataCollector = new SerializerDataCollector();
+
+ $dataCollector->collectEncode('traceIdOne', 'data', 'format', ['foo' => 'bar'], 1.0);
+ $dataCollector->collectDecode('traceIdTwo', 'data', 'format', ['foo' => 'bar'], 1.0);
+
+ $dataCollector->lateCollect();
+ $collectedData = $this->castCollectedData($dataCollector->getData());
+
+ $this->assertSame([[
+ 'data' => 'data',
+ 'dataType' => 'string',
+ 'type' => null,
+ 'format' => 'format',
+ 'time' => 1.0,
+ 'context' => ['foo' => 'bar'],
+ 'normalization' => [],
+ 'encoding' => [],
+ ]], $collectedData['encode']);
+
+ $this->assertSame([[
+ 'data' => 'data',
+ 'dataType' => 'string',
+ 'type' => null,
+ 'format' => 'format',
+ 'time' => 1.0,
+ 'context' => ['foo' => 'bar'],
+ 'normalization' => [],
+ 'encoding' => [],
+ ]], $collectedData['decode']);
+ }
+
+ public function testCollectNormalization()
+ {
+ $dataCollector = new SerializerDataCollector();
+
+ $dataCollector->collectNormalize('traceIdOne', 'data', 'format', ['foo' => 'bar'], 20.0);
+ $dataCollector->collectDenormalize('traceIdTwo', 'data', 'type', 'format', ['foo' => 'bar'], 20.0);
+
+ $dataCollector->collectNormalization('traceIdOne', DateTimeNormalizer::class, 1.0);
+ $dataCollector->collectNormalization('traceIdOne', DateTimeNormalizer::class, 2.0);
+ $dataCollector->collectNormalization('traceIdOne', ObjectNormalizer::class, 5.0);
+ $dataCollector->collectNormalization('traceIdOne', ObjectNormalizer::class, 10.0);
+
+ $dataCollector->collectNormalization('traceIdTwo', DateTimeNormalizer::class, 1.0);
+ $dataCollector->collectNormalization('traceIdTwo', DateTimeNormalizer::class, 2.0);
+ $dataCollector->collectNormalization('traceIdTwo', ObjectNormalizer::class, 5.0);
+ $dataCollector->collectNormalization('traceIdTwo', ObjectNormalizer::class, 10.0);
+
+ $dataCollector->lateCollect();
+ $collectedData = $dataCollector->getData();
+
+ $this->assertSame(10.0, $collectedData['normalize'][0]['normalizer']['time']);
+ $this->assertSame('ObjectNormalizer', $collectedData['normalize'][0]['normalizer']['class']);
+ $this->assertArrayHasKey('file', $collectedData['normalize'][0]['normalizer']);
+ $this->assertArrayHasKey('line', $collectedData['normalize'][0]['normalizer']);
+
+ $this->assertSame(3.0, $collectedData['normalize'][0]['normalization'][DateTimeNormalizer::class]['time']);
+ $this->assertSame(2, $collectedData['normalize'][0]['normalization'][DateTimeNormalizer::class]['calls']);
+ $this->assertSame('DateTimeNormalizer', $collectedData['normalize'][0]['normalization'][DateTimeNormalizer::class]['class']);
+ $this->assertArrayHasKey('file', $collectedData['normalize'][0]['normalization'][DateTimeNormalizer::class]);
+ $this->assertArrayHasKey('line', $collectedData['normalize'][0]['normalization'][DateTimeNormalizer::class]);
+
+ $this->assertSame(5.0, $collectedData['normalize'][0]['normalization'][ObjectNormalizer::class]['time']);
+ $this->assertSame(1, $collectedData['normalize'][0]['normalization'][ObjectNormalizer::class]['calls']);
+ $this->assertSame('ObjectNormalizer', $collectedData['normalize'][0]['normalization'][ObjectNormalizer::class]['class']);
+ $this->assertArrayHasKey('file', $collectedData['normalize'][0]['normalization'][ObjectNormalizer::class]);
+ $this->assertArrayHasKey('line', $collectedData['normalize'][0]['normalization'][ObjectNormalizer::class]);
+
+ $this->assertSame(10.0, $collectedData['denormalize'][0]['normalizer']['time']);
+ $this->assertSame('ObjectNormalizer', $collectedData['denormalize'][0]['normalizer']['class']);
+ $this->assertArrayHasKey('file', $collectedData['denormalize'][0]['normalizer']);
+ $this->assertArrayHasKey('line', $collectedData['denormalize'][0]['normalizer']);
+
+ $this->assertSame(3.0, $collectedData['denormalize'][0]['normalization'][DateTimeNormalizer::class]['time']);
+ $this->assertSame(2, $collectedData['denormalize'][0]['normalization'][DateTimeNormalizer::class]['calls']);
+ $this->assertSame('DateTimeNormalizer', $collectedData['denormalize'][0]['normalization'][DateTimeNormalizer::class]['class']);
+ $this->assertArrayHasKey('file', $collectedData['denormalize'][0]['normalization'][DateTimeNormalizer::class]);
+ $this->assertArrayHasKey('line', $collectedData['denormalize'][0]['normalization'][DateTimeNormalizer::class]);
+
+ $this->assertSame(5.0, $collectedData['denormalize'][0]['normalization'][ObjectNormalizer::class]['time']);
+ $this->assertSame(1, $collectedData['denormalize'][0]['normalization'][ObjectNormalizer::class]['calls']);
+ $this->assertSame('ObjectNormalizer', $collectedData['denormalize'][0]['normalization'][ObjectNormalizer::class]['class']);
+ $this->assertArrayHasKey('file', $collectedData['denormalize'][0]['normalization'][ObjectNormalizer::class]);
+ $this->assertArrayHasKey('line', $collectedData['denormalize'][0]['normalization'][ObjectNormalizer::class]);
+ }
+
+ public function testCollectEncoding()
+ {
+ $dataCollector = new SerializerDataCollector();
+
+ $dataCollector->collectEncode('traceIdOne', 'data', 'format', ['foo' => 'bar'], 20.0);
+ $dataCollector->collectDecode('traceIdTwo', 'data', 'format', ['foo' => 'bar'], 20.0);
+
+ $dataCollector->collectEncoding('traceIdOne', JsonEncoder::class, 1.0);
+ $dataCollector->collectEncoding('traceIdOne', JsonEncoder::class, 2.0);
+ $dataCollector->collectEncoding('traceIdOne', CsvEncoder::class, 5.0);
+ $dataCollector->collectEncoding('traceIdOne', CsvEncoder::class, 10.0);
+
+ $dataCollector->collectDecoding('traceIdTwo', JsonEncoder::class, 1.0);
+ $dataCollector->collectDecoding('traceIdTwo', JsonEncoder::class, 2.0);
+ $dataCollector->collectDecoding('traceIdTwo', CsvEncoder::class, 5.0);
+ $dataCollector->collectDecoding('traceIdTwo', CsvEncoder::class, 10.0);
+
+ $dataCollector->lateCollect();
+ $collectedData = $dataCollector->getData();
+
+ $this->assertSame(10.0, $collectedData['encode'][0]['encoder']['time']);
+ $this->assertSame('CsvEncoder', $collectedData['encode'][0]['encoder']['class']);
+ $this->assertArrayHasKey('file', $collectedData['encode'][0]['encoder']);
+ $this->assertArrayHasKey('line', $collectedData['encode'][0]['encoder']);
+
+ $this->assertSame(3.0, $collectedData['encode'][0]['encoding'][JsonEncoder::class]['time']);
+ $this->assertSame(2, $collectedData['encode'][0]['encoding'][JsonEncoder::class]['calls']);
+ $this->assertSame('JsonEncoder', $collectedData['encode'][0]['encoding'][JsonEncoder::class]['class']);
+ $this->assertArrayHasKey('file', $collectedData['encode'][0]['encoding'][JsonEncoder::class]);
+ $this->assertArrayHasKey('line', $collectedData['encode'][0]['encoding'][JsonEncoder::class]);
+
+ $this->assertSame(5.0, $collectedData['encode'][0]['encoding'][CsvEncoder::class]['time']);
+ $this->assertSame(1, $collectedData['encode'][0]['encoding'][CsvEncoder::class]['calls']);
+ $this->assertSame('CsvEncoder', $collectedData['encode'][0]['encoding'][CsvEncoder::class]['class']);
+ $this->assertArrayHasKey('file', $collectedData['encode'][0]['encoding'][CsvEncoder::class]);
+ $this->assertArrayHasKey('line', $collectedData['encode'][0]['encoding'][CsvEncoder::class]);
+
+ $this->assertSame(10.0, $collectedData['decode'][0]['encoder']['time']);
+ $this->assertSame('CsvEncoder', $collectedData['decode'][0]['encoder']['class']);
+ $this->assertArrayHasKey('file', $collectedData['decode'][0]['encoder']);
+ $this->assertArrayHasKey('line', $collectedData['decode'][0]['encoder']);
+
+ $this->assertSame(3.0, $collectedData['decode'][0]['encoding'][JsonEncoder::class]['time']);
+ $this->assertSame(2, $collectedData['decode'][0]['encoding'][JsonEncoder::class]['calls']);
+ $this->assertSame('JsonEncoder', $collectedData['decode'][0]['encoding'][JsonEncoder::class]['class']);
+ $this->assertArrayHasKey('file', $collectedData['decode'][0]['encoding'][JsonEncoder::class]);
+ $this->assertArrayHasKey('line', $collectedData['decode'][0]['encoding'][JsonEncoder::class]);
+
+ $this->assertSame(5.0, $collectedData['decode'][0]['encoding'][CsvEncoder::class]['time']);
+ $this->assertSame(1, $collectedData['decode'][0]['encoding'][CsvEncoder::class]['calls']);
+ $this->assertSame('CsvEncoder', $collectedData['decode'][0]['encoding'][CsvEncoder::class]['class']);
+ $this->assertArrayHasKey('file', $collectedData['decode'][0]['encoding'][CsvEncoder::class]);
+ $this->assertArrayHasKey('line', $collectedData['decode'][0]['encoding'][CsvEncoder::class]);
+ }
+
+ public function testCountHandled()
+ {
+ $dataCollector = new SerializerDataCollector();
+
+ $dataCollector->collectSerialize('traceIdOne', 'data', 'format', ['foo' => 'bar'], 1.0);
+ $dataCollector->collectDeserialize('traceIdTwo', 'data', 'type', 'format', ['foo' => 'bar'], 1.0);
+ $dataCollector->collectNormalize('traceIdThree', 'data', 'format', ['foo' => 'bar'], 20.0);
+ $dataCollector->collectDenormalize('traceIdFour', 'data', 'type', 'format', ['foo' => 'bar'], 20.0);
+ $dataCollector->collectEncode('traceIdFive', 'data', 'format', ['foo' => 'bar'], 20.0);
+ $dataCollector->collectDecode('traceIdSix', 'data', 'format', ['foo' => 'bar'], 20.0);
+ $dataCollector->collectSerialize('traceIdSeven', 'data', 'format', ['foo' => 'bar'], 1.0);
+
+ $dataCollector->lateCollect();
+
+ $this->assertSame(7, $dataCollector->getHandledCount());
+ }
+
+ public function testGetTotalTime()
+ {
+ $dataCollector = new SerializerDataCollector();
+
+ $dataCollector->collectSerialize('traceIdOne', 'data', 'format', ['foo' => 'bar'], 1.0);
+ $dataCollector->collectDeserialize('traceIdTwo', 'data', 'type', 'format', ['foo' => 'bar'], 2.0);
+ $dataCollector->collectNormalize('traceIdThree', 'data', 'format', ['foo' => 'bar'], 3.0);
+ $dataCollector->collectDenormalize('traceIdFour', 'data', 'type', 'format', ['foo' => 'bar'], 4.0);
+ $dataCollector->collectEncode('traceIdFive', 'data', 'format', ['foo' => 'bar'], 5.0);
+ $dataCollector->collectDecode('traceIdSix', 'data', 'format', ['foo' => 'bar'], 6.0);
+ $dataCollector->collectSerialize('traceIdSeven', 'data', 'format', ['foo' => 'bar'], 7.0);
+
+ $dataCollector->lateCollect();
+
+ $this->assertSame(28.0, $dataCollector->getTotalTime());
+ }
+
+ public function testReset()
+ {
+ $dataCollector = new SerializerDataCollector();
+
+ $dataCollector->collectSerialize('traceIdOne', 'data', 'format', ['foo' => 'bar'], 1.0);
+ $dataCollector->lateCollect();
+
+ $this->assertNotSame([], $dataCollector->getData());
+
+ $dataCollector->reset();
+ $this->assertSame([], $dataCollector->getData());
+ }
+
+ /**
+ * Cast cloned vars to be able to test nested values.
+ */
+ private function castCollectedData(array $collectedData): array
+ {
+ foreach ($collectedData as $method => $collectedMethodData) {
+ foreach ($collectedMethodData as $i => $collected) {
+ $collectedData[$method][$i]['data'] = $collected['data']->getValue();
+ $collectedData[$method][$i]['context'] = $collected['context']->getValue(true);
+ }
+ }
+
+ return $collectedData;
+ }
+}
diff --git a/src/Symfony/Component/Serializer/Tests/Debug/TraceableEncoderTest.php b/src/Symfony/Component/Serializer/Tests/Debug/TraceableEncoderTest.php
new file mode 100644
index 0000000000000..aa2393bbe7f69
--- /dev/null
+++ b/src/Symfony/Component/Serializer/Tests/Debug/TraceableEncoderTest.php
@@ -0,0 +1,106 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Serializer\Tests\Debug;
+
+use BadMethodCallException;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Serializer\DataCollector\SerializerDataCollector;
+use Symfony\Component\Serializer\Debug\TraceableEncoder;
+use Symfony\Component\Serializer\Debug\TraceableSerializer;
+use Symfony\Component\Serializer\Encoder\DecoderInterface;
+use Symfony\Component\Serializer\Encoder\EncoderInterface;
+
+class TraceableEncoderTest extends TestCase
+{
+ public function testForwardsToEncoder()
+ {
+ $encoder = $this->createMock(EncoderInterface::class);
+ $encoder
+ ->expects($this->once())
+ ->method('encode')
+ ->with('data', 'format', $this->isType('array'))
+ ->willReturn('encoded');
+
+ $decoder = $this->createMock(DecoderInterface::class);
+ $decoder
+ ->expects($this->once())
+ ->method('decode')
+ ->with('data', 'format', $this->isType('array'))
+ ->willReturn('decoded');
+
+ $this->assertSame('encoded', (new TraceableEncoder($encoder, new SerializerDataCollector()))->encode('data', 'format'));
+ $this->assertSame('decoded', (new TraceableEncoder($decoder, new SerializerDataCollector()))->decode('data', 'format'));
+ }
+
+ public function testCollectEncodingData()
+ {
+ $encoder = $this->createMock(EncoderInterface::class);
+ $decoder = $this->createMock(DecoderInterface::class);
+
+ $dataCollector = $this->createMock(SerializerDataCollector::class);
+ $dataCollector
+ ->expects($this->once())
+ ->method('collectEncoding')
+ ->with($this->isType('string'), \get_class($encoder), $this->isType('float'));
+ $dataCollector
+ ->expects($this->once())
+ ->method('collectDecoding')
+ ->with($this->isType('string'), \get_class($decoder), $this->isType('float'));
+
+ (new TraceableEncoder($encoder, $dataCollector))->encode('data', 'format', [TraceableSerializer::DEBUG_TRACE_ID => 'debug']);
+ (new TraceableEncoder($decoder, $dataCollector))->decode('data', 'format', [TraceableSerializer::DEBUG_TRACE_ID => 'debug']);
+ }
+
+ public function testNotCollectEncodingDataIfNoDebugTraceId()
+ {
+ $encoder = $this->createMock(EncoderInterface::class);
+ $decoder = $this->createMock(DecoderInterface::class);
+
+ $dataCollector = $this->createMock(SerializerDataCollector::class);
+ $dataCollector->expects($this->never())->method('collectEncoding');
+ $dataCollector->expects($this->never())->method('collectDecoding');
+
+ (new TraceableEncoder($encoder, $dataCollector))->encode('data', 'format');
+ (new TraceableEncoder($decoder, $dataCollector))->decode('data', 'format');
+ }
+
+ public function testCannotEncodeIfNotEncoder()
+ {
+ $this->expectException(BadMethodCallException::class);
+
+ (new TraceableEncoder($this->createMock(DecoderInterface::class), new SerializerDataCollector()))->encode('data', 'format');
+ }
+
+ public function testCannotDecodeIfNotDecoder()
+ {
+ $this->expectException(BadMethodCallException::class);
+
+ (new TraceableEncoder($this->createMock(EncoderInterface::class), new SerializerDataCollector()))->decode('data', 'format');
+ }
+
+ public function testSupports()
+ {
+ $encoder = $this->createMock(EncoderInterface::class);
+ $encoder->method('supportsEncoding')->willReturn(true);
+
+ $decoder = $this->createMock(DecoderInterface::class);
+ $decoder->method('supportsDecoding')->willReturn(true);
+
+ $traceableEncoder = new TraceableEncoder($encoder, new SerializerDataCollector());
+ $traceableDecoder = new TraceableEncoder($decoder, new SerializerDataCollector());
+
+ $this->assertTrue($traceableEncoder->supportsEncoding('data'));
+ $this->assertTrue($traceableDecoder->supportsDecoding('data'));
+ $this->assertFalse($traceableEncoder->supportsDecoding('data'));
+ $this->assertFalse($traceableDecoder->supportsEncoding('data'));
+ }
+}
diff --git a/src/Symfony/Component/Serializer/Tests/Debug/TraceableNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Debug/TraceableNormalizerTest.php
new file mode 100644
index 0000000000000..dc99a03c03b22
--- /dev/null
+++ b/src/Symfony/Component/Serializer/Tests/Debug/TraceableNormalizerTest.php
@@ -0,0 +1,106 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Serializer\Tests\Debug;
+
+use BadMethodCallException;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Serializer\DataCollector\SerializerDataCollector;
+use Symfony\Component\Serializer\Debug\TraceableNormalizer;
+use Symfony\Component\Serializer\Debug\TraceableSerializer;
+use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
+use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
+
+class TraceableNormalizerTest extends TestCase
+{
+ public function testForwardsToNormalizer()
+ {
+ $normalizer = $this->createMock(NormalizerInterface::class);
+ $normalizer
+ ->expects($this->once())
+ ->method('normalize')
+ ->with('data', 'format', $this->isType('array'))
+ ->willReturn('normalized');
+
+ $denormalizer = $this->createMock(DenormalizerInterface::class);
+ $denormalizer
+ ->expects($this->once())
+ ->method('denormalize')
+ ->with('data', 'type', 'format', $this->isType('array'))
+ ->willReturn('denormalized');
+
+ $this->assertSame('normalized', (new TraceableNormalizer($normalizer, new SerializerDataCollector()))->normalize('data', 'format'));
+ $this->assertSame('denormalized', (new TraceableNormalizer($denormalizer, new SerializerDataCollector()))->denormalize('data', 'type', 'format'));
+ }
+
+ public function testCollectNormalizationData()
+ {
+ $normalizer = $this->createMock(NormalizerInterface::class);
+ $denormalizer = $this->createMock(DenormalizerInterface::class);
+
+ $dataCollector = $this->createMock(SerializerDataCollector::class);
+ $dataCollector
+ ->expects($this->once())
+ ->method('collectNormalization')
+ ->with($this->isType('string'), \get_class($normalizer), $this->isType('float'));
+ $dataCollector
+ ->expects($this->once())
+ ->method('collectDenormalization')
+ ->with($this->isType('string'), \get_class($denormalizer), $this->isType('float'));
+
+ (new TraceableNormalizer($normalizer, $dataCollector))->normalize('data', 'format', [TraceableSerializer::DEBUG_TRACE_ID => 'debug']);
+ (new TraceableNormalizer($denormalizer, $dataCollector))->denormalize('data', 'type', 'format', [TraceableSerializer::DEBUG_TRACE_ID => 'debug']);
+ }
+
+ public function testNotCollectNormalizationDataIfNoDebugTraceId()
+ {
+ $normalizer = $this->createMock(NormalizerInterface::class);
+ $denormalizer = $this->createMock(DenormalizerInterface::class);
+
+ $dataCollector = $this->createMock(SerializerDataCollector::class);
+ $dataCollector->expects($this->never())->method('collectNormalization');
+ $dataCollector->expects($this->never())->method('collectDenormalization');
+
+ (new TraceableNormalizer($normalizer, $dataCollector))->normalize('data', 'format');
+ (new TraceableNormalizer($denormalizer, $dataCollector))->denormalize('data', 'type', 'format');
+ }
+
+ public function testCannotNormalizeIfNotNormalizer()
+ {
+ $this->expectException(BadMethodCallException::class);
+
+ (new TraceableNormalizer($this->createMock(DenormalizerInterface::class), new SerializerDataCollector()))->normalize('data');
+ }
+
+ public function testCannotDenormalizeIfNotDenormalizer()
+ {
+ $this->expectException(BadMethodCallException::class);
+
+ (new TraceableNormalizer($this->createMock(NormalizerInterface::class), new SerializerDataCollector()))->denormalize('data', 'type');
+ }
+
+ public function testSupports()
+ {
+ $normalizer = $this->createMock(NormalizerInterface::class);
+ $normalizer->method('supportsNormalization')->willReturn(true);
+
+ $denormalizer = $this->createMock(DenormalizerInterface::class);
+ $denormalizer->method('supportsDenormalization')->willReturn(true);
+
+ $traceableNormalizer = new TraceableNormalizer($normalizer, new SerializerDataCollector());
+ $traceableDenormalizer = new TraceableNormalizer($denormalizer, new SerializerDataCollector());
+
+ $this->assertTrue($traceableNormalizer->supportsNormalization('data'));
+ $this->assertTrue($traceableDenormalizer->supportsDenormalization('data', 'type'));
+ $this->assertFalse($traceableNormalizer->supportsDenormalization('data', 'type'));
+ $this->assertFalse($traceableDenormalizer->supportsNormalization('data'));
+ }
+}
diff --git a/src/Symfony/Component/Serializer/Tests/Debug/TraceableSerializerTest.php b/src/Symfony/Component/Serializer/Tests/Debug/TraceableSerializerTest.php
new file mode 100644
index 0000000000000..ae8a01623cdbf
--- /dev/null
+++ b/src/Symfony/Component/Serializer/Tests/Debug/TraceableSerializerTest.php
@@ -0,0 +1,194 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Serializer\Tests\Debug;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Serializer\DataCollector\SerializerDataCollector;
+use Symfony\Component\Serializer\Debug\TraceableSerializer;
+use Symfony\Component\Serializer\Encoder\DecoderInterface;
+use Symfony\Component\Serializer\Encoder\EncoderInterface;
+use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
+use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
+use Symfony\Component\Serializer\SerializerInterface;
+
+class TraceableSerializerTest extends TestCase
+{
+ public function testForwardsToSerializer()
+ {
+ $serializer = $this->createMock(Serializer::class);
+ $serializer
+ ->expects($this->once())
+ ->method('serialize')
+ ->with('data', 'format', $this->isType('array'))
+ ->willReturn('serialized');
+ $serializer
+ ->expects($this->once())
+ ->method('deserialize')
+ ->with('data', 'type', 'format', $this->isType('array'))
+ ->willReturn('deserialized');
+ $serializer
+ ->expects($this->once())
+ ->method('normalize')
+ ->with('data', 'format', $this->isType('array'))
+ ->willReturn('normalized');
+ $serializer
+ ->expects($this->once())
+ ->method('denormalize')
+ ->with('data', 'type', 'format', $this->isType('array'))
+ ->willReturn('denormalized');
+ $serializer
+ ->expects($this->once())
+ ->method('encode')
+ ->with('data', 'format', $this->isType('array'))
+ ->willReturn('encoded');
+ $serializer
+ ->expects($this->once())
+ ->method('decode')
+ ->with('data', 'format', $this->isType('array'))
+ ->willReturn('decoded');
+
+ $traceableSerializer = new TraceableSerializer($serializer, new SerializerDataCollector());
+
+ $this->assertSame('serialized', $traceableSerializer->serialize('data', 'format'));
+ $this->assertSame('deserialized', $traceableSerializer->deserialize('data', 'type', 'format'));
+ $this->assertSame('normalized', $traceableSerializer->normalize('data', 'format'));
+ $this->assertSame('denormalized', $traceableSerializer->denormalize('data', 'type', 'format'));
+ $this->assertSame('encoded', $traceableSerializer->encode('data', 'format'));
+ $this->assertSame('decoded', $traceableSerializer->decode('data', 'format'));
+ }
+
+ public function testCollectData()
+ {
+ $dataCollector = $this->createMock(SerializerDataCollector::class);
+ $dataCollector
+ ->expects($this->once())
+ ->method('collectSerialize')
+ ->with($this->isType('string'), 'data', 'format', $this->isType('array'), $this->isType('float'));
+ $dataCollector
+ ->expects($this->once())
+ ->method('collectDeserialize')
+ ->with($this->isType('string'), 'data', 'type', 'format', $this->isType('array'), $this->isType('float'));
+ $dataCollector
+ ->expects($this->once())
+ ->method('collectNormalize')
+ ->with($this->isType('string'), 'data', 'format', $this->isType('array'), $this->isType('float'));
+ $dataCollector
+ ->expects($this->once())
+ ->method('collectDenormalize')
+ ->with($this->isType('string'), 'data', 'type', 'format', $this->isType('array'), $this->isType('float'));
+ $dataCollector
+ ->expects($this->once())
+ ->method('collectEncode')
+ ->with($this->isType('string'), 'data', 'format', $this->isType('array'), $this->isType('float'));
+ $dataCollector
+ ->expects($this->once())
+ ->method('collectDecode')
+ ->with($this->isType('string'), 'data', 'format', $this->isType('array'), $this->isType('float'));
+
+ $traceableSerializer = new TraceableSerializer(new Serializer(), $dataCollector);
+
+ $traceableSerializer->serialize('data', 'format');
+ $traceableSerializer->deserialize('data', 'type', 'format');
+ $traceableSerializer->normalize('data', 'format');
+ $traceableSerializer->denormalize('data', 'type', 'format');
+ $traceableSerializer->encode('data', 'format');
+ $traceableSerializer->decode('data', 'format');
+ }
+
+ public function testAddDebugTraceIdInContext()
+ {
+ $serializer = $this->createMock(Serializer::class);
+
+ foreach (['serialize', 'deserialize', 'normalize', 'denormalize', 'encode', 'decode'] as $method) {
+ $serializer->method($method)->willReturnCallback(function (): string {
+ $context = func_get_arg(\func_num_args() - 1);
+ $this->assertIsString($context[TraceableSerializer::DEBUG_TRACE_ID]);
+
+ return '';
+ });
+ }
+
+ $traceableSerializer = new TraceableSerializer($serializer, new SerializerDataCollector());
+
+ $traceableSerializer->serialize('data', 'format');
+ $traceableSerializer->deserialize('data', 'format', 'type');
+ $traceableSerializer->normalize('data', 'format');
+ $traceableSerializer->denormalize('data', 'format');
+ $traceableSerializer->encode('data', 'format');
+ $traceableSerializer->decode('data', 'format');
+ }
+}
+
+class Serializer implements SerializerInterface, NormalizerInterface, DenormalizerInterface, EncoderInterface, DecoderInterface
+{
+ public function serialize(mixed $data, string $format, array $context = []): string
+ {
+ return 'serialized';
+ }
+
+ public function deserialize(mixed $data, string $type, string $format, array $context = []): mixed
+ {
+ return 'deserialized';
+ }
+
+ public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null
+ {
+ return 'normalized';
+ }
+
+ /**
+ * @param array $context
+ */
+ public function supportsNormalization(mixed $data, string $format = null /*, array $context = [] */): bool
+ {
+ return true;
+ }
+
+ public function denormalize(mixed $data, string $type, string $format = null, array $context = []): mixed
+ {
+ return 'denormalized';
+ }
+
+ /**
+ * @param array $context
+ */
+ public function supportsDenormalization(mixed $data, string $type, string $format = null /*, array $context = [] */): bool
+ {
+ return true;
+ }
+
+ public function encode(mixed $data, string $format, array $context = []): string
+ {
+ return 'encoded';
+ }
+
+ /**
+ * @param array $context
+ */
+ public function supportsEncoding(string $format /*, array $context = [] */): bool
+ {
+ return true;
+ }
+
+ public function decode(string $data, string $format, array $context = []): mixed
+ {
+ return 'decoded';
+ }
+
+ /**
+ * @param array $context
+ */
+ public function supportsDecoding(string $format /*, array $context = [] */): bool
+ {
+ return true;
+ }
+}
diff --git a/src/Symfony/Component/Serializer/Tests/DependencyInjection/SerializerPassTest.php b/src/Symfony/Component/Serializer/Tests/DependencyInjection/SerializerPassTest.php
index e9b1efd228d10..92c258a822af1 100644
--- a/src/Symfony/Component/Serializer/Tests/DependencyInjection/SerializerPassTest.php
+++ b/src/Symfony/Component/Serializer/Tests/DependencyInjection/SerializerPassTest.php
@@ -15,6 +15,8 @@
use Symfony\Component\DependencyInjection\Argument\BoundArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
+use Symfony\Component\Serializer\Debug\TraceableEncoder;
+use Symfony\Component\Serializer\Debug\TraceableNormalizer;
use Symfony\Component\Serializer\DependencyInjection\SerializerPass;
/**
@@ -29,6 +31,7 @@ public function testThrowExceptionWhenNoNormalizers()
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('You must tag at least one service as "serializer.normalizer" to use the "serializer" service');
$container = new ContainerBuilder();
+ $container->setParameter('kernel.debug', false);
$container->register('serializer');
$serializerPass = new SerializerPass();
@@ -40,6 +43,7 @@ public function testThrowExceptionWhenNoEncoders()
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('You must tag at least one service as "serializer.encoder" to use the "serializer" service');
$container = new ContainerBuilder();
+ $container->setParameter('kernel.debug', false);
$container->register('serializer')
->addArgument([])
->addArgument([]);
@@ -52,6 +56,7 @@ public function testThrowExceptionWhenNoEncoders()
public function testServicesAreOrderedAccordingToPriority()
{
$container = new ContainerBuilder();
+ $container->setParameter('kernel.debug', false);
$definition = $container->register('serializer')->setArguments([null, null]);
$container->register('n2')->addTag('serializer.normalizer', ['priority' => 100])->addTag('serializer.encoder', ['priority' => 100]);
@@ -73,6 +78,7 @@ public function testServicesAreOrderedAccordingToPriority()
public function testBindSerializerDefaultContext()
{
$container = new ContainerBuilder();
+ $container->setParameter('kernel.debug', false);
$container->register('serializer')->setArguments([null, null]);
$container->setParameter('serializer.default_context', ['enable_max_depth' => true]);
$definition = $container->register('n1')->addTag('serializer.normalizer')->addTag('serializer.encoder');
@@ -83,4 +89,32 @@ public function testBindSerializerDefaultContext()
$bindings = $definition->getBindings();
$this->assertEquals($bindings['array $defaultContext'], new BoundArgument(['enable_max_depth' => true], false));
}
+
+ public function testNormalizersAndEncodersAreDecoredAndOrderedWhenCollectingData()
+ {
+ $container = new ContainerBuilder();
+
+ $container->setParameter('kernel.debug', true);
+ $container->register('serializer.data_collector');
+
+ $container->register('serializer')->setArguments([null, null]);
+ $container->register('n')->addTag('serializer.normalizer');
+ $container->register('e')->addTag('serializer.encoder');
+
+ $serializerPass = new SerializerPass();
+ $serializerPass->process($container);
+
+ $traceableNormalizerDefinition = $container->getDefinition('debug.n');
+ $traceableEncoderDefinition = $container->getDefinition('debug.e');
+
+ $this->assertEquals(TraceableNormalizer::class, $traceableNormalizerDefinition->getClass());
+ $this->assertEquals(['n', null, 255], $traceableNormalizerDefinition->getDecoratedService());
+ $this->assertEquals(new Reference('debug.n.inner'), $traceableNormalizerDefinition->getArgument(0));
+ $this->assertEquals(new Reference('serializer.data_collector'), $traceableNormalizerDefinition->getArgument(1));
+
+ $this->assertEquals(TraceableEncoder::class, $traceableEncoderDefinition->getClass());
+ $this->assertEquals(['e', null, 255], $traceableEncoderDefinition->getDecoratedService());
+ $this->assertEquals(new Reference('debug.e.inner'), $traceableEncoderDefinition->getArgument(0));
+ $this->assertEquals(new Reference('serializer.data_collector'), $traceableEncoderDefinition->getArgument(1));
+ }
}
diff --git a/src/Symfony/Component/Serializer/Tests/Encoder/ChainEncoderTest.php b/src/Symfony/Component/Serializer/Tests/Encoder/ChainEncoderTest.php
index 6f999f612ba19..848087145bafe 100644
--- a/src/Symfony/Component/Serializer/Tests/Encoder/ChainEncoderTest.php
+++ b/src/Symfony/Component/Serializer/Tests/Encoder/ChainEncoderTest.php
@@ -12,6 +12,7 @@
namespace Symfony\Component\Serializer\Tests\Encoder;
use PHPUnit\Framework\TestCase;
+use Symfony\Component\Serializer\Debug\TraceableEncoder;
use Symfony\Component\Serializer\Encoder\ChainEncoder;
use Symfony\Component\Serializer\Encoder\EncoderInterface;
use Symfony\Component\Serializer\Encoder\NormalizationAwareInterface;
@@ -86,6 +87,21 @@ public function testNeedsNormalizationNormalizationAware()
$this->assertFalse($sut->needsNormalization(self::FORMAT_1));
}
+
+ public function testNeedsNormalizationTraceableEncoder()
+ {
+ $traceableEncoder = $this->createMock(TraceableEncoder::class);
+ $traceableEncoder->method('needsNormalization')->willReturn(true);
+ $traceableEncoder->method('supportsEncoding')->willReturn(true);
+
+ $this->assertTrue((new ChainEncoder([$traceableEncoder]))->needsNormalization('format'));
+
+ $traceableEncoder = $this->createMock(TraceableEncoder::class);
+ $traceableEncoder->method('needsNormalization')->willReturn(false);
+ $traceableEncoder->method('supportsEncoding')->willReturn(true);
+
+ $this->assertFalse((new ChainEncoder([$traceableEncoder]))->needsNormalization('format'));
+ }
}
class NormalizationAwareEncoder implements EncoderInterface, NormalizationAwareInterface