8000 [Serializer] PHP Warning - undefined array key in AbstractObjectNormalizer::isMaxDepthReached · Issue #54378 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content
[Serializer] PHP Warning - undefined array key in AbstractObjectNormalizer::isMaxDepthReached #54378
Closed
@jaydiablo

Description

@jaydiablo

Symfony version(s) affected

6.4.5

Description

A bundle we use (rompetompe/inertia-bundle) calls the Symfony serializer on any data that's passed to it (that will be converted to JSON for use by Inertia.js), after upgrading to Symfony 6 we're seeing PHP warnings in some situations. It seems to only happen when there is a stdClass object in the data that's passed to the serializer (from a json_decode call somewhere else in the app).

The line that causes the warning is this one:

if (!($enableMaxDepth = $context[self::ENABLE_MAX_DEPTH] ?? $this->defaultContext[self::ENABLE_MAX_DEPTH] ?? false)
            || null === $maxDepth = $attributesMetadata[$attribute]?->getMaxDepth()
        ) {
            return false;
        }

It happens because $attributesMetadata is an empty array in this context and $attribute is a string (which is one of the properties of the stdClass object it's trying to normalize). Earlier in the normalizer there is this chunk of code:

$class = ($this->objectClassResolver)($object);
$classMetadata = $this->classMetadataFactory?->getMetadataFor($class);
$attributesMetadata = $this->classMetadataFactory?->getMetadataFor($class)->getAttributesMetadata();

I mention this, because there is a check a little bit below this to see if $attributesMetadata is null and then it bypasses the isMaxDepthReached call:

foreach ($attributes as $attribute) {
            $maxDepthReached = false;
            if (null !== $attributesMetadata && ($maxDepthReached = $this->isMaxDepthReached($attributesMetadata, $class, $attribute, $context)) && !$maxDepthHandler) {
                continue;
            }

In the application $attributesMetadata is an empty array, but in an isolated test case that I've tried to put together, $attributesMetadata ends up being null so I can't reproduce the warning in an isolated test case.

Perhaps there's something else to this (why does stdClass have attributes?) in the context of the app, but it does seem like there should be a check if the $attribute is set on $attributesMetadata in the isMaxDepthReached method to avoid this warning.

Aside from the Warning being thrown, the serialization/normalization works as expected.

How to reproduce

I put this test case together to try to reproduce in the simplest way possible but it doesn't throw the warning as mentioned above:

<?php

ini_set('display_errors', 1);
ini_set('error_reporting', E_ALL);

use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;

include_once 'vendor/autoload.php';

$object = new stdClass();
$object->string = 'yes';

$serializer = new Symfony\Component\Serializer\Serializer([
    new Symfony\Component\Serializer\Normalizer\UnwrappingDenormalizer(),
    new Symfony\Component\Serializer\Normalizer\ProblemNormalizer(),
    new Symfony\Component\Serializer\Normalizer\UidNormalizer(),
    new Symfony\Component\Serializer\Normalizer\DateTimeNormalizer(),
    new Symfony\Component\Serializer\Normalizer\DateTimeZoneNormalizer(),
    new Symfony\Component\Serializer\Normalizer\DateIntervalNormalizer(),
    new Symfony\Component\Serializer\Normalizer\FormErrorNormalizer(),
    new Symfony\Component\Serializer\Normalizer\BackedEnumNormalizer(),
    new Symfony\Component\Serializer\Normalizer\DataUriNormalizer(),
    new Symfony\Component\Serializer\Normalizer\JsonSerializableNormalizer(),
    new Symfony\Component\Serializer\Normalizer\ArrayDenormalizer(),
    new Symfony\Component\Serializer\Normalizer\ObjectNormalizer(),
], [
    new Symfony\Component\Serializer\Encoder\XmlEncoder(),
    new Symfony\Component\Serializer\Encoder\JsonEncoder(),
    new Symfony\Component\Serializer\Encoder\YamlEncoder(),
    new Symfony\Component\Serializer\Encoder\CsvEncoder(),
]);

$page = [
    'props' => [
        'string' => 'yes',
        'array' => ['string' => 'yes'],
        'object' => $object,
        'nested_object' => [
            'string' => 'yes',
            'array' => ['string' => 'yes'],
            'object' => $object,
        ],
    ]
];

$context = [];

$json = $serializer->serialize($page, 'json', array_merge([
    'json_encode_options' => JsonResponse::DEFAULT_ENCODING_OPTIONS,
    AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER => function () { return null; },
    AbstractObjectNormalizer::PRESERVE_EMPTY_OBJECTS => true,
    AbstractObjectNormalizer::ENABLE_MAX_DEPTH => true,
], $context));

var_dump($json);

I don't know how to make this test case set attributes on the stdClass class like it's doing in the app, but I've tried to replicate how the serializer is configured as best as possible (as far as I can tell, the inertia-bundle just uses @serializer for the serializer dependency, which we're not modifying at all, so should be the Symfony default Serializer config).

Possible Solution

This could probably be fixed in a couple of ways, first, if $attributesMetadata is an empty array, it seems like it should probably be treated the same as if it were null.

In the normalize method of AbstractObjectNormalizer this check:

foreach ($attributes as $attribute) {
            $maxDepthReached = false;
            if (null !== $attributesMetadata && ($maxDepthReached = $this->isMaxDepthReached($attributesMetadata, $class, $attribute, $context)) && !$maxDepthHandler) {
                continue;
            }

could be changed to also check the count of the metadata variable so that the isMaxDepthReached method isn't called with an empty array:

foreach ($attributes as $attribute) {
            $maxDepthReached = false;
            if (null !== $attributesMetadata && count($attributesMetadata) > 0 && ($maxDepthReached = $this->isMaxDepthReached($attributesMetadata, $class, $attribute, $context)) && !$maxDepthHandler) {
                continue;
            }

This does fix it in our application.

Alternatively, the isMaxDepthReached method could be modified in a similar way to check if the $attribute is a key on the passed in $attributesMetadata.

Additional Context

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      0