Symfony Serializer десериализует XML, который содержит элемент с переменным количеством подэлементов

При десериализации XML-файла для объекта с использованием компонента Symfony Serializer возникает проблема, когда элемент в XML может содержать несколько элементов, но в этом случае содержит только один.

XML содержит несколько элементов, которые могут содержать ноль или более подэлементов. Например, "офисы" могут содержать ноль или более элементов "офис". Я представил пример кода, который упрощает проблему, с которой я столкнулся в реальности. См. Особенности вопроса ниже.

Пример кода:

sample.xml

<?xml version="1.0"?>
<buildings>
    <restaurants>
        <restaurant>
            <name>Some restaurant name</name>
            <type>Chinese</type>
        </restaurant>
        <restaurant>
            <name>Another restaurant name</name>
            <type>Italian</type>
        </restaurant>
    </restaurants>
    <offices>
        <office>
            <company>Some company</company>
            <owner>John</owner>
        </office>
    </offices>
</buildings>

serialize.php

<?php

use App\Entities\Buildings;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
use Symfony\Component\Serializer\Serializer;

require 'vendor/autoload.php';

$xml = file_get_contents(__DIR__ . '/sample.xml');

$serializer = new Serializer(
    [
        new ArrayDenormalizer(),
        new ObjectNormalizer(null, null, null, new PhpDocExtractor())
    ],
    [new XmlEncoder()]
);

$data = $serializer->deserialize($xml, Buildings::class, 'xml');

var_dump($data);

Buildings.php

<?php

namespace App\Entities;

class Buildings
{
    /**
     * @var Restaurants
     */
    private $restaurants;

    /**
     * @var Offices
     */
    private $offices;

    /**
     * @return Restaurants
     */
    public function getRestaurants(): Restaurants
    {
        return $this->restaurants;
    }

    /**
     * @param Restaurants $restaurants
     */
    public function setRestaurants(Restaurants $restaurants): void
    {
        $this->restaurants = $restaurants;
    }

    /**
     * @return Offices
     */
    public function getOffices(): Offices
    {
        return $this->offices;
    }

    /**
     * @param Offices $offices
     */
    public function setOffices(Offices $offices): void
    {
        $this->offices = $offices;
    }
}

Restaurants.php

<?php

namespace App\Entities;    

class Restaurants
{
    /**
     * @var Restaurant[]
     */
    private $restaurant;

    /**
     * @return Restaurant[]
     */
    public function getRestaurant(): array
    {
        return $this->restaurant;
    }

    /**
     * @param Restaurant[] $restaurant
     */
    public function setRestaurant(array $restaurant): void
    {
        $this->restaurant = $restaurant;
    }
}

Restaurant.php

<?php

namespace App\Entities;

class Restaurant
{
    /**
     * @var string
     */
    private $name;

    /**
     * @var string
     */
    private $type;

    /**
     * @return string
     */
    public function getName(): string
    {
        return $this->name;
    }

    /**
     * @param string $name
     */
    public function setName(string $name): void
    {
        $this->name = $name;
    }

    /**
     * @return string
     */
    public function getType(): string
    {
        return $this->type;
    }

    /**
     * @param string $type
     */
    public function setType(string $type): void
    {
        $this->type = $type;
    }
}

Offices.php

<?php

namespace App\Entities;

class Offices
{
    /**
     * @var Office[]
     */
    private $office;

    /**
     * @return Office[]
     */
    public function getOffice()
    {
        return $this->office;
    }

    /**
     * @param Office[] $office
     */
    public function setOffice($office): void
    {
        $this->office = $office;
    }
}

Office.php

<?php

namespace App\Entities;

class Office
{
    /**
     * @var string
     */
    private $company;

    /**
     * @var string
     */
    private $owner;

    /**
     * @return string
     */
    public function getCompany(): string
    {
        return $this->company;
    }

    /**
     * @param string $company
     */
    public function setCompany(string $company): void
    {
        $this->company = $company;
    }

    /**
     * @return string
     */
    public function getOwner(): string
    {
        return $this->owner;
    }

    /**
     * @param string $owner
     */
    public function setOwner(string $owner): void
    {
        $this->owner = $owner;
    }
}

При запуске serialize.php с sample.xml, как это предусмотрено, возникает следующая ошибка:

Fatal error: Uncaught Symfony\Component\Serializer\Exception\NotNormalizableValueException: The type of the key "company" must be "int" ("string" given). in /path/to/project/vendor/symfony/serializer/Normalizer/ArrayDenormalizer.php:57
Stack trace:
#0 /path/to/project/vendor/symfony/serializer/Serializer.php(172): Symfony\Component\Serializer\Normalizer\ArrayDenormalizer->denormalize(Array, 'App\\Entities\\Of...', 'xml', Array)
#1 /path/to/project/vendor/symfony/serializer/Normalizer/AbstractObjectNormalizer.php(271): Symfony\Component\Serializer\Serializer->denormalize(Array, 'App\\Entities\\Of...', 'xml', Array)
#2 /path/to/project/vendor/symfony/serializer/Normalizer/AbstractObjectNormalizer.php(202): Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer->validateAndDenormalize('App\\Entities\\Of...', 'office', Array, 'xml', Array)
#3 /path/to/project/vendor/symfony/serializer/Serializer.ph in /path/to/project/vendor/symfony/serializer/Normalizer/ArrayDenormalizer.php on line 57
PHP Fatal error:  Uncaught Symfony\Component\Serializer\Exception\NotNormalizableValueException: The type of the key "company" must be "int" ("string" given). in /path/to/project/vendor/symfony/serializer/Normalizer/ArrayDenormalizer.php:57
Stack trace:
#0 /path/to/project/vendor/symfony/serializer/Serializer.php(172): Symfony\Component\Serializer\Normalizer\ArrayDenormalizer->denormalize(Array, 'App\\Entities\\Of...', 'xml', Array)
#1 /path/to/project/vendor/symfony/serializer/Normalizer/AbstractObjectNormalizer.php(271): Symfony\Component\Serializer\Serializer->denormalize(Array, 'App\\Entities\\Of...', 'xml', Array)
#2 /path/to/project/vendor/symfony/serializer/Normalizer/AbstractObjectNormalizer.php(202): Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer->validateAndDenormalize('App\\Entities\\Of...', 'office', Array, 'xml', Array)
#3 /path/to/project/vendor/symfony/serializer/Serializer.ph in /path/to/project/vendor/symfony/serializer/Normalizer/ArrayDenormalizer.php on line 57

Process finished with exit code 255

Эта ошибка возникает из-за того, что в ArrayDenormalizer элемент Office в элементе Offices преобразуется в один элемент, а не в массив элементов. И поскольку PHPDoc указывает, что setOffice() в Offices.php ожидает массив Office, он ожидает, что значения ключа будут целыми числами.

Массив ArrayDenormalizer преобразует xml в:

array(2) {
  [0]=>
  array(2) {
    ["name"]=>
    string(20) "Some restaurant name"
    ["type"]=>
    string(7) "Chinese"
  }
  [1]=>
  array(2) {
    ["name"]=>
    string(23) "Another restaurant name"
    ["type"]=>
    string(7) "Italian"
  }
}
array(2) {
  ["company"]=>
  string(12) "Some company"
  ["owner"]=>
  string(4) "John"
}

Я ожидаю, что PHPDocExtractor и ObjectNormalizer поймут, что, хотя в массиве компаний есть только один элемент, он все равно должен быть массивом объектов, потому что я объявил это таким образом в PHPDoc в Offices.php.

Если я изменю PHPDoc в Offices.php на Office, а не Office[], я получу один экземпляр Office, который работает нормально. Если я добавлю еще один элемент office в XML, два элемента Office преобразуются в объекты Office и добавляются в офисы в массиве, как и ожидалось. Это также работает для ресторанов.

Конечно, ни одно из этих решений не является решением моей проблемы, поскольку это допустимый сценарий, когда в элементе офисов присутствует только один элемент office. То же самое касается ресторанов. Если я удалю один элемент Restaurant из элемента Restaurants в файле sample.xml, произойдет та же ошибка.

Я пытался использовать различные комбинации нормализаторов и экстракторов, таких как ReflectionExtractor, но ни одна из них не достигла того, что мне нужно, что, на мой взгляд, не является необычной ситуацией.

Как я могу убедиться, что Symfony Serializer знает, что независимо от того, является ли он нулем, одним или несколькими элементами в родительском элементе, я всегда ожидаю, что массив объектов будет добавлен в свойство Office в моем объекте Offices.

0 ответов

Другие вопросы по тегам