Pear XML Serializer and Attributes - php

is there a way, to "tell" the PEAR XML_Serializer, which properties it should serialize as attribute and which as sub element?
For example:
class User {
public $id;
public $name;
public $address;
}
Should be serialized like this:
<User id="0">
<name>John Doe</name>
<address></address>
</User>
I thought about using the "XML_SERIALIZER_OPTION_SCALAR_AS_ATTRIBUTES" Option, but unfortunately, I need some scalars as attribute and some as sub element.
Is there a way to tell the XML_Serializer how he should serialize the properties of the source class?

Done some code review and got the solution:
$serializer->setOption(
XML_SERIALIZER_OPTION_SCALAR_AS_ATTRIBUTES => array(
"User" => array("id")
)
);
Does the trick ... everything will be serialized as XML-Element but the "id" property of the User Element will be serialized as Attribute

Related

Array-Properties with the SeralizedPath-Attribute can't be serialized by Serializer

I have the problem that Properties with SerializedPath attributes, which I convert to an XML via Symfony Serializer (SerializerInterface, symfony/serializer-pack), does not have an expected behavior with arrays. Instead, the AbstractObjectNormalizer throws an exception. Symfony and related packages are using v6.2.5.
My goal is to create an XML like this one, with e.g. the following Object:
<?xml version="1.0"?>
<response>
<payment>
<allowedMethods>
<include name="Method A"/>
<include name="Method B"/>
</allowedMethods>
</payment>
</response>
// This is not working
class Payment
{
#[SerializedPath('[payment][allowedMethods][include][#name]')]
public array $methods = ['Method A', 'Method B'];
}
// ...
$this->serializer->serialize(new Payment(), 'xml');
But it results to the following Exception: The element you are trying to set is already populated: "[payment][allowedMethods][include][#name]".
which is thrown in the AbstractObjectNormalizer of Symfony:
if (null !== $classMetadata && null !== $serializedPath = ($attributesMetadata[$attribute] ?? null)?->getSerializedPath()) {
$propertyAccessor = PropertyAccess::createPropertyAccessor();
if ($propertyAccessor->isReadable($data, $serializedPath) && null !== $propertyAccessor->getValue($data, $serializedPath)) {
throw new LogicException(sprintf('The element you are trying to set is already populated: "%s".', (string) $serializedPath));
}
$propertyAccessor->setValue($data, $serializedPath, $attributeValue);
return $data;
}
My expected behavior would be that it behaves identically to the scalar values and therefore recognizes the # as an XML attribute and the rest as nodes:
// This is working great!
class Payment
{
#[SerializedPath('[payment][usedMethod][#name]')]
public string $usedMethod = 'Method A';
}
<?xml version="1.0"?>
<response>
<payment>
<usedMethod name="Method A"/>
</payment>
</response>
I've been trying to identify the exact problem and find a solution for some time, but since SerializedPath is very new (since Symfony 6.2), there is no great documentation for it. Internally it uses the PropertyAccessor, but where I could not find a suitable solution either.
Can someone maybe explain me the problem in more detail or even know a solution? Or even an alternative way. I'm trying to do it via the SerializedPath to avoid all the nested DTOs that are just there to map the (not always plausible) data model of the XML.
Another quite interesting fact is, if you comment out the thrown Exception in the AbstractObjectNormalizer it kind of works (for json fully, for xml partially, because the array can only hold one entry because of the annotation key):
class Payment
{
#[SerializedPath('[payment][allowedMethods][include]')]
public array $methods = [
'#name' => 'Method A'
];
}
<?xml version="1.0"?>
<response>
<payment>
<allowedMethods>
<include name="Method A"/>
</allowedMethods>
</payment>
</response>

Change Namespace of Childern Nodes SimpleXMLElement

<ret2:formFields xsi:type="ret1:FormFieldsType" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
I want to know how can I do the following? The namespace of children are getting changed by setting the xsi:type attribute.
My Code:
$ret2FormFields = $ret2FileBody->addChild('ret2:formFields', null, 'http://www.w3.org/2001/XMLSchema-instance');
$ret2FormFields->addAttribute("xsi:type", "ret1:FormFieldsType", 'http://www.w3.org/2001/XMLSchema-instance');
$ret2FormFields->addChild('ret1:isReverseReplace', false);
$ret2FormFields->addChild('ret1:payDayDate', '2018-04-10'); /** #todo date will be dynamic */
Expected XML:
<ret2:formFields xsi:type="ret1:FormFieldsType" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<ret1:isReverseReplace>false</ret1:isReverseReplace>
<ret1:payDayDate>2018-04-10</ret1:payDayDate>
</ret2:formFields>
My Incorrect XML:
<ret2:formFields xmlns:ret2="http://www.w3.org/2001/XMLSchema-instance" ret2:type="ret1:FormFieldsType">
<ret1:isReverseReplace>false</ret1:isReverseReplace>
<ret1:payDayDate>2018-04-10</ret1:payDayDate>
</ret2:formFields>
I am stuck how to change the children namespaces ret1 without changing the parent namespace ret2
You are passing the wrong namespace to addChild here:
addChild('ret2:formFields', null, 'http://www.w3.org/2001/XMLSchema-instance');
This tells SimpleXML that you want the element to have prefix ret2 and namespace URI http://www.w3.org/2001/XMLSchema-instance, so it will generate:
<ret2:formFields xmlns:ret2="http://www.w3.org/2001/XMLSchema-instance">
You don't show in your example what the prefix ret2 has been assigned elsewhere in the document, but that's what you need to provide, e.g.:
addChild('ret2:formFields', null, 'http://example.org/mynamespaces/ret2');

PHP native, ignores namespace in soapvar

I recently created a php web service using php native soap. I have created the wsdl, xsd and the php code to construct the response.
In my soapvar when I construct the soap arrayObject using the namespace prefix, some nodes have it and some don't.
What I want is all the nodes have the "ns1:" prefix or none of them.
In order to overcome the issue I removed the namespace from soapvar. So this removed the ns prefix But I always have the message from my wsdl "retrieveDataResponse" node with "ns1:" prefix and and all the rest I constructed without.
In my php I have nested foreach run in every node and children adding "XSD_STRING" or "SOAP_ENC_OBJECT" depending on the enc_type.
My soapvar in php in the foreach is :
$dataStruct[] = new SoapVar($ListOfDataStruct, SOAP_ENC_OBJECT, null, null, 'ListOfData', 'http://localhost/soap/retrieveCstData');
My XML response is
<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://localhost/soap/retrieveCstData">
<SOAP-ENV:Body>
<ns1:retrieveDataResponse>
<ns1:cstData>
<ns1:Description>Discription N/A</ns1:Description>
<ns1:ListOfData>
<ns1:Customer-Data-Header>
<ns1:AssetDescription>Basic 12 Months</ns1:AssetDescription>
<ns1:AssetId>1-3QGMHQ9</ns1:AssetId>
<ns1:ProductDescription>Basic 12 Months</ns1:ProductDescription>
<ns1:ProductId>1-2E543A</ns1:ProductId>
<ns1:ProductName>Basic Product Subscription</ns1:ProductName>
<ns1:ListOfCstData-Asset>
<ns1:CstData-Asset>
<AssetIntegrationId>1-3Q3KSNI</AssetIntegrationId>
<ProductName>Basic Product Subscription</ProductName>
<ProductPartNumber>SAT0028</ProductPartNumber>
<StartDate>08/19/2015 21:00:00</StartDate>
<Status>Active</Status>
<ListOfProductXA/>
<ListOfAddress/>
<ListOfContact/>
</ns1:CstData-Asset>
</ns1:ListOfCstData-Asset>
</ns1:Customer-Data-Header>
<ns1:Customer-Data-Header>
<ns1:AssetId>1-7MRO-241</ns1:AssetId>
<ns1:ProductDescription>SubProduct A</ns1:ProductDescription>
<ns1:ProductId>1-65TVM</ns1:ProductId>
<ns1:ProductName>SubProduct A Type</ns1:ProductName>
<ns1:ProductType>Product</ns1:ProductType>
<ns1:ListOfCstData-Asset>
<ns1:CstData-Asset>
<AssetIntegrationId>1-5T126KG</AssetIntegrationId>
<ProductName>Asset-Product 1</ProductName>
<ProductPartNumber>N/A</ProductPartNumber>
<StartDate>08/16/2016 21:00:00</StartDate>
<Status>Active</Status>
<ListOfProductXA/>
<ListOfAddress/>
<ListOfContact/>
</ns1:CstData-Asset>
<ns1:CstData-Asset>
<AssetIntegrationId>W-C5PLG-11H-1</AssetIntegrationId>
<ProductName>SubProduct A Type</ProductName>
<ProductPartNumber>Data Packets</ProductPartNumber>
<RegisteredDate>02/21/1978</RegisteredDate>
<ServiceID>#56487%</ServiceID>
<StartDate>02/21/1978 00:00:00</StartDate>
<ListOfProductXA/>
<ListOfAddress>
<CutAddress>
<AddressType>Installation</AddressType>
<TEK>1651</TEK>
<Type>Old</Type>
<Country>US</Country>
<StreetNumberFrom>37</StreetNumberFrom>
<PostalCode>66857</PostalCode>
<State>CA</State>
<StreetName>
<State>JAX Avenue</State>
</StreetName>
</CutAddress>
</ListOfAddress>
<ListOfContact>
<Contact>
<ActiveStatus>Y</ActiveStatus>
<IsPrimaryMVG>Y</IsPrimaryMVG>
<CellularPhone>555687676</CellularPhone>
<FirstName>Jhon</FirstName>
<LastName>Doe</LastName>
<PreferredCommunicationMethod>SMS</PreferredCommunicationMethod>
<ContactType>Technical</ContactType>
</Contact>
</ListOfContact>
</ns1:CstData-Asset>
</ns1:ListOfCstData-Asset>
</ns1:Customer-Data-Header>
<ns1:Customer-Data-Header>
<ns1:AssetDescription>Satelite 80CM</ns1:AssetDescription>
<ns1:AssetId>1-3QGMHX9</ns1:AssetId>
<ns1:ProductDescription>Satelite 80CM</ns1:ProductDescription>
<ns1:ProductId>1-2DIYLT</ns1:ProductId>
<ns1:ProductName>TV SAT</ns1:ProductName>
<ns1:ProductType>Product</ns1:ProductType>
<ns1:ListOfCstData-Asset>
<ns1:CstData-Asset>
<SubscriberId>664668941</SubscriberId>
<Comments>Suspension/Reactivation</Comments>
<AssetIntegrationId>1-3Q3KSNJ</AssetIntegrationId>
<ProductName>TV SAT</ProductName>
<ProductPartNumber>TV_SAT</ProductPartNumber>
<ServiceID>9995654321587</ServiceID>
<StartDate>08/19/2015 21:00:00</StartDate>
<Status>Active</Status>
<ListOfProductXA/>
<ListOfAddress>
<CutAddress>
<AddressType>Installation</AddressType>
<TEK>1651</TEK>
<Type>Old</Type>
<Area>CA</Area>
<Country>US</Country>
<StreetNumberFrom>37</StreetNumberFrom>
<ResidenceType>Business</ResidenceType>
<Floor>0</Floor>
<MailBox>US</MailBox>
<PostalCode>66857</PostalCode>
<State>CA</State>
<StreetName>JAX AVENUE</StreetName>
<District>DownTown</District>
</CutAddress>
</ListOfAddress>
<ListOfContact/>
</ns1:CstData-Asset>
</ns1:ListOfCstData-Asset>
</ns1:Customer-Data-Header>
</ns1:ListOfData>
</ns1:retrieveDataResponse>
</ns1:retrieveDataResponse>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
So as you can see prefix is not in every node.
Well I found the problem.
In one of the many foreach loops I used a previous declared arrayObject instead of the array key.
So soapvar does not ingone the namespace :)

Deserialize nested xml nodes

I've got an xml response from an api formatted as follows:
<?xml version='1.0' encoding='UTF-8'?>
<response success="true">
<messages>
<message type="WARNING" key="warning-unpublished-changes" values="" parentId="1">
You have unpublished changes. Your changes will not be visible every where until it is published.</message>
</messages>
<output>
<accounts>
<account
id="1"
code="AssetsChild"
name="AssetsChild"
description="Total Assets Child"
displayAs="CURRENCY"
accountTypeCode="A"
decimalPrecision="0"
isAssumption="0"
suppressZeroes="1"
isDefaultRoot="1"
shortName=""
exchangeRateType="E"
balanceType="DEBIT"
formula=""
isLinked="0"
owningSheetId=""
isSystem="0"
isIntercompany="0"
dataEntryType=""
planBy="DELTA"
timeRollup="LAST"
timeWeightAcctId=""
levelDimRollup="SUM"
levelDimWeightAcctId=""
rollupText=""
startExpanded="1"
hasSalaryDetail=""
dataPrivacy="PRIVATE"
isBreakbackEligible=""
subType="CUMULATIVE"
enableActuals="1"
isGroup="0"
/>
</accounts>
</output>
</response>
I'd like to have it deserialized to a response object defined as:
class Response
{
protected $success;
protected $messages;
protected $accounts;
}
I've been able to successfully get the success value and message array using the config below. Is it possible to get the hydrate the accounts property with the list of account nodes?
Response\AccountResponse:
xml_root_name: response
properties:
success:
type: boolean
xml_attribute: true
xml_value: false
messages:
type: array<Entity\Message>
xml_list:
entry_name: message
Use SimpleXml. specifically, use simple_xml_load_string to transform the string into a SimpleXmlElement the use the class methods to navigate and extract the data.

Symfony, serialize object with arrays as XML preserving attributes and structure

I am using Symfony2 and trying to serialize different collections of objects into XML. For the sake of brevity, let's assume I am trying to list and unlist entities and this is the XML I want to get as a result:
<?xml version="1.0" encoding="UTF-8"?>
<r someattribute="value">
<data_list>
<item id="9" type="a"><![CDATA[list data 1]></item>
<item id="10" type="a"><![CDATA[list data 2]></item>
<item id="11" type="b"><![CDATA[list data 3]></item>
</data_list>
<data_unlist>
<uitem id="9" type="a" />
</data_unlist>
</r>
Here are my classes: Item for the "item" nodes, Uitem for the "uitem" nodes
and Model, to contain them all:
class Item
{
private $data=array();
public function getData() {return $this->data;}
public function __construct($id, $type, $value)
{
$this->data["#id"]=$id;
$this->data["#type"]=$type;
//How do I put $value as the node value????
}
}
class UItem
{
private $data=array();
public function getData() {return $this->data;}
public function __construct($id, $type)
{
$this->data["#id"]=$id;
$this->data["#type"]=$type;
}
}
class Model
{
private $data_list=array();
private $data_unlist=array();
public function getDataList() {return $this->data_list;}
public function getDataUnlist() {return $this->data_unlist;}
public function __construct()
{
$this->data_list[]=new Item(9, 'a', 'list data 1');
$this->data_list[]=new Item(10, 'a', 'list data 2');
$this->data_list[]=new Item(11, 'b', 'list data 3');
$this->data_unlist[]=new UItem(9, 'a');
}
}
Save for the problem I left commented in the Item class (how to put the node value there) I think that should serialize correctly so...
$model=new Model();
$encoders=array(new XmlEncoder());
$normalizers=array(new GetSetMethodNormalizer());
$serializer=new Serializer($normalizers, $encoders);
$contents_xml=$serializer->serialize($model, 'xml');
This is the result I am getting:
<response>
<data_list>
<item id="9" type="a" />
</data_list>
<data_list>
<item id="11" type="b" />
</data_list>
<data_unlist>
<uitem id="9" type="a" />
</data_unlist>
</response>
As you can see, two separate nodes for "data_list" have been created instead of grouping them into one single node.
Here are my questions:
Can I put the two "item" into a single "data_list"?. If so, how?.
How can I specify the value of a item node (instead of its attributes only) preserving the desired structure?.
How do I alter the root node name and add attributes to it?.
For the record, I am using the vanilla serializer, no JMS here.
Thanks in advance.
Using Symfony XmlEncoder I am able to build an array and turn it into xml using the encode method. Any array items which are named as #something are turned into attributes of the wrapping element rather than child elements, so:
$encoder = new \Symfony\Component\Serializer\Encoder\XmlEncoder();
$data = [
'something' => [
'#foo' => 'bar',
'yoo' => 'bar',
]
];
$xml = $encoder->encode($data, 'xml');
Gives me:
<?xml version="1.0"?>
<response>
<something foo="bar">
<yoo>bar</yoo>
</something>
</response>
JMSSerializer was created for these purposes. And I think you should start to use it in your project. It has no overhead and is easy to use.
You can control your serializing options with annotations:
http://jmsyst.com/libs/serializer/master/reference/annotations

Categories