I'm wondering if anyone knows how to build an XML file, dynamically from a list of classes?
The classes contains public variables, and look like this.
class node {
public $elementname;
}
I do note that some of the classes are named the same as variables, and in this case, the node would be a subnode element of a node:
class data {
public $dataaset;
}
class dataset {
public $datasetint;
}
would be:
<data>
<dataset>datasetint</dataset>
</data>
Maybe something in SimpleXML or something?
The only solution i can think of linking 2 or more unrelated class is using Annotations.
Annotations is not supported by default in PHP but currently in RFC (Request for Comments: Class Metadata) but bending the time is supported or rejected you can create yours using ReflectionClass & Comments functionality
Example If you have 3 classes like this
class Data {
/**
*
* #var Cleaner
*/
public $a;
/**
*
* #var Extraset
*/
public $b;
public $runMe;
function __construct() {
$this->runMe = new stdClass();
$this->runMe->action = "RUN";
$this->runMe->name = "ME";
}
}
class Cleaner {
public $varInt = 2;
public $varWelcome = "Hello World";
/**
*
* #var Extraset
*/
public $extra;
}
class Extraset {
public $boo = "This is Crazy";
public $far = array(1,2,3);
}
Then you can run a code like this
$class = "Data";
$xml = new SimpleXMLElement("<$class />");
getVariablesXML($class, $xml);
header("Content-Type: text/xml");
$xml->asXML('data.xml');
echo $xml->asXML();
Output
<?xml version="1.0"?>
<Data>
<Cleaner name="a">
<varInt type="integer">2</varInt>
<varWelcome type="string">Hello World</varWelcome>
<Extraset name="extra">
<boo type="string">This is Crazy</boo>
<far type="serialized">a:3:{i:0;i:1;i:1;i:2;i:2;i:3;}</far>
</Extraset>
</Cleaner>
<Extraset name="b">
<boo type="string">This is Crazy</boo>
<far type="serialized">a:3:{i:0;i:1;i:1;i:2;i:2;i:3;}</far>
</Extraset>
<runMe type="serialized">O:8:"stdClass":2:{s:6:"action";s:3:"RUN";s:4:"name";s:2:"ME";}</runMe>
</Data>
Function Used
function getVariablesXML($class, SimpleXMLElement $xml) {
$reflect = new ReflectionClass($class);
foreach ( $reflect->getProperties(ReflectionProperty::IS_PUBLIC) as $property ) {
$propertyReflect = $reflect->getProperty($property->getName());
preg_match("/\#var (.*)/", $propertyReflect->getDocComment(), $match);
$match and $match = trim($match[1]);
if (empty($match)) {
$value = $property->getValue(new $class());
if (is_object($value) || is_array($value)) {
$type = "serialized";
$value = serialize($value);
} else {
$type = gettype($value);
}
$child = $xml->addChild($property->getName(), $value);
$child->addAttribute("type", $type);
} else {
$child = $xml->addChild($match);
$child->addAttribute("name", $property->getName());
if (class_exists($match)) {
getVariablesXML($match, $child);
}
}
}
}
Related
I'm migrating the Zend\Db driven DBAL of a Zend Framework 3 application to Doctrine. Everything is working fine, but now I got a problem with the export of data.
Before the migration it was working as follows:
There is a more or less complex data structure. The Mapper executed some database requests and built a nested DataObject from this data. So, the start point for the export was an object, filled with all data and having sub-objects, also with all their data. So I simply converted it to JSON:
public function exportToJson(AbstractDataObject $dataObject)
{
return json_encode($dataObject, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
}
public function exportToXml(AbstractDataObject $dataObject)
{
$dataObjectVars = json_decode(json_encode($dataObject->jsonSerialize()), true);
$xml = new \SimpleXMLElement('<' . self::XML_DEFAULT_ROOT_ELEMENT . ' />');
$this->arrayToXml($dataObjectVars, $xml);
$domxml = new \DOMDocument('1.0');
$domxml->preserveWhiteSpace = false;
$domxml->formatOutput = true;
$domxml->loadXML($xml->asXML());
$xmlString = $domxml->saveXML();
return $xmlString;
}
protected function arrayToXml($array, &$xml)
{
foreach ($array as $key => $value) {
if(is_array($value)){
if(is_int($key)){
$key = self::XML_DEFAULT_ELEMENT_NAME;
}
$label = $xml->addChild($key);
$this->arrayToXml($value, $label);
}
else {
$xml->addChild($key, $value);
}
}
}
All DataObjects extended the AbstractDataObject and it provided a method, that made it easily exportable to JSON:
class AbstractDataObject implements \JsonSerializable
{
public function jsonSerialize()
{
$reflection = new \ReflectionClass($this);
$properties = $reflection->getProperties();
$members = [];
foreach ($properties as $property) {
$property->setAccessible(true);
$members[$property->getName()] = $property->getValue($this);
}
$keys = array_keys($members);
$values = array_values($members);
$keysUnderscored = preg_replace_callback('/([A-Z])/', function($matches) {
return '_' . strtolower($matches[1]);
}, $keys);
$varsUnderscored = array_combine($keysUnderscored, $values);
return $varsUnderscored;
}
}
Now the object to export is an entity and it usually doesn't not have all its data loaded. That means, the approach described above doesn't work anymore.
Is there / What is a proper way to convert a nested entity (means an entity with its sub-entities) to a structured data format (array / JSON / XML)?
Finally I've got it working as suggested in Cerad's comment with the Symfony Serializer Component.
I got some troubles with the encoding: The JSON_ERROR_UTF8 for JSON and the "Warning: DOMDocument::saveXML(): invalid character value" for XML. So I had to "utf8ize" the array data received from the Serializer and reimplement the exportToXml(...) by using the dom_import_simplexml(...) as shown here. But now it's working, here we go:
namespace MyNamespace\DataExport;
use MyNamespace\DataObject\AbstractDataObject;
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use Doctrine\Common\Annotations\AnnotationReader;
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
class DataExporter
{
/** #var string */
const EXPORT_FORMAT_JSON = 'json';
/** #var string */
const EXPORT_FORMAT_XML = 'xml';
/** #var string */
const XML_DEFAULT_ROOT_ELEMENT = 'my_root_element_name';
/** #var string */
const XML_DEFAULT_ELEMENT_NAME = 'item';
/** #var Serializer */
protected $serializer;
public function __construct()
{
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$normalizer = new ObjectNormalizer($classMetadataFactory, new CamelCaseToSnakeCaseNameConverter());
$normalizer->setCircularReferenceLimit(1);
$normalizer->setIgnoredAttributes(['__initializer__', '__cloner__', '__isInitialized__']);
$normalizer->setCircularReferenceHandler(function ($object) {
// #todo A cleaner solution need.
try {
$return = $object->getId();
} catch (\Error $exception) {
$return = null;
}
$return = null;
return $return;
});
$normalizers = [$normalizer];
$this->serializer = new Serializer($normalizers);
}
public function exportToJson(AbstractDataObject $dataObject)
{
$data = $this->serializer->normalize($dataObject, null, ['groups' => ['export']]);
$data = $this->utf8ize($data);
return json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
}
public function exportToXml(AbstractDataObject $dataObject)
{
$data = $this->serializer->normalize($dataObject, null, ['groups' => ['export']]);
$data = $this->utf8ize($data);
$xml = new \SimpleXMLElement('<' . self::XML_DEFAULT_ROOT_ELEMENT . ' />');
$this->arrayToXml($data, $xml);
$domDocument = dom_import_simplexml($xml)->ownerDocument;
$domDocument->formatOutput = true;
$xmlString = $domDocument->saveXML();
return $xmlString;
}
protected function utf8ize($data) {
if (is_array($data)) {
foreach ($data as $key => $value) {
$data[$key] = $this->utf8ize($value);
}
} else if (is_string ($data)) {
return utf8_encode($data);
}
return $data;
}
/**
* Converts an $array to XML and
* saves the result to the $xml argument.
*
* #param array $array
* #param \SimpleXMLElement $xml
* #return void
*/
protected function arrayToXml($array, &$xml){
foreach ($array as $key => $value) {
if(is_array($value)){
if(is_int($key)){
$key = self::XML_DEFAULT_ELEMENT_NAME;
}
$label = $xml->addChild($key);
$this->arrayToXml($value, $label);
}
else {
$xml->addChild($key, $value);
}
}
}
}
I built an basic xml class (using simple xml), so far I built simple xml nodes.
Now I want to create a function in which generate a specific number of nodes in a foreach, which I specify in the parameter.
it looks like this now:
class Xml
{
private $data = "";
protected $xml = "";
protected $xmlManager = "";
protected $menus = "";
protected $xmlMenus = "";
public function __construct($data = [])
{
$this->data = $data;
$this->xml = new \SimpleXmlElement('<test></test>');
$this->setMenus();
return $this->xml;
}
private function setMenus()
{
$this->xmlMenus = $this->xmlManager->addChild('menus');
}
public function setMenuNode($parameter)
{
$this->data->menu = []
foreach ($this->data->menus as $menuKey => $menuValue)
{
$this->xmlMenu = $this->xmlMenus->addChild('menu');
$menueValue->addAttribute($param1, $param2, $param3);
$menueValue->addAttribute($param1, $param2, $param3);
}
}
}
Later on I want to call it like this
Xml->setMenuNode($param1, $param2, $param3);
Which should create 3 menu nodes.
My xml should look like this later on.
<?xml version="1.0"?>
<menus>
<menu id="1" entry="title">
...
</menu>
<menu id="2" entry="title2">
...
</menu>
<menu id="3" entry="title2">
...
</menu>
</menus>
</dvd>
I am not quiet sure how to manager this in a good way.
This are two jobs, so you should split it into two classes. For a loose dependency define an interface. The menu and the items need to be append itself to their parent.
I use DOM because SimpleXMLElement does not support an empty XML document. This solution would work with SimpleXML, too. But only with an additional outer XML element node.
Interface
This is a contract, so you should define an interface for it.
interface DOMAppendable {
public function appendTo(\DOMNode $parent);
}
Item Class
For a single item implement the interface:
class MenuItem implements DOMAppendable {
private $_id = '';
private $_title = '';
public function __construct($id, $title) {
$this->_id = $id;
$this->_title = $title;
}
public function appendTo(\DOMNode $parent) {
$document = $parent->ownerDocument ?: $parent;
$item = $parent->appendChild($document->createElement('menu'));
$item->setAttribute('id', $this->_id);
$item->setAttribute('title', $this->_title);
}
}
The constructor stores the data into private properties. The method from the interface appends the nodes to the provided parent. The menu items contain more data, have subitems. As long as they implement the interface it will still work.
Menu/List Class
A class for the menu itself stores added menu items into an array property. It implements the interface, too. (So it can be appended to another menu). Implementing __toString() allows to cast the menu into a string.
class Menu implements DOMAppendable {
private $_items = [];
public function add(\DOMAppendable $item) {
foreach (func_get_args() as $item) {
if (!($item instanceOf DOMAppendable)) {
throw new \InvalidArgumentException("Invalid menu item.");
}
$this->_items[] = $item;
}
}
public function appendTo(\DOMNode $parent) {
$document = $parent->ownerDocument ?: $parent;
$menu = $parent->appendChild($document->createElement('menus'));
foreach ($this->_items as $item) {
$item->appendTo($menu);
}
}
public function __toString() {
$document = new \DOMDocument();
$document->formatOutput = TRUE;
$this->appendTo($document);
return $document->saveXml();
}
}
Use
With that you can create the menu using the new classes.
$menu = new Menu();
$menu->add(
new MenuItem(1, 'title1'),
new MenuItem(2, 'title2'),
new MenuItem(3, 'title3')
);
echo $menu;
Output:
<?xml version="1.0"?>
<menus>
<menu id="1" title="title1"/>
<menu id="2" title="title2"/>
<menu id="3" title="title3"/>
</menus>
The interface allows to use different menu item classes, even a menu can be an item:
$menu = new Menu();
$menu->add(
new MenuItem(1, 'title1'),
new MenuItem(2, 'title2')
);
$subMenu = new Menu();
$subMenu->add(new MenuItem(3.1, 'title3.1'));
$menu->add($subMenu);
echo $menu;
Output:
<?xml version="1.0"?>
<menus>
<menu id="1" title="title1"/>
<menu id="2" title="title2"/>
<menus>
<menu id="3.1" title="title3.1"/>
</menus>
</menus>
PHP >= 5.6
PHP 5.6 and later support the new variadics syntax. This allows to remove the func_get_args() call and simplify the Menu::add() method.
class Menu implements DOMAppendable {
private $_items = [];
public function add(\DOMAppendable ...$items) {
foreach ($items as $item) {
$this->_items[] = $item;
}
}
...
Based on what you described you would get (PHP 5.6):
function setMenuNode(...$parameters)
{
foreach ($parameters as $parameter) {
$this->xmlMenu = $this->xmlMenus->addChild('menu');
$menueValue->addAttribute('id', $parameter);
$menueValue->addAttribute('entry', 'title' . $parameter);
}
}
I need to integrate the code that is in the answer to this thread.
How to rotate SVG by PHP
When I do it as a library I get error "construct", I do not have much knowledge about it, I hope you can help me.
From this class:
class ExSimpleXMLElement extends SimpleXMLElement
{
public function _construct($xml){
parent::_construct($xml);
}
/**
* Add SimpleXMLElement code into a SimpleXMLElement
* #param SimpleXMLElement $append
*/
public function appendXML($append)
{
if ($append) {
if (strlen(trim((string) $append))==0) {
$xml = $this->addChild($append->getName());
foreach($append->children() as $child) {
$xml->appendXML($child);
}
} else {
$xml = $this->addChild($append->getName(), (string) $append);
}
foreach($append->attributes() as $n => $v) {
$xml->addAttribute($n, $v);
}
}
}
}
You would transform it like this:
<?php
class ExSimpleXMLElement
{
private $sxe;
public function loadXml($xml){
$this->sxe = new SimpleXMLElement($xml);
}
/**
* Add SimpleXMLElement code into a SimpleXMLElement
* #param SimpleXMLElement $append
*/
public function appendXML($append)
{
if ($append) {
if (strlen(trim((string) $append))==0) {
$xml = $this->sxe->addChild($append->getName());
foreach($append->children() as $child) {
$xml->appendXML($child);
}
} else {
$xml = $this->sxe->addChild($append->getName(), (string) $append);
}
foreach($append->attributes() as $n => $v) {
$xml->addAttribute($n, $v);
}
}
}
}
And use it like this:
$this->load->library('ExSimpleXMLElement');
$this->exsimplexmlelement->loadXML($xml);
I am using Reflections to adjust various values in objects, and I have an object who's parent I need to adjust.
For example:
class Ford extends Car
{
private $model;
}
class Car
{
private $color;
}
I can easily use Reflection to change the model, but how can I separate the parent from the child, so that I can use Reflection on the parent?
Some psuedo code for what I'm hoping is possible:
$ford = new Ford();
$manipulator = new Manipulator($ford);
$manipulator->set('model','F-150');
$manipulator->setParentValue('color','red');
class Manipulator
{
public function __construct($class) {
$this->class = $class;
$this->reflection = new \ReflectionClass($class);
}
public function set($property,$value) {
$property = $this->reflection->getProperty($property);
$property->setAccessible(true);
$property->setValue($this->class,$value);
}
public function setParentValue() {
$parent = $this->reflection->getParent();
$property = $this->reflection->getProperty($property);
$property->setAccessible(true);
// HOW DO I DO THIS?
$property->setValue($this->class::parent,$value);
}
}
Gist of the question:
In this case, how can I change the $color from outside the object altogether?
Is there something like Ford::parent() or get_parent_object($ford) available?
Note
The objects used above are not the exact scenario, but just used to illustrate the concept. In the real world case, I have a parent/child relationship, and I need to be able to access/change values in each from the outside.
ANSWER
Please check my answer below...I figured it out.
After extensive review, I have found that I can't access the parent of an object AS AN OBJECT outside of the object itself.
However, using Reflections, I was able to solve the example posted above:
<?php
class Car
{
private $color;
public function __construct()
{
$this->color = 'red';
}
public function color()
{
return $this->color;
}
}
class Ford extends Car
{
}
$ford = new Ford();
echo $ford->color(); // OUTPUTS 'red'
$reflection = new ReflectionClass($ford);
$properties = $reflection->getProperties();
foreach($properties as $property) {
echo $property->getName()."\n>";
}
$parent = $reflection->getParentClass();
$color = $parent->getProperty('color');
$color->setAccessible(true);
$color->setValue($ford,'blue');
echo $ford->color(); // OUTPUTS 'blue'
See it in action here: http://codepad.viper-7.com/R45LN0
See get_parent_class(): http://php.net/manual/en/function.get-parent-class.php
function getPrivateProperty(\ReflectionClass $class, $property)
{
if ($class->hasProperty($property)) {
return $class->getProperty($property);
}
if ($parent = $class->getParentClass()) {
return getPrivateProperty($parent, $property);
}
return null;
}
Here is the static version of the function I answered your other question with:
function getProperties($object) {
$properties = array();
try {
$rc = new \ReflectionClass($object);
do {
$rp = array();
/* #var $p \ReflectionProperty */
foreach ($rc->getProperties() as $p) {
$p->setAccessible(true);
$rp[$p->getName()] = $p->getValue($object);
}
$properties = array_merge($rp, $properties);
} while ($rc = $rc->getParentClass());
} catch (\ReflectionException $e) { }
return $properties;
}
I've made my first class using TDD (SimpleTest). It's working pretty well. This class parses an XML config file and returns it as an array. How could I improve it (performance, any tips)? What about the class responsabilities? Maybe XMLtoArray should be moved to another class, i don't know...
<?php
class Configuration
{
private $domdocument_object;
private $domxpath_object;
public function __construct($filename) {
$this->loadXML($filename);
$this->domxpath_object = new DOMXPath($this->domdocument_object);
}
private function loadXML($filename)
{
if (!file_exists($filename))
{
throw new ConfigurationException('Configuration file not found');
}
$this->domdocument_object = $domdocument_object = new DOMDocument();
$this->domdocument_object->preserveWhiteSpace = false;
if (!$this->domdocument_object->load($filename))
{
throw new ConfigurationException('Malformed configuration file');
}
}
public function get($path = '/*') {
$configuration = array();
$domnodelist_object = $this->domxpath_object->query($path);
$configuration = $this->XMLToArray($domnodelist_object);
/**
* Get a configuration entry as string or array
*
* For example:
* $xml = '<foo><bar>baz</bar></foo>'
* $path = '/foo/bar/'
* return just baz, as string instead of an array('baz');
*
* Another example:
* $xml = '<foo><bar>baz</bar><lorem>ipsum</lorem></foo>';
* $path = '/foo'
* return just array('bar' => 'baz', 'lorem' => ipsum);
* instead of array('foo' => array('bar' => 'baz', 'lorem' => ipsum));
*/
while (!is_string($configuration) && count($configuration) == 1)
{
$configuration_values = array_values($configuration);
$configuration = $configuration_values[0];
}
if (empty($configuration))
{
$configuration = null;
}
return $configuration;
}
public function XMLToArray(DOMNodeList $domnodelist_object) {
$configuration = array();
foreach ($domnodelist_object as $element)
{
if ($element->nodeType == XML_DOCUMENT_NODE)
{
if ($element->hasChildNodes())
{
$configuration = $this->XMLToArray($element->childNodes);
}
}
else if ($element->nodeType == XML_ELEMENT_NODE)
{
if (!$element->hasChildNodes())
{
$configuration[$element->nodeName] = null;
}
else if (
$element->firstChild->nodeType == XML_TEXT_NODE ||
$element->firstChild->nodeType == XML_CDATA_SECTION_NODE
)
{
$configuration[$element->nodeName] = $element->nodeValue;
}
else if ($element->firstChild->nodeType == XML_ELEMENT_NODE)
{
$configuration[$element->nodeName] = $this->XMLToArray($element->childNodes);
}
}
}
return $configuration;
}
}
?>
This class ignores XML attributes.
Thank you.
Something that stood out to me is your creating a new object every time an object is executed, you should store the object locally (in the object) and then it only uses 1 portion of your memory.
Here are the changes I would do:
class Configuration
{
private $domdocument_object;
private $domxpath_object; //+
public function __construct($filename)
{
$this->loadXML($filename);
$this->domxpath_object = new DOMXPath($this->domdocument_object); //+
}
public function get($path = '/*')
{
//Remove the following
$domxpath_object = new DOMXPath($this->domdocument_object);
}
}
and then change the $domxpath_object to $this->domxpath_object where its required.
But this should really be moved over to CoderReview as this is off topic.
This is bad for performance:
$xml = preg_replace("/>\s+</", "><", $xml);
Aditionally it is not guaranteed to be reliable (this could change comments and CDATA-sections in undesired ways). It is though not easy to find a better solution. Iterating over all text nodes trimming them will be more reliable, but not faster.
If you just care for making this an array php's SAX parser or SimpleXML may be more suitable. Both options may be faster (I did not test).