0

I'm migrating the Zend\Db driven DBAL of a Zend Framework 3 application to Doctrine. Everything is working fine, but now I got a problem with the export of data.

Before the migration it was working as follows:

There is a more or less complex data structure. The Mapper executed some database requests and built a nested DataObject from this data. So, the start point for the export was an object, filled with all data and having sub-objects, also with all their data. So I simply converted it to JSON:

public function exportToJson(AbstractDataObject $dataObject)
{
    return json_encode($dataObject, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
}

public function exportToXml(AbstractDataObject $dataObject)
{
    $dataObjectVars = json_decode(json_encode($dataObject->jsonSerialize()), true);
    $xml = new \SimpleXMLElement('<' . self::XML_DEFAULT_ROOT_ELEMENT . ' />');
    $this->arrayToXml($dataObjectVars, $xml);
    $domxml = new \DOMDocument('1.0');
    $domxml->preserveWhiteSpace = false;
    $domxml->formatOutput = true;
    $domxml->loadXML($xml->asXML());
    $xmlString = $domxml->saveXML();
    return $xmlString;
}

protected function arrayToXml($array, &$xml)
{
    foreach ($array as $key => $value) {
        if(is_array($value)){
            if(is_int($key)){
                $key = self::XML_DEFAULT_ELEMENT_NAME;
            }
            $label = $xml->addChild($key);
            $this->arrayToXml($value, $label);
        }
        else {
            $xml->addChild($key, $value);
        }
    }
}

All DataObjects extended the AbstractDataObject and it provided a method, that made it easily exportable to JSON:

class AbstractDataObject implements \JsonSerializable
{

    public function jsonSerialize()
    {
        $reflection = new \ReflectionClass($this);
        $properties = $reflection->getProperties();
        $members = [];
        foreach ($properties as $property) {
            $property->setAccessible(true);
            $members[$property->getName()] = $property->getValue($this);
        }
        $keys = array_keys($members);
        $values = array_values($members);
        $keysUnderscored = preg_replace_callback('/([A-Z])/', function($matches) {
            return '_' . strtolower($matches[1]);
        }, $keys);
        $varsUnderscored = array_combine($keysUnderscored, $values);
        return $varsUnderscored;
    }

}

Now the object to export is an entity and it usually doesn't not have all its data loaded. That means, the approach described above doesn't work anymore.

Is there / What is a proper way to convert a nested entity (means an entity with its sub-entities) to a structured data format (array / JSON / XML)?

automatix
  • 14,018
  • 26
  • 105
  • 230
  • 2
    You can load the entity by using a query and specifying all the related entities in the select clause. This will basically override the lazy loading. Or you could adjust the serializer to call getPropertyName() instead of accessing the properties directly. Finally, you could take a look at the Symfony serializer component which is perhaps the "official" way of doing these sorts of things. And now that I think about, it you don't really need an object then $query->getArrayResult() might be all you need. – Cerad Sep 07 '17 at 00:04
  • @Cerad Thanks a lot for your comment! So you suggest tree ways: `1.` A query with all dependencies. -- It would be a large one, I would like to avoid this. `2.` To "adjust the serializer to call getPropertyName() instead of accessing the properties directly". *What do you mean?* `3.` Symfony serializer component. I will take a look. Thanks for the advice! `4.` `$query->getArrayResult()` -- It would be a really elegant solution, but it doesn't work for a nested structure. – automatix Sep 07 '17 at 13:51
  • Consider updating your question with a simple example of an entity with missing data. I think you are having trouble with lazy loading but I'm not sure. – Cerad Sep 07 '17 at 14:14
  • Following up on @Cerad comment, you could make a [custom hydration mode](http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/dql-doctrine-query-language.html#custom-hydration-modes) that would return an array with all the nested objects – William Perron Sep 07 '17 at 20:10

1 Answers1

0

Finally I've got it working as suggested in Cerad's comment with the Symfony Serializer Component.

I got some troubles with the encoding: The JSON_ERROR_UTF8 for JSON and the "Warning: DOMDocument::saveXML(): invalid character value" for XML. So I had to "utf8ize" the array data received from the Serializer and reimplement the exportToXml(...) by using the dom_import_simplexml(...) as shown here. But now it's working, here we go:

namespace MyNamespace\DataExport;

use MyNamespace\DataObject\AbstractDataObject;
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use Doctrine\Common\Annotations\AnnotationReader;
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;

class DataExporter
{

    /** @var string */
    const EXPORT_FORMAT_JSON = 'json';
    /** @var string */
    const EXPORT_FORMAT_XML = 'xml';
    /** @var string */
    const XML_DEFAULT_ROOT_ELEMENT = 'my_root_element_name';
    /** @var string */
    const XML_DEFAULT_ELEMENT_NAME = 'item';

    /** @var Serializer */
    protected $serializer;

    public function __construct()
    {
        $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
        $normalizer = new ObjectNormalizer($classMetadataFactory, new CamelCaseToSnakeCaseNameConverter());
        $normalizer->setCircularReferenceLimit(1);
        $normalizer->setIgnoredAttributes(['__initializer__', '__cloner__', '__isInitialized__']);
        $normalizer->setCircularReferenceHandler(function ($object) {
            // @todo A cleaner solution need.
            try {
                $return = $object->getId();
            } catch (\Error $exception) {
                $return = null;
            }
            $return = null;
            return $return;
        });
        $normalizers = [$normalizer];
        $this->serializer = new Serializer($normalizers);
    }

    public function exportToJson(AbstractDataObject $dataObject)
    {
        $data = $this->serializer->normalize($dataObject, null, ['groups' => ['export']]);
        $data = $this->utf8ize($data);
        return json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
    }

    public function exportToXml(AbstractDataObject $dataObject)
    {
        $data = $this->serializer->normalize($dataObject, null, ['groups' => ['export']]);
        $data = $this->utf8ize($data);
        $xml = new \SimpleXMLElement('<' . self::XML_DEFAULT_ROOT_ELEMENT . ' />');
        $this->arrayToXml($data, $xml);
        $domDocument = dom_import_simplexml($xml)->ownerDocument;
        $domDocument->formatOutput = true;
        $xmlString = $domDocument->saveXML();
        return $xmlString;
    }

    protected function utf8ize($data) {
        if (is_array($data)) {
            foreach ($data as $key => $value) {
                $data[$key] = $this->utf8ize($value);
            }
        } else if (is_string ($data)) {
            return utf8_encode($data);
        }
        return $data;
    }

    /**
     * Converts an $array to XML and
     * saves the result to the $xml argument.
     *
     * @param array $array
     * @param \SimpleXMLElement $xml
     * @return void
     */
    protected function arrayToXml($array, &$xml){
        foreach ($array as $key => $value) {
            if(is_array($value)){
                if(is_int($key)){
                    $key = self::XML_DEFAULT_ELEMENT_NAME;
                }
                $label = $xml->addChild($key);
                $this->arrayToXml($value, $label);
            }
            else {
                $xml->addChild($key, $value);
            }
        }
    }

}
automatix
  • 14,018
  • 26
  • 105
  • 230
  • 2
    You might also want to look at [JMS Serializer](https://jmsyst.com/libs/serializer). It willl serialize to json or xml. – gview Sep 07 '17 at 19:56
  • Seems to be a good tool and "integrates with Doctrine ORM". Thank you for the advice! – automatix Sep 07 '17 at 20:02
  • Yes, have done multiple REST api projects using it. Great tool, and I think was the inspiration for the symfony component. Works nicely with Annotations as well. – gview Sep 07 '17 at 20:21