-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[Serializer] add WebDebugToolbar collector for normalize/denormalize actions inside the serializer. #39325
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
Conversation
Hey! Nice work here, I this makes me happy. To help keep things organized, we don't allow "Draft" pull requests. Could you please click the "ready for review" button or close this PR and open a new one when you are done? Note that a pull request does not have to be "perfect" or "ready for merge" when you first open it. We just want it to be ready for a first review. Cheers! Carsonbot |
src/Symfony/Component/Serializer/Debug/Normalizer/AbstractTraceableNormalizer.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Serializer/Debug/Normalizer/Denormalization.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Serializer/Debug/Normalizer/Normalization.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Serializer/Tests/Debug/TraceableSerializerTest.php
Outdated
Show resolved
Hide resolved
Not to self: add a
|
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.
Looks good so far. I think we need to make sure that we limit the amount of data that we actually store in the profile. If an app serializes 120 MiB of data, we probably don't want the full output to be logged.
src/Symfony/Component/Serializer/DataCollector/SerializerDataCollector.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Serializer/DataCollector/SerializerDataCollector.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Serializer/Debug/Normalizer/AbstractTraceableNormalizer.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Serializer/Debug/Normalizer/AbstractTraceableNormalizer.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Serializer/Debug/Normalizer/Denormalization.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Serializer/Debug/Normalizer/Normalization.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Serializer/Tests/Debug/Normalizer/TestCacheableNormalizer.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Serializer/Tests/Debug/TraceableSerializerTest.php
Outdated
Show resolved
Hide resolved
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.
Thank you for working on this @Basster
src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/SerializerDebugPass.php
Outdated
Show resolved
Hide resolved
src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/SerializerDebugPass.php
Outdated
Show resolved
Hide resolved
src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/SerializerDebugPass.php
Outdated
Show resolved
Hide resolved
->setArguments([$normalizerDef]) | ||
->addTag('debug.normalizer') | ||
->setDecoratedService($id) | ||
->setAutowired(true) |
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.
Is it needed?
same for autoconfigure
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 think it's required indeed?
src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer_debug.php
Outdated
Show resolved
Hide resolved
src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/serializer.html.twig
Show resolved
Hide resolved
src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/serializer.html.twig
Show resolved
Hide resolved
src/Symfony/Component/Serializer/Debug/Normalizer/AbstractTraceableNormalizer.php
Outdated
Show resolved
Hide resolved
I think |
19a4e9b
to
fd5f1e8
Compare
I have no clue, why travis fails this particular build? |
…tor and data_collector for the container.
* and some code improvements.
… icon on serializer.html.twig.
…ep custom methods on custom normalizers working, which are not part of the interface.
…memory by replacing too large source-objects/results as replacement (LargeContent).
dae8641
to
f861ffe
Compare
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.
This is a huge work and needs to be tested in real-life applications with extensive usage of the Serializer to see how it behaves and how we can make it the most relevant.
It'll also be the the starting point for a case I left in #27262, about having Serializer metrics in the performance panel.
I think there is still a bit of work, but thank you @Basster for working on this!
} elseif ($isDenormalizer) { | ||
$decoratorClass = TraceableDenormalizer::class; | ||
} else { | ||
throw new RuntimeException(sprintf('Normalizer with id "%s" neither implements NormalizerInterface nor DenormalizerInterface!', $id)); |
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 think it should rather be a LogicException, since it' a user misconfiguration/code issue.
But I also wonder why this pass would throw while the SerializerPass
doesn't?
If it's interesting DX-wise, it should be in the SerializerPass
(not necessarily in this PR).
->setArguments([$normalizerDef]) | ||
->addTag('debug.normalizer') | ||
->setDecoratedService($id) | ||
->setAutowired(true) |
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 think it's required indeed?
->setAutowired(true) | ||
; | ||
|
||
$container->setDefinition($aliasName, $decoratorDef); |
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.
$aliasName
could be inlined here. Also what about using ->register
directly?
return $this->delegate->$name(...$arguments); | ||
} | ||
|
||
throw new \LogicException('Unexpected method call.'); |
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.
throw new \LogicException('Unexpected method call.'); | |
throw new \BadMethodCallException(sprintf('Unexpected "%s" method call.', $name)); |
?
|
||
public function setSerializer(SerializerInterface $serializer): void |
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.
public function setSerializer(SerializerInterface $serializer): void | |
public function setSerializer(SerializerInterface $serializer): void |
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; | ||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface; | ||
|
||
interface SerializerActionFactoryInterface |
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.
What is the use-case for this interface?
I don't think we need it
public function createDenormalization(DenormalizerInterface $denormalizer, $data, $result, string $type, string $format, array $context = []): Denormalization | ||
{ | ||
return new Denormalization( | ||
$denormalizer, |
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.
Do we need to hold a reference on the (de)normalizer instance here? What about storing the classname or original service name instead?
|
||
public function createNormalization(NormalizerInterface $normalizer, $object, $result, string $format, array $context = []): Normalization | ||
{ | ||
return new 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.
Same as raised by @jderusse in #39325 (comment), I'm not convinced about the need of an object to store these information for this specific use-case (yet I usually agree about not using arrays for structured data).
Also related to my comment about the factory interface, this factory is almost pure internal details of the traceable normalizers and serializer, to share some methods, so are these objects. The factory could be internal and instantiated from the traceable objects, or refactored as a trait.
if we'd like to keep these objects, I think at least:
- the factory interface should be removed
- the factory class and these objects must be
@internal
- the factory can be instantiated from the traceable instances directly
WDYT?
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.
In an API listing endpoint, the number of normalization calls can quickly grow and exposing each of these calls might not really be relevant nor handy for the developper.
I wonder if we shouldn't target something more concise, like only showing the root method calls to the serializer's serialize
, deserialize
, normalize
and denormalize
methods, instead of decorating each normalizers/denormalizers.
I know this would defeat some use-cases of this profiler panel, but debugging each (de)normalizers output might not be the role of this panel.
We could also add the context in which was made this call (source file and line N°), as in the validation panel:
The table output might not be the best to read neither:
Perhaps reversing the row/columns with a unique table per call would be better (as done for the Messenger panel).
<td class="nowrap">{{ normalization.format }}</td> | ||
<td class="nowrap">{{ normalization.normalizer | abbr_class }}</td> | ||
<td>{{ profiler_dump(normalization.data, maxDepth=2) }}</td> | ||
<td>{{ profiler_dump(normalization.result, maxDepth=2) }}</td> |
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.
In case the normalization result is a string, it'll fail with:
Argument 2 passed to Symfony\Bundle\WebProfilerBundle\Twig\WebProfilerExtension::dumpData() must be an instance of Symfony\Component\VarDumper\Cloner\Data, string given
The trick is to use seek
for such cases:
<td>{{ profiler_dump(normalization.result, maxDepth=2) }}</td> | |
<td>{{ profiler_dump(normalization.seek('result'), maxDepth=2) }}</td> |
<td class="font-normal text-small text-muted nowrap">{{ loop.index }}</td> | ||
<td class="nowrap">{{ normalization.format }}</td> | ||
<td class="nowrap">{{ normalization.normalizer | abbr_class }}</td> | ||
<td>{{ profiler_dump(normalization.data, maxDepth=2) }}</td> |
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.
Since the normalizer input is usually an object, but the interface accepts mixed
, it could be a string as well, so same here, use seek
<td class="nowrap">{{ denormalization.denormalizer | abbr_class }}</td> | ||
<td class="nowrap">{{ denormalization.type | abbr_class }}</td> | ||
<td>{{ profiler_dump(denormalization.data, maxDepth=2) }}</td> | ||
<td>{{ profiler_dump(denormalization.result, maxDepth=2) }}</td> |
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.
Same for denormalization data and result, use seek
<td class="font-normal text-small text-muted nowrap">{{ loop.index }}</td> | ||
<td class="nowrap">{{ serialization.format }}</td> | ||
<td>{{ profiler_dump(serialization.data, maxDepth=2) }}</td> | ||
<td>{{ serialization.result }}</td> |
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.
Serialization output is always a string, so not using profiler_dump
is okay, but using it can streamline the rendering in the profiler, and ellipse too long outputs:
<td>{{ serialization.result }}</td> | |
<td>{{ profiler_dump(serialization.seek('result')) }}</td> |
(should probably add a word-break: break-word;
at least)
vs
don't know if we want it or not.
<td class="font-normal text-small text-muted nowrap">{{ loop.index }}</td> | ||
<td class="nowrap">{{ deserialization.format }}</td> | ||
<td class="nowrap">{{ deserialization.type | abbr_class }}</td> | ||
<td>{{ deserialization.data }}</td> |
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.
Same here
<td class="nowrap">{{ deserialization.format }}</td> | ||
<td class="nowrap">{{ deserialization.type | abbr_class }}</td> | ||
<td>{{ deserialization.data }}</td> | ||
<td>{{ profiler_dump(deserialization.result, maxDepth=2) }}</td> |
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.
Deserialization result might be a string in some cases, so let's use seek
here too
|
||
<footer> | ||
<small class="text-muted"> | ||
The icon is taken from <a href="https://fontawesome.com/icons/exchange-alt?style=solid">fontawesome</a> |
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.
We'll probably add our own icon, once this PR is ready
return static function (ContainerConfigurator $container) { | ||
$container->services() | ||
->set('debug.serializer', TraceableSerializer::class) | ||
->decorate('serializer', null, 255) |
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.
The Serializer class also implements normalizers and encoders interfaces, hence the places in userland were the @serializer
service was injected and using encode
, decode
, normalize
or denormalize
will fail with:
Attempted to call an undefined method named "decode" of class "Symfony\Component\Serializer\Debug\TraceableSerializer".
and the DI aliases on the respective interfaces won't work anymore.
So decorating this service directly might not be a good idea?
We could decorate each alias instead, but then no data would be collected where the @serializer
service itself is injected.
So, should we have a traceable serializer implementing all of these interfaces? 🤔
{% endblock %} | ||
|
||
{% block menu %} | ||
{% set disable = collector.serializations | length == 0 and collector.deserializations | length == 0 %} |
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.
Even if there is no serialization/deserialization, there could be some normalize and denormalize calls that would be caught by this panel and should make it active.
</div> | ||
<div class="metric"> | ||
<span class="value">{{ collector.denormalizations | length }}</span> | ||
<span class="label">Denormalizations</span> |
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.
We already have these numbers in the tabs below, no need to duplicate here I guess
Closing in favor of #45656 as it's up to date and it addresses the main issues reported here. Thank you for the PR. |
Add a WebDebugToolbar collector for normalize/denormalize actions inside the serializer.
How it works?
debug.normalizer
, as well.instanceof
checks.debug.normalizer
tag.serializer_debug.php
theserializer
services gets decorated with a TraceableSerializer and the SerializerDataCollector gets registered.#SymfonyHackday