Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Custom normalizer causes infinite recursion #53708

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

Closed
emi87290 opened this issue Jan 31, 2024 · 2 comments
Closed

Custom normalizer causes infinite recursion #53708

emi87290 opened this issue Jan 31, 2024 · 2 comments

Comments

@emi87290
Copy link

Symfony version(s) affected

7.0.3

Description

When I create a basic custom normalizer to add a data to the normalization process I run into an infinite recursion.
My worked on 6.4 but without the NormalizerAwareInterface, since I ran in 7.0.3 I needed it or the serializer would not be instancied.
Anyways, I took the documentation and try it without success:

Maximum call stack size of 8339456 bytes (zend.max_allowed_stack_size - zend.reserved_stack_size) reached. Infinite recursion?

If I look in the profiler, the recursion loop on the line $data = $this->normalizer->normalize($topic, $format, $context); from the documentation like code

How to reproduce

Create a custom serializer near to the documentation (adding url to the process)

<?php

namespace App\Serializer;

use App\Entity\Category;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;

class CategoryNormalizer implements NormalizerInterface, NormalizerAwareInterface
{
    use NormalizerAwareTrait;

    public function __construct(private readonly UrlGeneratorInterface $router) {}

    public function normalize($category, string $format = null, array $context = []): array
    {
        $data = $this->normalizer->normalize($category, $format, $context);

        if (isset($context['groups']) && in_array('admin:category:list', $context['groups'])) {
            $data['link'] = [];

            $data['link']['edit'] = $this->router->generate('app_admin_photo_shoot_category_edit', [
                'id' => $category->getId()
            ]);

            $data['link']['delete'] = $this->router->generate('app_admin_photo_shoot_category_delete', [
                'id' => $category->getId()
            ]);
        }

        return $data;
    }

    public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool
    {
        return $data instanceof Category;
    }

    public function getSupportedTypes(?string $format): array
    {
        return [
            Category::class => true,
        ];
    }
}

And a controller that serve the data:

#[Route('/data/categories', name: '_api_categories')]
    public function getCategories(CategoryRepository $categoryRepository): JsonResponse
    {
        $categories = $categoryRepository->findBy([], ['label' => Criteria::ASC]);

        return $this->json($categories, context: ['groups' => ['admin:category:list']]);
    }

Possible Solution

No response

Additional Context

No response

@mtarld
Copy link
Contributor

mtarld commented Feb 8, 2024

Indeed, the documentation leads to an infinite recursion. I created a PR aiming to fix it.
To solve your precise use case, you can do the following:

<?php

namespace App\Serializer;

use App\Entity\Category;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;

class CategoryNormalizer implements NormalizerInterface
{
    public function __construct(
        #[Autowire(service: 'serializer.normalizer.object')]
        private readonly NormalizerInterface $normalizer,
        private readonly UrlGeneratorInterface $router,
    ) {}

    public function normalize($category, string $format = null, array $context = []): array
    {
        $data = $this->normalizer->normalize($category, $format, $context);

        if (isset($context['groups']) && in_array('admin:category:list', $context['groups'])) {
            $data['link'] = [];

            $data['link']['edit'] = $this->router->generate('app_admin_photo_shoot_category_edit', [
                'id' => $category->getId()
            ]);

            $data['link']['delete'] = $this->router->generate('app_admin_photo_shoot_category_delete', [
                'id' => $category->getId()
            ]);
        }

        return $data;
    }

    public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool
    {
        return $data instanceof Category;
    }

    public function getSupportedTypes(?string $format): array
    {
        return [
            Category::class => true,
        ];
    }
}

javiereguiluz added a commit to symfony/symfony-docs that referenced this issue Feb 8, 2024
This PR was merged into the 7.0 branch.

Discussion
----------

[Serializer] Fix recursive custom normalizer

As mentioned in the following issue: symfony/symfony#53708, the example showing how to create a custom normalizer leads to an infinite recursion.

I could have been fixed like that:
```diff
class TopicNormalizer implements NormalizerInterface, NormalizerAwareInterface
{
    use NormalizerAwareTrait;

+    private const ALREADY_CALLED = self::class.'_already_called';
+
    public function __construct(
        private UrlGeneratorInterface $router,
    ) {
    }

    public function normalize($topic, string $format = null, array $context = []): array
    {
+       $context[self::ALREADY_CALLED] = true;
+
        $data = $this->normalizer->normalize($topic, $format, $context);

        // Here, add, edit, or delete some data:
        $data['href']['self'] = $this->router->generate('topic_show', [
            'id' => $topic->getId(),
        ], UrlGeneratorInterface::ABSOLUTE_URL);

        return $data;
    }

    public function supportsNormalization($data, string $format = null, array $context = []): bool
    {
+        if ($context[self::ALREADY_CALLED] ?? false) {
+            return false;
+        }
+
        return $data instanceof Topic;
    }

    public function getSupportedTypes(?string $format): array
    {
        return [
-             Topic::class => true,
+             Topic::class => false,
        ];
    }
}
```

But this will prevent the normalizer to be cacheable (because it depends on the context).

Instead, I dropped the use of `NormalizerAwareInterface` and `NormalizerAwareTrait` and used an explicit constructor injection instead.

WDYT?

Commits
-------

56c8b4d [Serializer] Fix recursive custom normalizer
@mtarld
Copy link
Contributor

mtarld commented Feb 13, 2024

I think that this issue can be closed 🙂

@xabbuh xabbuh closed this as completed Feb 13, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants