Deserialize xml to object with Symfony2 - php

I collect some data in xml format through an API and would like to deserialize it in an objects list.
I'm using Symfony2 and find out JMSSerializerBundle but I do not really know how to use it.
I know that Sf2 allows to serialize/deserialize object to/from array, but I'm looking for something more specific.
For example, for this class :
class Screenshot
{
/**
* #var integer $id
*/
private $id;
/**
* #var string $url_screenshot
*/
private $url_screenshot;
public function __construct($id, $url_screenshot) {
$this->id = $id;
$this->url_screenshot = $url_screenshot;
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set url_screenshot
*
* #param string $urlScreenshot
*/
public function setUrlScreenshot($urlScreenshot)
{
$this->url_screenshot = $urlScreenshot;
}
/**
* Get url_screenshot
*
* #return string
*/
public function getUrlScreenshot()
{
return $this->url_screenshot;
}
/**
* Serializes the Screenshot object.
*
* #return string
*/
public function serialize()
{
return serialize(array(
$this->id,
$this->url_screenshot
));
}
/**
* Unserializes the Screenshot object.
*
* #param string $serialized
*/
public function unserialize($serialized)
{
list(
$this->id,
$this->url_screenshot
) = unserialize($serialized);
}
public function __toString() {
return "id: ".$this->id
."screenshot: ".$this->url_screenshot;
}
}
I would like serializing/deserializing to/from this kind of xml :
<?xml version="1.0" encoding="UTF-8" ?>
<screenshots>
<screenshot>
<id>1</id>
<url_screenshot>screenshot_url1</url_screenshot>
</screenshot>
<screenshot>
<id>2</id>
<url_screenshot>screenshot_url2</url_screenshot>
</screenshot>
<screenshot>
<id>3</id>
<url_screenshot>screenshot_url3</url_screenshot>
</screenshot>
</screenshots>
I really want to use something integrated/to integrate in Sf2 (something "smooth") and prefer avoiding any homemade xml parsers.

Due to the nature of XML, the exact thing you want, is not possible. You would always need something to translate object -> xml and xml -> object.
My suggestion to you would be a class that works as collection and takes care of it for you, holding the list of objects as property, which can be created from an xml input and create xml output from existing objects.
An alternativ (if you don't really need to have it as xml anymore) would be to simply serialize the objects and store them that way, or searialize an array (or collection object) if you want them all at once. The plain serialize() and unserialize() functions from PHP will do the trick there. Since it's data only, you don't even need the methods serialize and unserialize in your class.
Update: If it's only take the XML into an object, then simplexml already has you covered:
http://www.php.net/manual/en/function.simplexml-load-string.php
The second parameter is class name.
Quote: You may use this optional parameter so that simplexml_load_string() will return an object of the specified class. That class should extend the SimpleXMLElement class.
If only this is your goal, then simplexml does it already.
Update 2: I've read some more into the bundle. It does NOT do what you want. It takes an object and the serializes it into XML/YAML, and then of course reverses that process again from those serialized states. It cannot take some random XML file and turn that into a perfect object for you.

Related

How to properly typehint SimpleXMLElement?

Is there a way to properly typehint a \SimpleXMLElement? So that I do not have to typehint all what it accesses also a a \SimpleXMLElement?
If I want to have typehinting all the way, I currently have to do it this way:
/**
* #var \SimpleXMLElement $values (this is not! an array, yet it is traversable)
*/
$values = $response->params->param->value->array->data->value;
foreach ($values as $row) {
$row = $row->array->data->value;
/**
* #var \SimpleXMLElement $row
*/
$entry = $row[0];
/**
* #var \SimpleXMLElement $entry
*/
$xmlString = $entry->asXML();
}
This seems utterly verbose and redundant. Is there a way to typehint a SimpleXMLElement so that all what it returns will also be coreclty typehinted?
If you Ctrl-click through to the "definition" of SimpleXMLElement in PHPStorm, you will see that it has a stub class definition which it uses for auto-completion and code analysis.
In older versions of PHPStorm, the overloaded -> operator was represented in that stub as follows (taken from PHPStorm 9.0):
/**
* Provides access to element's children
* #param $name child name
* #return SimpleXMLElement[]
*/
function __get($name) {}
Note that the return type here is SimpleXMLElement[], i.e. "an array of SimpleXMLElement objects". This allows it to correctly auto-complete if you write something like $node->childName[0]->grandChild[0]->asXML(), but not if you use the short-hand form of $node->childName->grandChild->asXML()
This could be classed as a bug in the IDE, and was filed in their public tracker as WI-15760, which is now fixed.
As of PHPStorm 2018.1.2, the stub instead declares the return type of __get() as SimpleXMLElement and also declares implements ArrayAccess with offsetGet() also returning SimpleXMLElement.
/**
* Provides access to element's children
* #access private Method not callable directly, stub exists for typehint only
* #param string $name child name
* #return SimpleXMLElement
*/
private function __get($name) {}
/**
* Class provides access to children by position, and attributes by name
* #access private Method not callable directly, stub exists for typehint only
* #param string|int $offset
* #return SimpleXMLElement Either a named attribute or an element from a list of children
*/
private function offsetGet ($offset) {}
This should correctly auto-complete for both explicit [0] and short-hand cases.
(The #access private is a hack to stop the method showing up in auto-complete results, since you can't actually call $node->__get() or $node->offsetGet() in real PHP code.)

Can symfony serializer deserialize return nested entity of type child entity?

When I deserialize my doctrine entity, the initial object is constructed/initiated correctly, however all child relations are trying to be called as arrays.
The root level object's addChild(ChildEntity $entity) method is being called, but Symfony is throwing an error that addChild is receiving an array and not an instance of ChildEntity.
Does Symfony's own serializer have a way to deserialize nested arrays (child entities) to the entity type?
JMS Serializer handles this by specifying a #Type("ArrayCollection<ChildEntity>") annotation on the property.
I believe the Symfony serializer attempts to be minimal compared to the JMS Serializer, so you might have to implement your own denormalizer for the class. You can see how the section on adding normalizers.
There may be an easier way, but so far with Symfony I am using Discriminator interface annotation and type property for array of Objects. It can also handle multiple types in one array (MongoDB):
namespace App\Model;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
/**
* #DiscriminatorMap(typeProperty="type", mapping={
* "text"="App\Model\BlogContentTextModel",
* "code"="App\Model\BlogContentCodeModel"
* })
*/
interface BlogContentInterface
{
/**
* #return string
*/
public function getType(): string;
}
and parent object will need to define property as interface and get, add, remove methods:
/**
* #var BlogContentInterface[]
*/
protected $contents = [];
/**
* #return BlogContentInterface[]
*/
public function getContents(): array
{
return $this->contents;
}
/**
* #param BlogContentInterface[] $contents
*/
public function setContents($contents): void
{
$this->contents = $contents;
}
/**
* #param BlogContentInterface $content
*/
public function addContent(BlogContentInterface $content): void
{
$this->contents[] = $content;
}
/**
* #param BlogContentInterface $content
*/
public function removeContent(BlogContentInterface $content): void
{
$index = array_search($content, $this->contents);
if ($index !== false) {
unset($this->contents[$index]);
}
}

PHP SimpleXML xpath does not keep the namespaces when returns data

I'm trying to register a namespace , but everytime I use th returned value from xpath , I have to register the same namespace again and again.
<?php
$xml= <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<response>
<extension>
<xyz:form xmlns:xyz="urn:company">
<xyz:formErrorData>
<xyz:field name="field">
<xyz:error>REQUIRED</xyz:error>
<xyz:value>username</xyz:value>
</xyz:field>
</xyz:formErrorData>
</xyz:form>
</extension>
</response>
</epp>
XML;
The parser :
$xmlObject = simplexml_load_string(trim($xml), NULL, NULL);
$xmlObject->registerXPathNamespace('ns','urn:company');
$fields = $xmlObject->xpath("//ns:field");
foreach($fields as $field){
//PHP Warning: SimpleXMLElement::xpath(): Undefined namespace prefix in
//$errors = $field->xpath("//ns:error");
// I have to register the same namespace again so it works
$field->registerXPathNamespace('ns','urn:company');
$errors = $field->xpath("//ns:error"); // no issue
var_dump((string)current($errors));
}
?>
Notice that I had to register the namespace again inside the loop, if I did not I will get the following error :
//PHP Warning: SimpleXMLElement::xpath(): Undefined namespace prefix
in...
Do you have any idea how to keep the registered namespaces in the returned simplexml objects from xpath function.
Yes you're right for your example, not registering the xpath namespace again would create a warning like the following then followed by another warning leading to an empty result:
Warning: SimpleXMLElement::xpath(): Undefined namespace prefix
Warning: SimpleXMLElement::xpath(): xmlXPathEval: evaluation failed
The explanations given in the comments aren't too far off, however they do not offer a good explanation that could help to answer your question.
First of all the documentation is not correct. It's technically not only for the next ::xpath() invocation:
$xmlObject->registerXPathNamespace('ns', 'urn:company');
$fields = $xmlObject->xpath("//ns:field");
$fields = $xmlObject->xpath("//ns:field");
$fields = $xmlObject->xpath("//ns:field");
$fields = $xmlObject->xpath("//ns:field");
This does not give the warning despite it's not only the next, but another further three calls. So the description from the comment is perhaps more fitting that this is related to the object.
One solution would be to extend from SimpleXMLElement and interfere with the namespace registration so that when the xpath query is executed, all result elements could get the namespace prefix registered as well. But that would be much work and won't work when you would access further children of a result.
Additionally you can't assign arrays or objects to store the data within a SimpleXMLElement it would always create new element nodes and then error that objects / arrays are not supported.
One way to circumvent that is to store not inside the SimpleXMLElement but inside the DOM which is accessible via dom_import_simplexml.
So, if you create a DOMXpath you can register namespaces with it. And if you store the instance inside the owner document, you can access the xpath object from any SimpleXMLElement via:
dom_import_simplexml($xml)->ownerDocument-> /** your named field here **/
For this to work, a circular reference is needed. I outlined this in The SimpleXMLElement Magic Wonder World in PHP and an encapsulated variant with easy access could look like:
/**
* Class SimpleXpath
*
* DOMXpath wrapper for SimpleXMLElement
*
* Allows assignment of one DOMXPath instance to the document of a SimpleXMLElement so that all nodes of that
* SimpleXMLElement have access to it.
*
* #link
*/
class SimpleXpath
{
/**
* #var DOMXPath
*/
private $xpath;
/**
* #var SimpleXMLElement
*/
private $xml;
...
/**
* #param SimpleXMLElement $xml
*/
public function __construct(SimpleXMLElement $xml)
{
$doc = dom_import_simplexml($xml)->ownerDocument;
if (!isset($doc->xpath)) {
$doc->xpath = new DOMXPath($doc);
$doc->circref = $doc;
}
$this->xpath = $doc->xpath;
$this->xml = $xml;
}
...
This class constructor takes care that the DOMXPath instance is available and sets the private properties according to the SimpleXMLElement passed in the ctor.
A static creator function allows easy access later:
/**
* #param SimpleXMLElement $xml
*
* #return SimpleXpath
*/
public static function of(SimpleXMLElement $xml)
{
$self = new self($xml);
return $self;
}
The SimpleXpath now always has the xpath object and the simplexml object when instantiated. So it only needs to wrap all the methods DOMXpath has and convert returned nodes back to simplexml to have this compatible. Here is an example on how to convert a DOMNodeList to an array of SimpleXMLElements of the original class which is the behavior of any SimpleXMLElement::xpath() call:
...
/**
* Evaluates the given XPath expression
*
* #param string $expression The XPath expression to execute.
* #param DOMNode $contextnode [optional] <The optional contextnode
*
* #return array
*/
public function query($expression, SimpleXMLElement $contextnode = null)
{
return $this->back($this->xpath->query($expression, dom_import_simplexml($contextnode)));
}
/**
* back to SimpleXML (if applicable)
*
* #param $mixed
*
* #return array
*/
public function back($mixed)
{
if (!$mixed instanceof DOMNodeList) {
return $mixed; // technically not possible with std. SimpleXMLElement
}
$result = [];
$class = get_class($this->xml);
foreach ($mixed as $node) {
$result[] = simplexml_import_dom($node, $class);
}
return $result;
}
...
It's more straight forward for the actual registering of xpath namespaces because it works 1:1:
...
/**
* Registers the namespace with the DOMXPath object
*
* #param string $prefix The prefix.
* #param string $namespaceURI The URI of the namespace.
*
* #return bool true on success or false on failure.
*/
public function registerNamespace($prefix, $namespaceURI)
{
return $this->xpath->registerNamespace($prefix, $namespaceURI);
}
...
With these implementations in the chest, all what is left is to extend from SimpleXMLElement and wire it with the newly created SimpleXpath class:
/**
* Class SimpleXpathXMLElement
*/
class SimpleXpathXMLElement extends SimpleXMLElement
{
/**
* Creates a prefix/ns context for the next XPath query
*
* #param string $prefix The namespace prefix to use in the XPath query for the namespace given in ns.
* #param string $ns The namespace to use for the XPath query. This must match a namespace in use by the XML
* document or the XPath query using prefix will not return any results.
*
* #return bool TRUE on success or FALSE on failure.
*/
public function registerXPathNamespace($prefix, $ns)
{
return SimpleXpath::of($this)->registerNamespace($prefix, $ns);
}
/**
* Runs XPath query on XML data
*
* #param string $path An XPath path
*
* #return SimpleXMLElement[] an array of SimpleXMLElement objects or FALSE in case of an error.
*/
public function xpath($path)
{
return SimpleXpath::of($this)->query($path, $this);
}
}
With this modification under the hood, it works transparently with your example if you use that sub-class:
/** #var SimpleXpathXMLElement $xmlObject */
$xmlObject = simplexml_load_string($buffer, 'SimpleXpathXMLElement');
$xmlObject->registerXPathNamespace('ns', 'urn:company');
$fields = $xmlObject->xpath("//ns:field");
foreach ($fields as $field) {
$errors = $field->xpath("//ns:error"); // no issue
var_dump((string)current($errors));
}
This example then runs error free, see here: https://eval.in/398767
The full code is in a gist, too: https://gist.github.com/hakre/1d9e555ac1ebb1fc4ea8

Copy links, title within <a> tag and date values from foreign website using Zend Framework

It is possible by using Zend Request extract link, strings within tags also data values from foreign site and copy all this to an array and echo it?
For example following website http://bills.ru/, extract from table below "события на долговом рынке", all data should be store in an array having following structure.
id
date
title
url
Or can somebody at least give some good example of implementation Zend Request?
I recommend using something like Goutte which not let you filter the html returned.
If you don't want to use additional libraries you can also use Zend\Dom to query the html from your Request.
This is code i managed to make on following task and it works.
<?php
use Zend\Http\Client;
use Zend\Dom\Query;
/**
* Extracts date values, titles and links from block "события на долговом рынке" then save all date in 1
* array and prints it.
*
* Using Zend\Http\Client to make connection to website and further manipulate with Zend\Dom\Query's CSS selectors
* to retrieve date values, link and titles within block "событья на долговом рынке". Three private method are used to
* return values for each type and 1 public function used for retrieving
*
* #var client is Zend\Http\Client object and makes connection using function setUri() with declared website
* #var response servers as getting response from requested website
* #var dom is a Zend\Dom\Query object that allows manipulating with Zend\Http\Client objects
* #var results is a Zend\Dom\NodeList object made by using function execute()
* #var result used in foreach loop and for retrieving titles and url from a tag
* #var results_date same as #var results but for date values
* #var result_date same as #var result but for date values
* #var dateArray array where date values will be stored
* #var valuesArray array where data will be stored and printed afterwards
* #var html used to story content from #var client
*/
class BILLS
{
public $client;
public $response;
public $dom;
public $results;
public $result;
public $results_date;
public $result_date;
public $dateArray;
public $valuesArray;
public $html;
/**
* When new object with following class is created an object Zend\Http\Client is created and set Uri attribute.
* A request is being done to this object and data is put into $html variable for further use.
* #see client, response, html
*/
function __construct ()
{
$this->client = new \Zend\Http\Client();
$this->client->setUri('http://bills.ru');
$this->client->send();
$this->response = $this->client->getResponse();
$this->html = $this->response->getBody();
}
/**
* Returns date values within object
* #see result_date
*/
private function _date()
{
return $this->result_date->textContent;
}
/**
* Returns text content within object
* #see result
*/
private function _title()
{
return $this->result->textContent;
}
/**
* Returns url within object
* #see result
*/
private function _url()
{
return $this->result->getAttribute('href');
}
/**
* If connection has no problems a new Query object is created and searched for a tags with class new. Then
* using a foreach loop found data is stored in array and printed to screen. Uses 3 private function for returning
* values for each type that will be stored in array an printed afterwards.
*
* #see dom, results_date, dateArray, results, valuesArray, _date(), _url(), _title()
*
*/
public function printTask()
{
$iteration = 0;
$iterationData = 0;
if($this->response->getStatusCode() == 200)
{
$this->dom = new Query($this->html);
$this->results_date = $this->dom->execute('table tr td.news');
foreach ($this->results_date as $this->result_date)
{
if($iterationData < 5)
{
$dateArray[$iterationData] = $this->_date();
$iterationData++;
}
}
$this->results = $this->dom->execute('table tr td a.news');
foreach ($this->results as $this->result)
{
if($iteration < 5)
{
$valuesArray = array(
'id' => $iteration+1,
'date' => $dateArray[$iteration],
'title' => $this->_title(),
'url' => "http://bills.ru".$this->_url()
);
echo '<pre>';
print_r($valuesArray);
echo '</pre>';
$iteration++;
}
}
}
}
}
$object = new BILLS;
$object->printTask();
?>

General help about Iterators with OOP in PHP

I am just learning about OOP in PHP from a book and the section on Iterators and Iteration has me stumped.
For what I understand, I think in order to loop through an object's attributes, you need to implement the built-in class Iterator. Then implement the IteratorAggregate interface and create a getIterator method within. However, I am currently confused on the role each of these elements play and in what format they need to be written. In other words, I am just looking for a simply (plain-English) explanation of these concepts and a simple example. Any help would be greatly appreciated!!!!!
And thank you for your time and help in advance!!!!
The thing you want to loop over must implement Traversable. However, you can't implement Traversable directly; you have to implement one of its subtypes. How you'll do things depends on how the object will be used.
If you wanted, you could just implement Iterator. That works well enough if your type is intended to be a forward-only list of stuff already. If you decide to build an iterator, you'll have 5 methods to implement:
current, which retrieves the value at the current location;
key, which retrieves the current location's key;
next, which advances to the next location;
rewind, which resets the current location to the first; and
valid, which returns false if iteration has fallen off the end of the list of stuff being iterated over.
PHP calls those methods over the course of iteration. Specifically, let's say you have code like this:
foreach ($it as $key => $value) {
doStuffWith($key, $value);
}
This is rather equivalent to the following:
for ($it->rewind(); $it->valid(); $it->next()) {
$value = $it->current();
$key = $it->key(); // only if the foreach has a `$key =>`
doStuffWith($key, $value);
}
Basically you just need to build a type that implements Iterator and responds properly to those methods being invoked in roughly that order. Ideally it should also be possible for something to happen in between...but that's usually not an issue unless you're passing references around.
If you don't need custom iteration, you could instead just implement IteratorAggregate, and return an existing iterator type if it's able to do what you need. (For example, if you want to allow looping over an internal array, there's already an ArrayIterator made for the job. No need to roll your own.) For IteratorAggregate, you only have to implement getIterator, which returns something that's traversable. This is a better solution if you have something that can already be traversed by one of the built-in SPL iterators, or can easily be reduced to an array or something.
That same loop above, if called on an IteratorAggregate, would equate to something like
foreach ($it->getIterator() as $key => $value) {
doStuffWith($key, $value);
}
getIterator() has to return either an implementation of Iterator or some primitive traversable thingie, like an array.
As for Java-style iterators, you might build an Iterator subtype that can remember its place and loop over your collection, and then implement IteratorAggregate on your collection to return an instance of the iterator type.
For basic functionality you only really need to implement Iterator and add the relevant functionality for the rewind, valid, key, current, and next methods. See below for an example:
/**
* Awesome
*
* Do awesome stuff
*/
final class Awesome implements Iterator
{
/**
* An array of data
*
* #access private
* #var array $_data
*/
private $_data;
/**
* Store the initial data
*
* #access public
* #param array $data
*/
public function __construct(array $data = array())
{
$this->_data = $data;
}
/**
* Rewind the iterator
*
* #access public
*/
public function rewind()
{
reset($this->_data);
}
/**
* Validate the existence of the next element
*
* #access public
* #return boolean
*/
public function valid()
{
return isset($this->_data[$this->key()]);
}
/**
* Return the current key
*
* #access public
* #return integer
*/
public function key()
{
return key($this->_data);
}
/**
* Return the current value
*
* #access public
* #return mixed
*/
public function current()
{
return current($this->_data);
}
/**
* Increment the iteration index
*
* #access public
*/
public function next()
{
next($this->_data);
}
}
// Instantiate a new Awesome object
$awesome = new Awesome(array('Michael', ' ', 'Rushton', ' ', 'is', ' ', 'awesome', '!'));
// Iterate over the awesome object and output a universal truth
foreach ($awesome as $almost_awesome)
{
echo $almost_awesome;
}
If you wish to instead iterate over the object's properties simply change the __construct to:
/**
* Construct the object
*
* #access public
*/
public function __construct()
{
// Get the properties
$object_vars = get_object_vars($this);
// Unset the data reference
unset($object_vars['_data']);
// Set the data
$this->_data = $object_vars;
}

Categories