Issue
I am developing a web application that should handle XML data, and I'm facing some problems when I try to edit data that I have previously saved.
This is What I'm doing from the beginning: The user has to fill a form with several inputs, I grab that data in my controller, convert it into an XML string with Xml::fromArray and store it in a single database field. This works perfectly and the generated XML string is exactly what I need. This is (part of) the form.
echo $this->Form->input('data.FT.Header.Transmission.Number');
echo $this->Form->input('data.FT.Header.Transmission.Format');
echo $this->Form->input('data.FT.Header.Transmission.Destination');
Now I am trying to make add an edit function for that data. From my controller I grab that data, convert it back to an object with Xml::build and... this is where the problem starts.
If I do this
debug($invoice->data->FT->Header->Transmission);
everything looks fine as it tells me that I have an object containing this:
object(SimpleXMLElement) {
Number => 'Npslu'
Format => 'Rgytz'
Destination => 'Uheac'
}
But, if I try to see what $invoice->data->FT->Header->Transmission->Number contains, instead of a string I get an empty result. Which is why (I think) all the form inputs are empty.
I had a look to the official documentation, tried to convert it to arrays (which should be the old way cakephp handled data) and everything that came to my mind... still no results.
What am I doing wrong?
UPDATE: I managed to create a working object this (dirty) way:
$array = Xml::toArray( Xml::build( $xmlString ) );
$object = json_decode(json_encode($array), FALSE);
Now, if I debug $invoice->data->FT->Header->Transmission I have the correct value, which is a huge step forward in my situation :)
The problem is that the Form helper still don't recognize that value. If I have this in my edit template
echo $this->Form->input('data.FT.Header.Transmission.Number');
then the field is empty. If I specify its value this way
echo $this->Form->input('data.FT.Header.Transmission.Number', array('value' => $invoice->data->FT->Header->Transmission->Number);
it works correctly as it should. I know I could specify every value but I'd like to have it automatically, as it does with other fields (non XML related) that I have in that page, like $invoice->sent which is correctly recognized by $this->Form->input('sent')...
UPDATE 2: this is the XML generated code:
<?xml version="1.0" encoding="UTF-8"?>
<FT>
<Header>
<Transmission>
<Number>1234</Number>
<Format>qwer</Format>
<Destination>asdf</Destination>
</Transmission>
</Header>
</FT>
And these are the functions I'm using to convert the input field's values to an XML and back:
// Values to XML, called from the add function this way:
// $this->request->data['data'] = $this->_arrayToXml($this->request->data['data']);
protected function _arrayToXml($array) {
$xmlObject = Xml::fromArray($array);
$xmlString = $xmlObject->asXML();
return $xmlString;
}
// XML to Values, called from the edit function this way:
// $invoice->data = $this->_xmlToObject($invoice->data);
protected function _xmlToObject($xmlString) {
$array = Xml::toArray( Xml::build( $xmlString ) );
$object = json_decode(json_encode($array), FALSE);
return $object;
}
p.s. please forgive me if you see some minor incongruences, I'm trying to strip all non related code to avoid posting tons of code...
Solution
Empty SimpleXMLElement
objects
The object returned actually isn't empty, Cakes debug()
function just can't handle objects like SimpleXMLElement
yet, where the values aren't explicitly stored in properties. Use var_dump()
or cast the value and you'll see that it's there:
var_dump($invoice->data->FT->Header->Transmission);
debug((string)$invoice->data->FT->Header->Transmission);
Mystery solved.
Values not shown in form inputs
Now why aren't the values appearing in the form inputs? Well, data in forms is accessed via context providers, and since you are passing an entity, the form uses the entity context provider.
The entity provider tries to fetch the data from the entity in case it's not available in the request object, which is the case for an edit form that wasn't yet submitted. The problem here is that the provider cannot handle your data structure, it expects nested entities, not arrays or standard/SimpleXML objects.
The most simple way to achieve what you are trying to do would be to always set the request data, so that the entity context provider doesn't try to fetch the data from the entity, but always grabs it from the request data (see EntityContext::val()
). Something like:
Controller
public function edit($id = null) {
// ...
if ($this->request->is(['post', 'put'])) {
// convert array input data to XML string
$this->request->data['data'] =
Xml::fromArray($this->request->data['data'])->asXML();
$invoice = $this->Invoices->patchEntity($invoice, $this->request->data);
if ($this->Invoices->save($invoice)) {
// ...
}
}
// convert XML string to array
$this->request->data['data'] = XML::toArray(Xml::build($invoice->data));
// ...
$this->set(compact('invoice'));
}
That way the entity is not being touched when obtaining data for the inputs, and everything should work fine.
A possibly cleaner approach
A possibly cleaner, but somewhat more complex approach might be to use a custom data type that converts the data, and a custom context provider that can handle dot notated path access to XML objects. That way there is no need for any special conversion etc in the controller.
However, this isn't really part of the question/problem, so I'm just broaching the subject here by providing an example (not tested thoroughly) with some hints.
XML Data Type
This converts the string retrieved from the database into an XML object which the entity is going to expose, and it marshalls the array request input data into an XML string
namespace App\Database\Type;
use Cake\Database\Driver;
use Cake\Database\Type;
use Cake\Utility\Xml;
class XmlType extends Type {
public function toPHP($value, Driver $driver)
{
if($value === null)
{
return null;
}
return Xml::build($value);
}
public function marshal($value) {
return Xml::fromArray($value)->asXML();
}
}
XML Entity Context
This extracts the data from the XML object in case necessary. The outer parts are copied from the original Entity::val()
method.
namespace App\View\Form;
use Cake\ORM\Entity;
use Cake\View\Form\EntityContext;
class XmlEntityContext extends EntityContext
{
public function val($field) {
$val = $this->_request->data($field);
if ($val !== null) {
return $val;
}
if (empty($this->_context['entity'])) {
return null;
}
$parts = explode('.', $field);
$entity = $this->_getEntity($parts);
// begin: special XML element treatment
if($entity instanceof \SimpleXMLElement)
{
array_shift($parts);
return current($entity->xpath('/' . implode('/', $parts)));
}
// end: special XML element treatment
if (end($parts) === '_ids' && !empty($entity)) {
return $this->_extractMultiple($entity, $parts);
}
if ($entity instanceof Entity) {
return $entity->get(array_pop($parts));
}
return null;
}
}
See
For more information, check
- http://book.cakephp.org/3.0/en/orm/database-basics.html#data-types
- http://book.cakephp.org/3.0/en/orm/database-basics.html#adding-custom-types
- http://book.cakephp.org/3.0/en/views/helpers/form.html#creating-context-classes
Answered By - ndm
0 Comments:
Post a Comment
Note: Only a member of this blog may post a comment.