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

Skip to content

Allow arrays being denormalized in AbstractObjectNormalizer #19545

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
hensoko opened this issue Aug 5, 2016 · 12 comments
Closed

Allow arrays being denormalized in AbstractObjectNormalizer #19545

hensoko opened this issue Aug 5, 2016 · 12 comments

Comments

@hensoko
Copy link

hensoko commented Aug 5, 2016

Heyho,

I currently work on a problem when using the ObjectNormalizer with an multidimensional array as input. My array looks like this:

[
    'base_property1' => 'value123',
    'nested_resources' => [
        [
            'nested_property' => 'value1234'
        ],
        [
            'nested_property' => 'value2345'
        ],
    ]
]

I defined the following classes the data of this array should be passed to:

class Resource {
    /**
     * @var string
     */
    private $baseProperty1;

    /**
     * @var nestedResource[]|ArrayCollection
     */
    private $nestedResources;

    public function getBaseProperty1() {
        return $this->baseProperty1;
    }

    public function setBaseProperty1($baseProperty1) {
        $this->baseProperty1 = $baseProperty1;
        return $this;
    }

    public function getNestedResources() {
        return $this->nestedResources;
    }

    public function setNestedResources($nestedResources) {
        $this->nestedResources = $nestedResources;
        return $this;
    }
}

class NestedResource {
     /**
     * @var string
     */
    private $nestedProperty;

    public function getNestedProperty() {
        return $this->nestedProperty;
    }

    public function setNestedProperty($nestedProperty) {
        $this->nestedProperty = $nestedProperty;
        return $this;
    }
}

When using the Serializer-Component of Symfony to denormalize the input-array I expected to get the following result:

   Resource {
        -baseProperty1: "value123"
        -nestedResources: array:2 [
            0 => NestedResource {
                -nestedProperty: 'value1234'
            }
            2 => NestedResource {
                -nestedProperty: 'value2345'
            }
        ]
   }

but instead I get the following:

   Resource {
        -baseProperty1: "value123"
        -nestedResources: array:2 [
            0 => array [
                nestedProperty => 'value1234'
            }
            1 => array [
                nestedProperty => 'value2345'
            }
        ]
   }

I defined the following services to get a serializer-service using the ReflectionExtrator:

services:
    name_converter.camel_case_to_sneak_case_name:
        class: Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter

    property_accessor.reflection:
        class: Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor

    denormalizer.array:
        class: Symfony\Component\Serializer\Normalizer\ArrayDenormalizer

    normalizer.object:
        class: Symfony\Component\Serializer\Normalizer\ObjectNormalizer
        arguments:
            -
            - "@name_converter.camel_case_to_sneak_case_name"
            -
            - "@property_accessor.reflection"

    encoder.json:
        class: Symfony\Component\Serializer\Encoder\JsonEncoder

    serializer:
        class: Symfony\Component\Serializer\Serializer
        arguments:
            - ["@normalizer.object", "@denormalizer.array"]
            - ["@encoder.json"]

I tested it with latest Symfony 3.1.3. I think the issue occures because of a missing check for a collection in class AbstractObjectNormalizer in line 255 to 263. It is only checked for an object, but if $type is an collection this is not honored. I get a correct result if I add the following check just before line 255, but I think this is not a nice solution because of the need to add brackets after the class-name:

if ($type->isCollection() && $type->getCollectionValueType()->getBuiltinType() === Type::BUILTIN_TYPE_OBJECT) {
    $class = $type->getCollectionValueType()->getClassName() . '[]';
    $builtinType = $type->getCollectionValueType()->getBuiltinType();
}

I get the same result with PhpDocExtractor.

@theofidry
Copy link
Contributor

Has been solved in #19277 but sadly will only be available in 3.2 unless you accept to use dev-master

@javiereguiluz
Copy link
Member

@theofidry in your opinion, should this be considered a bug or a new feature? Thanks.

@theofidry
Copy link
Contributor

Well it never really worked and has never been documented either so I'm not sure we can consider it being a bug. That said it's a change with a limited impact and not having it can be a serious limitation, so it could be passed as a bug and backported to be released in a patch version rather than waiting months for the 3.2.

But I think @dunglas is more qualified than me to make a decision on this :P

@hensoko
Copy link
Author

hensoko commented Aug 8, 2016

Actually I'm not able to test it, because the profiler of 3.2-dev let's Chrome and Firefox eat up all my memory... But it would be nice to have this feature in 3.1 branch aswell...

@hensoko
Copy link
Author

hensoko commented Aug 8, 2016

Ok, just got it working, but I think the fix in #19277 does not fit my case.
My property should get an array passed with instances of a perticular class and not just one instance.

@theofidry
Copy link
Contributor

theofidry commented Aug 8, 2016

What does it give you instead of an array of two nested resources?

@hensoko
Copy link
Author

hensoko commented Aug 8, 2016

Something like this:

  -premises: array:4 [▼
    0 => array:35 [▶]
    1 => array:35 [▶]
    2 => array:35 [▶]
    3 => array:35 [▶]
  ]

But what I expected was this:

  -premises: array:4 [▼
    0 => Premise {▶}
    1 => Premise {▶}
    2 => Premise {▶}
    3 => Premise {▶}
  ]

When I do a dump of $type-variable in AbstractObjectNormalizer Line 255 I get the following result:

Type {#877 ▼
  -builtinType: "array"
  -nullable: false
  -class: null
  -collection: true
  -collectionKeyType: Type {#875 ▼
    -builtinType: "int"
    -nullable: false
    -class: null
    -collection: false
    -collectionKeyType: null
    -collectionValueType: null
  }
  -collectionValueType: Type {#876 ▼
    -builtinType: "object"
    -nullable: false
    -class: "AppBundle\Resource\Premise"
    -collection: false
    -collectionKeyType: null
    -collectionValueType: null
  }
}

So I think the class-type is correctly figured-out by the property type extractor but data being an array of classes is not handled properlt.

@theofidry
Copy link
Contributor

I'll take a look at this when I'll have a bit of time. In the meantime I'm afraid a custom normalizer will be the solution.

@hensoko
Copy link
Author

hensoko commented Aug 8, 2016

Ok, thanks alot :)

@dunglas
Copy link
Member

dunglas commented Aug 17, 2016

Your solution is good @hensoko. Adding brackets after the class name for arrays is how the serializer works since #14756.

@hensoko
Copy link
Author

hensoko commented Aug 17, 2016

I created a custom ObjectNormalizer that now checks explicitly for an array. I realized that $type->getCollectionValueType() returns null if the given type is a collection but not of a specific type so my fix from above will raise an PHP error.
Furthermore I now use a loop to iterate through each array-entry because I found it a bit nasty to simply add square-brackets to the class-name but this could be of course the more efficient way to deserialize an array.

if (is_array($data)
    && $builtinType === Type::BUILTIN_TYPE_ARRAY
    && $type->isCollection()
    && $type->getCollectionValueType()
    && $type->getCollectionValueType()->getBuiltinType() === TYPE::BUILTIN_TYPE_OBJECT
)
{
    if (!$this->serializer instanceof DenormalizerInterface) {
        throw new LogicException(sprintf('Cannot denormalize attribute "%s" for class "%s" because injected serializer is not a denormalizer', $attribute, $class));
    }

    $out = array();
    foreach($data as $key => $value) {
        if ($this->serializer->supportsDenormalization($key, $type->getCollectionValueType()->getClassName(), $format)) {
            $out[$key] = $this->serializer->denormalize($value, $type->getCollectionValueType()->getClassName(), $format, $context);
        }
    }

    if (!empty($out)) {
        return $out;
    }
}

vs.

if (is_array($data)
    && $builtinType === Type::BUILTIN_TYPE_ARRAY
    && $type->isCollection()
    && $type->getCollectionValueType()
    && $type->getCollectionValueType()->getBuiltinType() === TYPE::BUILTIN_TYPE_OBJECT
)
{
    if (!$this->serializer instanceof DenormalizerInterface) {
        throw new LogicException(sprintf('Cannot denormalize attribute "%s" for class "%s" because injected serializer is not a denormalizer', $attribute, $class));
    }

    if ($this->serializer->supportsDenormalization($key, $type->getCollectionValueType()->getClassName(), $format)) {
        return $this->serializer->denormalize($value, $type->getCollectionValueType()->getClassName() . '[]', $format, $context);
    }
}

@dunglas
Copy link
Member

dunglas commented Aug 17, 2016

I'll open a PR soon to fix this (it is very similar to your last example).

fabpot added a commit that referenced this issue Aug 19, 2016
This PR was squashed before being merged into the 3.1 branch (closes #19649).

Discussion
----------

[Serializer] Fix denormalization of arrays

| Q             | A
| ------------- | ---
| Branch?       | 3.1
| Bug fix?      | yes
| New feature?  | no
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #19545
| License       | MIT
| Doc PR        | n/a

ping @hensoko @theofidry

Commits
-------

99c582b [Serializer] Fix denormalization of arrays
@fabpot fabpot closed this as completed Aug 19, 2016
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

6 participants