serializer
serializer copied to clipboard
Setting xsi:type attribute on node
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
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
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
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...
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 :-)
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 Thanks for the elaborate example, I've used a similar approach in my project.