serializer icon indicating copy to clipboard operation
serializer copied to clipboard

Setting xsi:type attribute on node

Open simonbowen opened this issue 9 years ago • 6 comments

Hi,

I hope it's ok for me to ask for advice here, I am not sure where to go otherwise. I have a simple object that I am using JMS/Serializer to convert to XML.


class DirParty {

    protected $class = 'entity';
    protected $action = 'create';

    protected $knownAs;
    protected $languageId;
    protected $name;
    protected $nameAlias;
    protected $partyNumber;
    protected $primaryAddressLocation;
    protected $recId;
    protected $recVersion;
    protected $dunsNumberRecId;
    protected $phoneticName;
    protected $orgName;

    protected $dirPartyPostalAddressView = array();
    protected $dirPartyContactInfoView = array();

    public function getAction()
    {
        return $this->action;
    }

    public function setAction($action)
    {
        $this->action = $action;
    }

    public function addDirPartyPostalAddressView(DirPartyPostalAddressView $postalAddressView)
    {
        $this->dirPartyPostalAddressView[] = $postalAddressView;
    }

    public function addDirPartyContactInfoView(DirPartyContactInfoView $contactInfoView)
    {
        $this->dirPartyContactInfoView[] = $contactInfoView;
    }

    public function getKnownAs()
    {
        return $this->knownAs;
    }

    public function setKnownAs($knownAs)
    {
        $this->knownAs = $knownAs;
    }

    public function getLanguageId()
    {
        return $this->languageId;
    }

    public function setLanguageId($languageId)
    {
        $this->languageId = $languageId;
    }

    public function getName()
    {
        return $this->name;
    }

    public function setName($name)
    {
        $this->name = $name;
    }

    public function getNameAlias()
    {
        return $this->nameAlias;
    }

    public function setNameAlias($nameAlias)
    {
        $this->nameAlias = $nameAlias;
    }

    public function getPartyNumber()
    {
        return $this->partyNumber;
    }

    public function setPartyNumber($partyNumber)
    {
        $this->partyNumber = $partyNumber;
    }

    public function getPrimaryAddressLocation()
    {
        return $this->primaryAddressLocation;
    }

    public function setPrimaryAddressLocation($primaryAddressLocation)
    {
        $this->primaryAddressLocation = $primaryAddressLocation;
    }

    public function getRecId()
    {
        return $this->recId;
    }

    public function setRecId($recId)
    {
        $this->recId = $recId;
    }

    public function getRecVersion()
    {
        return $this->recVersion;
    }


    public function setRecVersion($recVersion)
    {
        $this->recVersion = $recVersion;
    }

    public function getDunsNumberRecId()
    {
        return $this->dunsNumberRecId;
    }

    public function setDunsNumberRecId($dunsNumberRecId)
    {
        $this->dunsNumberRecId = $dunsNumberRecId;
    }

    public function getPhoneticName()
    {
        return $this->phoneticName;
    }

    public function setPhoneticName($phoneticName)
    {
        $this->phoneticName = $phoneticName;
    }

    public function getOrgName()
    {
        return $this->orgName;
    }

    public function setOrgName($orgName)
    {
        $this->orgName = $orgName;
    }


} 

I have got this working to this stage

<DirParty xmlns="http://schemas.microsoft.com/dynamics/2008/01/documents/Customer" class="entity" action="create">
            <KnownAs xsi:nil="true"/>
            <LanguageId xsi:nil="true"/>
            <Name><![CDATA[PHP Unit]]></Name>
            <NameAlias xsi:nil="true"/>
            <PartyNumber xsi:nil="true"/>
            <PrimaryAddressLocation xsi:nil="true"/>
            <RecId xsi:nil="true"/>
            <RecVersion xsi:nil="true"/>
            <DunsNumberRecId xsi:nil="true"/>
            <PhoneticName xsi:nil="true"/>
            <OrgName xsi:nil="true"/>
          </DirParty>

This is my config in YML

Nanopore\XML\Objects\DirParty:

  xml_namespaces:
    "": http://schemas.microsoft.com/dynamics/2008/01/documents/Customer


  properties:
    action:
      xml_attribute: true
      xml_value: true
      type: string

    class:
      xml_attribute: true
      xml_value: true
      type: string


    knownAs:
      type: string

    languageId:
      type: integer

    name:
      type: string

    nameAlias:
      type: string

    partyNumber:
      type: string

    primaryAddressLocation:
      type: string

    recId:
      type: string

    recVersion:
      type: string

    dunsNumberRecId:
      type: string

    phoneticName:
      type: string

    orgName:
      type: string

    dirPartyPostalAddressView:
      xml_list:
        inline: true
        entry_name: DirPartyPostalAddressView
      type: array<Nanopore\XML\Objects\DirPartyPostalAddressView>

    dirPartyContactInfoView:
      xml_list:
        inline: true
        entry_name: DirPartyContactInfoView
      type: array<Nanopore\XML\Objects\DirPartyContactInfoView>

However I wish to set an attribute on the DirParty node that looks like xsi:type='AxdEntity_DirParty_DirOrganization'

Could some one point me in the right direction?

Thanks

simonbowen avatar Apr 20 '15 20:04 simonbowen

Hi,

i am facing the same problem with xsi:type. I´ve defined all the namespaces but I`ve got no clue how to add the attribute to the node.

Any news on this?

Patrick

patrickse avatar Jul 06 '15 08:07 patrickse

Hi Patrick,

I didn't find the solution, unfortunately I ended up doing some str_replace and preg_replace on the generated XML to achieve this, dirty and hacky, but I needed to get it out of the door.

Simon

simonbowen avatar Jul 07 '15 09:07 simonbowen

I´ve found a solution based on the Attributes-Annotation. I will send you the sample on Friday.. out of office at the moment. The trick is to define namespaces and add an attribute with the name XSI:TYPE to the object. You can´t do it with normal annotation but if you´re using the array annotation for attributes... everything is fine.

Maybe not the best solution but it works for me...

patrickse avatar Jul 08 '15 06:07 patrickse

Sorry... I was sick for a few days...

So here´s my solution

XSIStringField.php

<?php

namespace domain;

use JMS\Serializer\Annotation\Type;
use JMS\Serializer\Annotation\XmlRoot;
use JMS\Serializer\Annotation\XmlValue;
use JMS\Serializer\Annotation\XmlElement;
use JMS\Serializer\Annotation\XmlNamespace;
use JMS\Serializer\Annotation\SerializedName;
use JMS\Serializer\Annotation\XmlAttributeMap;

/**
 * @XmlNamespace(uri="http://www.w3.org/2001/XMLSchema-instance", prefix="xsi")
 * @XmlNamespace(uri="http://www.w3.org/2001/XMLSchema", prefix="xs")
 */
class XSIStringField {

    public function __construct($value) {
        $this->value = $value;
    }

    /**
     * @Type("string")
     * @XmlAttributeMap
     */
    public $attrib = ['xsi:type' => 'xs:string'];

    /**
     * @Type("string")
     * @SerializedName("key")
     * @XmlValue
    */
    public $value;

}

GenericPluginEntry.php

<?php

namespace domain;

use JMS\Serializer\Annotation\Type;
use JMS\Serializer\Annotation\XmlRoot;
use JMS\Serializer\Annotation\XmlValue;
use JMS\Serializer\Annotation\XmlElement;
use JMS\Serializer\Annotation\XmlNamespace;
use JMS\Serializer\Annotation\SerializedName;
use JMS\Serializer\Annotation\XmlAttributeMap;

class GenericPluginEntry {

    public function __construct($key, $value) {
        $this->key = new XSIStringField($key);
        $this->value = new XSIStringField($value);
    }

    /**
     * @Type("domain\XSIStringField")
     * @SerializedName("key")
     */
    public $key;

    /**
     * @Type("domain\XSIStringField")
     * @SerializedName("value")
     */
    public $value;

}

This allows me to set the XSI:Type stuff on my key/value pairs and result into something like that...

<entry>
          <key xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string"><![CDATA[Blup]]></key>
          <value xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string"><![CDATA[Bla]]></value>
        </entry>

Hope that it will help someone in the future :-)

patrickse avatar Jul 13 '15 09:07 patrickse

I implemented this for myself as listener.

This is currently most likely POC but maybe you want to adapt the approach (and maybe even include it in the serializer meta data):

services.yml

services:
    jms_xsi_type_handling_listener:
        class: Application\EventListener\JmsXsiTypeHandlingListener
        arguments:
            # Provide any xsi:type override here.
            - { namespace: http://example.org/some/namespace,  type: Type1,     class: PhpNamespace\Type1Class }
            - { namespace: http://example.org/some/namespace,  type: Type2,     class: PhpNamespace\Type2Class }
<?php

namespace Application\EventListener;

use DOMElement;
use JMS\Serializer\EventDispatcher\ObjectEvent;
use JMS\Serializer\EventDispatcher\PreDeserializeEvent;
use JMS\Serializer\XmlSerializationVisitor;
use SimpleXMLElement;

/**
 * This class implements XML schema instance type handling.
 *
 * When serializing and deserializing, one can override classes with other types.
 */
class JmsXsiTypeHandlingListener
{
    /**
     * The XML schema type indicator overrides.
     *
     * @var array
     */
    private $override = [];

    /**
     * The XML schema type indicator reverse overrides.
     *
     * @var array
     */
    private $reverse = [];

    /**
     * The XMLSchema instance namespace.
     */
    const URI_XSI = 'http://www.w3.org/2001/XMLSchema-instance';

    /**
     * The XML namespace namespace.
     */
    const URI_XMLNS = 'http://www.w3.org/2000/xmlns/';

    /**
     * Create a new instance.
     *
     * You may pass as many override parameters as desired.
     *
     * @param array $override The type(s) to override.
     */
    public function __construct(array $override = [])
    {
        foreach (func_get_args() as $override) {
            $this->addOverride($override['namespace'], $override['type'], $override['class']);
        }
    }

    /**
     * Add a schema override.
     *
     * @param string $namespace The namespace the type name is located in.
     * @param string $typeName  The type name to change.
     * @param string $newClass  The new class name.
     *
     * @return void
     */
    public function addOverride(string $namespace, string $typeName, string $newClass)
    {
        if (!isset($this->override[$namespace])) {
            $this->override[$namespace] = [];
        }
        $this->override[$namespace][$typeName] = $newClass;
        $this->reverse[$newClass]              = [$namespace, $typeName];
    }

    /**
     * Add the xsi:type attribute to the element if needed.
     *
     * @param ObjectEvent $event The event being dispatched.
     *
     * @return void
     */
    public function postSerialize(ObjectEvent $event)
    {
        $visitor = $event->getVisitor();

        // We only handle XML serializing in here.
        if (!($visitor instanceof XmlSerializationVisitor)) {
            return;
        }

        if ($override = ($this->reverse[$event->getType()['name']] ?? null)) {
            /** @var DOMElement $element */
            $element = $visitor->getCurrentNode();

            // Obtain prefix or add it if not defined yet.
            $prefix = $this->lookupPrefixOrAdd($element, $override[0]);

            $element->setAttributeNS(self::URI_XSI, 'type', $prefix . ':' . $override[1]);
        }
    }

    /**
     * Change destination class if there is a schema type declared and we can handle it.
     *
     * @param PreDeserializeEvent $event The event being dispatched.
     *
     * @return void
     */
    public function preDeSerialize(PreDeserializeEvent $event)
    {
        /** @var SimpleXMLElement $data */
        $data       = $event->getData();
        $attributes = $data->attributes(self::URI_XSI);

        if (isset($attributes['type'])) {
            $override = $this->tryTypeOverride($attributes['type'], $data);
            $event->setType($override, $event->getType()['params']);
        }
    }

    /**
     * Lookup the namespace prefix or add it to the document if not found.
     *
     * @param DOMElement $element   The XML element to add the prefix for.
     * @param string     $namespace The namespace to look up.
     *
     * @return string
     */
    private function lookupPrefixOrAdd(DOMElement $element, $namespace)
    {
        // Obtain prefix or add it if not defined yet.
        if ($prefix = ($element->lookupPrefix($namespace))) {
            return $prefix;
        }
        $element->ownerDocument->documentElement->setAttributeNS(
            self::URI_XMLNS,
            'xmlns:ns-' . crc32($namespace),
            $namespace
        );

        return $element->ownerDocument->documentElement->lookupPrefix($namespace);
    }

    /**
     * Try to override the type.
     *
     * @param string           $typeName The type name.
     * @param SimpleXMLElement $element  The element.
     *
     * @return string
     *
     * @throws \RuntimeException When an unknown XML schema type is encountered.
     */
    private function tryTypeOverride(string $typeName, SimpleXMLElement $element)
    {
        if (false === strpos($typeName, ':')) {
            // Try to find a xsi:schemaLocation in parents.
            $override = $this->tryOverrideViaRootNamespace($typeName, $element);
        } else {
            $override = $this->tryOverrideViaNamespacePrefix($typeName, $element);
        }

        if (!$override) {
            throw new \RuntimeException('Invalid XML schema type: ' . $typeName);
        }

        return $override;
    }

    /**
     * Try to find a schema location in the parent elements and return the correct type then.
     *
     * @param string           $type    The type name.
     * @param SimpleXMLElement $element The element to start from.
     *
     * @return string|null
     */
    private function tryOverrideViaRootNamespace(string $type, SimpleXMLElement $element)
    {
        $namespaces = $element->getNamespaces(true);
        // We have a proper root NS, use it.
        if (isset($namespaces[''])) {
            return ($this->override[$namespaces['']][$type] ?? null);
        }

        return null;
    }

    /**
     * Try to find a schema location in the parent elements and return the correct type then.
     *
     * @param string           $typeName The type name.
     * @param SimpleXMLElement $element  The element to start from.
     *
     * @return string|null
     */
    private function tryOverrideViaNamespacePrefix(string $typeName, SimpleXMLElement $element)
    {
        $xsiType = explode(':', $typeName, 2);

        return ($this->override[$element->getDocNamespaces(true)[$xsiType[0]]][$xsiType[1]] ?? null);
    }
}

This way there is no need to define any virtual property or real property.

discordier avatar Aug 23 '17 07:08 discordier

@discordier Thanks for the elaborate example, I've used a similar approach in my project.

rvdbogerd avatar Mar 28 '19 15:03 rvdbogerd