Skip to content
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

Setting xsi:type attribute on node #433

Open
simonbowen opened this issue Apr 20, 2015 · 6 comments
Open

Setting xsi:type attribute on node #433

simonbowen opened this issue Apr 20, 2015 · 6 comments

Comments

@simonbowen
Copy link

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

@patrickse
Copy link

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

@simonbowen
Copy link
Author

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

@patrickse
Copy link

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
Copy link

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 :-)

@discordier
Copy link
Contributor

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.

@rvdbogerd
Copy link

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

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

5 participants