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

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>

Related

Doctrine return null in place of EntityNotFoundException

I have broken FKs in my database and if I load an entity and ask for a related entity Doctrine will throw \Doctrine\ORM\EntityNotFoundException.
For the entity in question, I would prefer that where the FK is broken it would return NULL rather than throw an exception. This is because its within a Twig template that the exception occurs and I would prefer Twig to not have to have to handle the exception in this case.
The following is an example configuration.
<?xml version="1.0" encoding="utf-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="Foo\Click" table="clicks">
<id name="id" type="bigint" column="click_id">
<generator strategy="IDENTITY"/>
</id>
<!-- .. -->
<many-to-one field="visitor" target-entity="Foo\Visitor" fetch="LAZY">
<join-columns>
<join-column name="visitor_id" referenced-column-name="visitor_id"/>
</join-columns>
</many-to-one>
</entity>
<entity name="Foo\Visitor" table="visitors" read-only="true">
<id name="visitorId" type="integer" column="visitor_id">
<generator strategy="IDENTITY"/>
</id>
<!-- ... -->
<one-to-one field="firstClick" target-entity="Foo\Click" fetch="LAZY">
<join-columns>
<join-column name="click_id" referenced-column-name="click_id"/>
</join-columns>
</one-to-one>
</entity>
</doctrine-mapping>
The following is an example of expected results where the the click as a visitor ID, but the a visitor record does not exists with that ID. In this case, I would rather not have to wrap the logic in Try/Catch and instead have Click::getVisitor() return null;
<?php
$clickOne = $entityManager()->find(Foo\Click::class, 1);
$v = $clickOne->getVisitor();
if ($v !== null) {
echo $v->getId(); // may throw Doctrine\ORM\EntityNotFoundException
}
Is there a strategy for this with Doctrine?
Update: Added example configuration and code, and now I see the why this is not achievable with a simple Doctrine configuration.
EntityNotFoundException is thrown from Doctrine's proxy. So you can use EAGER loading method to get rid of proxies and get NULL instead of exception.
This is the strategy I have adopted based on the comment made by iainn.
<?php
class Parent
{
protected $child;
public function getChild()
{
if ($this->child instance of \Doctrine\ORM\Proxy\Proxy) {
try {
$this->child->__load();
} catch (\Doctrine\ORM\EntityNotFoundException $e) {
$this->child = null
}
}
return $this->child;
}
}
I am sure it is not recommended for your entities to interact directly with proxies. But I preferred this over calling a known method on the entity (which would also have the effect of loading it) because the intention of this code is clearer to the next developer who might read it.
I'm not sure if there are any side effects with interacting with the proxy like this.

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.

Pear XML Serializer and Attributes

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

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