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
Related
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>
This question is a followup.
I have two entities DataSet and DataGroup.
class DataSet {
string $name;
Collection $groups; // Collection<int, DataGroup>
}
class DataGroup {
string $name;
DataSet $dataSet;
?DataGroup $nextGroup; // Condition: $nextGroup !== $this && $nextGroup->dataSet === $this->dataSet
}
The property DataGroup::nextGroup may refer to any other DataGroup entity associated with the same DataSet.
I want to create a CRUD form where I can add, edit and remove DataSet entities. In this DataSet form, I also want to include a tab where I can CRUD DataGroup entities associated with the current DataSet.
I have created list metadata data_sets.xml and data_groups.xml, as well as form metadata data_set.xml and data_group.xml.
<!-- lists/data_sets.xml -->
<list xmlns="http://schemas.sulu.io/list-builder/list">
<key>data_sets</key>
<properties>
<property name="name" visibility="always" searchability="yes">
<field-name>name</field-name>
<entity-name>App\Entity\DataSet</entity-name>
</property>
</properties>
</list>
<!-- lists/data_groups.xml -->
<list xmlns="http://schemas.sulu.io/list-builder/list">
<key>data_groups</key>
<properties>
<property name="name" visibility="always" searchability="yes">
<field-name>name</field-name>
<entity-name>App\Entity\DataSet</entity-name>
</property>
<property name="dataSet" visibility="always">
<field-name>name</field-name>
<entity-name>App\Entity\DataSet</entity-name>
<joins>
<join>
<entity-name>App\Entity\DataSet</entity-name>
<field-name>App\Entity\DataGroup.dataSet</field-name>
</join>
</joins>
</property>
</properties>
</list>
<!-- forms/data_set.xml -->
<form xmlns="http://schemas.sulu.io/template/template"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://schemas.sulu.io/template/template
http://schemas.sulu.io/template/form-1.0.xsd"
>
<key>data_set</key>
<properties>
<property name="name" type="text_line" mandatory="true">
<params>
<param name="headline" value="true"/>
</params>
</property>
</properties>
</form>
<!-- forms/data_group.xml -->
<form xmlns="http://schemas.sulu.io/template/template"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://schemas.sulu.io/template/template
http://schemas.sulu.io/template/form-1.0.xsd"
>
<key>data_group</key>
<properties>
<property name="name" type="text_line" mandatory="true">
<params>
<param name="headline" value="true"/>
</params>
</property>
<property name="nextGroup" type="single_data_group_selection">
<params>
<param name="type" value="auto_complete"/>
<param name="resource_store_properties_to_request" type="collection">
<param name="dataSetId" value="id"/>
</param>
</params>
</property>
</properties>
</form>
I have also configured sulu_admin.yaml for REST routes and the custom single selection:
sulu_admin:
resources:
data_sets:
routes:
list: app.get_data_sets
detail: app.get_data_set
data_groups:
routes:
list: app.get_data_groups
details: app.get_data_group
field_type_options:
single_selection:
single_data_group_selection:
default_type: 'auto_complete'
resource_key: 'data_groups'
types:
auto_complete:
display_property: 'name'
search_properties:
- 'name'
I implemented two REST controllers for both entities. DataSetController is built as of the Docs, while DataGroupController has a small extension for the list route:
class DataGroupController implements ClassResourceInterface
{
public function cgetAction(int $dataSetId, Request $request): Response
{
// ... Init field descriptors and execute listBuilder
$list = new ListRepresentation(
$listResponse,
'data_groups',
\array_merge(['dataSetId' => $dataSetId], $request->query->all()), // add DataSet ID
$listBuilder->getCurrentPage(),
$listBuilder->getLimit(),
$listBuilder->count()
);
// ... handle view
}
// ...
}
Finally, I implemented a custom Admin class like explained in the Docs.
class DataAdmin extends Admin
{
public const DATA_SET_DETAILS = 'data_set_details';
public const DATA_SET_GROUPS = 'data_set_groups';
public const DATA_SET_LIST = 'app.data_sets_list';
public const DATA_SET_ADD_FORM = 'app.data_set_add_form';
public const DATA_SET_ADD_FORM_DETAILS = 'app.data_set_add_form.details';
public const DATA_SET_EDIT_FORM = 'app.data_set_edit_form';
public const DATA_SET_EDIT_FORM_DETAILS = 'app.data_set_edit_form.details';
public const DATA_SET_EDIT_FORM_GROUPS = 'app.data_set_edit_form.groups';
public function configureViews(ViewCollection $viewCollection): void
{
// Add DataSet list view
// Add DataSet add form view
// Add DataSet edit form view (details)
/*
* Custom second DataSet edit form tab for DataGroup CRUDding
*/
$groupsFormOverlayList = $this->viewBuilderFactory
->createFormOverlayListViewBuilder(self::DATA_SET_EDIT_FORM_GROUPS, '/groups')
->setResourceKey(self::DATA_SET_GROUPS)
->setListKey(self::DATA_SET_GROUPS)
->addListAdapters(['table'])
->addRouterAttributesToListRequest(['id' => 'dataSetId'])
->setFormKey(self::DATA_SET_GROUPS)
->addRouterAttributesToFormRequest(['id' => 'dataSetId'])
->setTabTitle('app.data_groups')
->addToolbarActions([
new ToolbarAction('sulu_admin.add'),
new ToolbarAction('sulu_admin.delete')
])
;
$viewCollection->add($groupsFormOverlayList->setParent(self::DATA_SET_EDIT_FORM));
}
}
As of now
I can perform all CRUD operations on DataSet entities.
I can list and remove DataGroup entities belonging to a DataSet.
When trying to add a new DataGroup, I can't perform autocomplete search for nextGroup because the dataSetId parameter is not passed to the field, forbidding the ResourceRequester to perform the REST request.
How do I pass dataSetId to fields of the "New DataGroup" form in order to search for matching entities only?
Thanks a lot for the detailed description! Unfortunately, I am afraid that it is not possible to implement what you are trying to do while using the built-in autocomplete component at the moment.
The resource_store_properties_to_request param reads the values from the data of the form that renders the autocomplete component. If you are creating a new DataGroup entity, the data of the form is empty (because there is now existing data for a new entity) and therefore the resource_store_properties_to_request param is not able to read a dataSetId value.
I am sorry to say, but I think you would need to implement a custom autocomplete field-type that reads the dataSetId value from the current url to achieve the desired functionality. If you are interested in doing this, I would recommend to have a look at the basic field-type example in the sulu-demo repository.
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.
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
The Question: How do I insert values from a database table (#__mytable) into form text fields (motitle and modescription) which have been rendered from an XML file within the Joomla 3.0 platform?
-
I've been trying for days to solve this "easy" Joomla! based undocumented challenge.
I have followed Joomla!'s guide for Developing an MVC, read most of their out-of-date documentation and torn apart the com_content component but have still no idea how to populate my fields.
I've been playing with $this->form->bind($this->item);.
Below I have included some of my code to show the structure I am using. Please feel free to point out any issues you spot along the way.
Models\Forms\item.xml
<?xml version="1.0" encoding="UTF-8"?>
<form>
<fields name="groupOPTIONS">
<fieldset name="Options">
<field
type="text"
name="motitle"
id="motitle"
label="Title"
description="MY TEXT FIELD DESCRIPTION"
maxLength="255" />
<field
type="textarea"
name="modescription"
id="modescription"
label="Description"
description="MY TEXT FIELD DESCRIPTION"
rows="15"
cols="5"
maxLength="255" />
</fieldset>
</fields>
</form>
Models\item.php
jimport('joomla.application.component.modelitem');
class MagicObjectsModelItem extends JModelForm {
public function getForm($data = array(), $loadData = true) {
// Get the form 'items'
$form = $this->loadForm('com_magicobjects.item', 'item',
array('control' => 'jform', 'load_data' => $loadData));
if (empty($form)) {
return false;
}
return $form;
}
protected function loadFormData() {
// Check the session for previously entered form data.
$data = JFactory::getApplication()->getUserState('com_magicobjects.item.edit.data', array());
if (empty($data)) {
$data = $this->getDBItem(1);
}
return $data;
}
public function getDBItem($pk) {
//Obtain JDatabase static connection
$oDb = JFactory::getDbo();
$oQuery = $oDb->getQuery(true);
$sValueToMatch = $pk;
$oQuery
->select(array('mid', 'name', 'keyword', 'description'))
->from('#__mytable')
->where('mid = "' . $sValueToMatch . '"')
->order('mid ASC');
$oDb->setQuery($oQuery);
return $oDb->loadObjectList();
}
views\item\view.html.php
jimport('joomla.application.component.view');
function display($tpl = null) {
// Initialise variables.
$this->form = $this->get('Form');
$this->item = $this->get('Item');
//Display the view
parent::display($tpl);
}
Views\item\tmpl\default.php
foreach ($this->form->getFieldset('Options') as $field) {
echo $field->label;
echo $field->input;
}
By performing a print_r() on item I can see I have the data, but I need to insert the data into the fields shown.
In case this it's still a problem because the form definition is defining a <fields /> group.
This mean that the form fields will be output as follows:
input type="text" name="[groupOPTIONS][motitle]"
based on the example you give above. If you remove the fields grouping it will probably work. It's annoying though as certain JForm methods only work on groups...
You would probably need to change the way in which the data was being passed into the form if you wanted to keep the fields grouping (e.g. by overloading getItem).
Hope that makes sense...
In Joomla 3, the JForm::bind() method appears to accept either an object or associative array as a parameter. All of the object/array fields are then stored in a protected JRegistry type data member called JForm::$data.
When you're trying to display a form field (by calling JForm::getInput()), the call stack is as follows
JForm::getInput() -> JForm::getField() -> JForm::loadField() -> JForm::getValue()
JForm::getValue() returns a value from the (aforementioned JRegistry) JForm::$data data member. The JForm::loadField() method also passes the default value (defined by the form) to the JForm::getValue() method in case a value in the JForm::$data variable doesn't exist.
In terms of doing this inside a model, you might want to generate a object or assoc array from a database query or table (ensuring that the field names correspond with the field names defined in the form xml) and then pass it to JForm::bind() as a parameter. However, if you are using JModelForm, I think you should only override JModelForm::loadFormData() to pass the object/assoc array to the loaded form.
See : libraries/joomla/form/form.php
Hope that helps :)
Instead of $oDb->loadObjectList(); use $oDb->loadAssoc(); in your getDBItem() function.
Check this - http://docs.joomla.org/Accessing_the_database_using_JDatabase/1.5
Check this also - joomla loadformdata
Hope this will work.