Given two SimpleXMLElement objects structured as follows (identical):
$obj1
object SimpleXMLElement {
["#attributes"] =>
array(0) {
}
["event"] =>
array(1) {
[0] =>
object(SimpleXMLElement) {
["#attributes"] =>
array(1) {
["url"] =>
string "example.com"
}
}
}
}
$obj2
object SimpleXMLElement {
["#attributes"] =>
array(0) {
}
["event"] =>
array(1) {
[0] =>
object(SimpleXMLElement) {
["#attributes"] =>
array(1) {
["url"] =>
string "another-example.com"
}
}
}
}
I am trying to copy all the child event items from the second object to the first as so:
foreach ($obj2->event as $event) {
$obj1->event[] = $event
}
They move, but the copied objects are now empty.
$obj1
object SimpleXMLElement {
["#attributes"] =>
array(0) {
}
["event"] =>
array(2) {
[0] =>
object(SimpleXMLElement) {
["#attributes"] =>
array(1) {
["url"] =>
string "example.com"
}
}
[1] =>
object(SimpleXMLElement) {
}
}
}
SimpleXML's editing functionality is rather limited, and in this case, both the assignment you're doing and ->addChild can only set the text content of the new element, not its attributes and children.
This may be one case where you need the power of the more complex DOM API. Luckily, you can mix the two in PHP with almost no penalty, using dom_import_simplexml and simplexml_import_dom to switch to the other wrapper.
Once you have a DOM representation, you can use the appendChild method of DOMNode to add a full node, but there's a catch - you can only add nodes "owned by" the same document. So you have to first call the importNode method on the document you're trying to edit.
Putting it all together, you get something like this:
// Set up the objects you're copying from and to
// These don't need to be complete documents, they could be any element
$obj1 = simplexml_load_string('<foo><event url="example.com" /></foo>');
$obj2 = simplexml_load_string('<foo><event url="another.example.com" /></foo>');
// Get a DOM representation of the root element we want to add things to
// Note that $obj1_dom will always be a DOMElement not a DOMDocument,
// because SimpleXML has no "document" object
$obj1_dom = dom_import_simplexml($obj1);
// Loop over the SimpleXML version of the source elements
foreach ($obj2->event as $event) {
// Get the DOM representation of the element we want to copy
$event_dom = dom_import_simplexml($event);
// Copy the element into the "owner document" of our target node
$event_dom_copy = $obj1_dom->ownerDocument->importNode($event_dom, true);
// Add the node as a new child
$obj1_dom->appendChild($event_dom_adopted);
}
// Check that we have the right output, not trusting var_dump or print_r
// Note that we don't need to convert back to SimpleXML
// - $obj1 and $obj1_dom refer to the same data internally
echo $obj1->asXML();
Related
I'm not sure if this is the expected behavior or if I'm doing something wrong:
<?php
$xml = '<?xml version="1.0"?>
<foobar>
<foo>
<nested>
<img src="example1.png"/>
</nested>
</foo>
<foo>
<nested>
<img src="example2.png"/>
</nested>
</foo>
</foobar>';
$dom = new DOMDocument();
$dom->loadXML($xml);
$node = $dom->getElementsByTagName('foo')[0];
$simplexml = simplexml_import_dom($node);
echo $simplexml->asXML() . "\n";
echo " === With // ====\n";
var_dump($simplexml->xpath('//img'));
echo " === With .// ====\n";
var_dump($simplexml->xpath('.//img'));
Even though I only imported a specific DomNode, and asXml() returns only that part, the xpath() still seems to operate on the whole document.
I can prevent that by using .//img, but that seemed rather strange to me.
Result:
<foo>
<nested>
<img src="example1.png"/>
</nested>
</foo>
=== With // ====
array(2) {
[0] =>
class SimpleXMLElement#4 (1) {
public $#attributes =>
array(1) {
'src' =>
string(12) "example1.png"
}
}
[1] =>
class SimpleXMLElement#5 (1) {
public $#attributes =>
array(1) {
'src' =>
string(12) "example2.png"
}
}
}
=== With .// ====
array(1) {
[0] =>
class SimpleXMLElement#5 (1) {
public $#attributes =>
array(1) {
'src' =>
string(12) "example1.png"
}
}
}
It is expected behavior. You're importing an DOM element node into an SimpleXMLElement. This does not modify the XML document in the background - the node keeps its context.
Here are Xpath expressions that go up (parent::, ancestor::) or to siblings (preceding-sibling::, following-sibling::).
Location paths starting with a / are always relative to the document, not the context node. An explicit reference to the current node with the . avoids that trigger. .//img is short for current()/descendant-or-self::img - an alternative would be descendant::img.
However you don't need to convert the DOM node into a SimpleXMLElement to use Xpath.
$document = new DOMDocument();
$document->loadXML($xml);
$xpath = new DOMXpath($document);
foreach ($xpath->evaluate('//foo[1]') as $foo) {
var_dump(
$xpath->evaluate('string(.//img/#src)', $foo)
);
}
Output:
string(12) "example1.png"
//foo[1] fetches the first foo element node in the document. If here is no matching element in the document it will return an empty list. Using foreach allows to avoid an error in that case. It will be iterated once or never.
string(.//img/#src) fetches the src attribute of descendant img elements and casts the first one into a string. If here is no matching node the return value will be and empty string. The second argument to DOMXpath::evaluate() is the context node.
I read the following XML:
http://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml
controller:
$ECB_rates = array();
$currencies = explode(',', 'GBP,USD,RUB,AUD');
foreach($currencies as $currency) {
$ECB_rates[$currency] = $ECB_XML->xpath('//Cube/Cube/Cube[#currency="' . $currency . '"]/#rate');
}
$this->set('ECB_rates', $ECB_rates);
view:
var_dump($ECB_rates);
and I get the following:
array(4) { ["GBP"]=> array(0) { } ["USD"]=> array(0) { } ["RUB"]=> array(0) { } ["AUD"]=> array(0) { } }
I can't figure out why the rates are returned as empty array.
The document has a default namespace, which can commonly confuse XPath expressions if not done correctly. The other part is that xpath() returns an array of SimpleXMLElements which also doesn't help your cause.
The following code first registers the default namespace with the prefix 'def' and then (I've simplified the expression as well) uses this as a prefix to find the <Cube> element with the currency you want. Then it takes the first result (there should only be one anyway) and casts it to a string to make it more useful.
$ECB_XML->registerXPathNamespace("def", "http://www.ecb.int/vocabulary/2002-08-01/eurofxref");
$ECB_rates = array();
$currencies = explode(',', 'GBP,USD,RUB,AUD');
foreach($currencies as $currency) {
$ECB_rates[$currency] = (string)$ECB_XML->xpath('//def:Cube[#currency="' . $currency . '"]/#rate')[0];
}
var_dump($ECB_rates);
Which gives...
array(4) {
'GBP' =>
string(7) "0.87295"
'USD' =>
string(6) "1.2234"
'RUB' =>
string(7) "70.8270"
'AUD' =>
string(6) "1.5934"
}
Update:
You could (if the XML format is stable) just use SimpleXML element/attribute access. Checking if the currency is in the array of currencies your after...
foreach($ECB_XML->Cube->Cube->Cube as $rate) {
$currency = (string)$rate["currency"];
if ( in_array($currency, $currencies)) {
$ECB_rates[$currency] = (string)$rate["rate"];
}
}
This may give you the items in a different order, but this might not be an issue.
I'm trying to get a multi-dimensional array from an Entity.
Symfony Serializer can already convert to XML, JSON, YAML etc. but not to an array.
I need to convert because I want have a clean var_dump. I now have entity with few connections and is totally unreadable.
How can I achieve this?
You can actually convert doctrine entities into an array using the built in serializer. I actually just wrote a blog post about this today:
https://skylar.tech/detect-doctrine-entity-changes-without/
You basically call the normalize function and it will give you what you want:
$entityAsArray = $this->serializer->normalize($entity, null);
I recommend checking my post for more information about some of the quirks but this should do exactly what you want without any additional dependencies or dealing with private/protected fields.
Apparently, it is possible to cast objects to arrays like following:
<?php
class Foo
{
public $bar = 'barValue';
}
$foo = new Foo();
$arrayFoo = (array) $foo;
var_dump($arrayFoo);
This will produce something like:
array(1) {
["bar"]=> string(8) "barValue"
}
If you have got private and protected attributes see this link : https://ocramius.github.io/blog/fast-php-object-to-array-conversion/
Get entity in array format from repository query
In your EntityRepository you can select your entity and specify you want an array with getArrayResult() method.
For more informations see Doctrine query result formats documentation.
public function findByIdThenReturnArray($id){
$query = $this->getEntityManager()
->createQuery("SELECT e FROM YourOwnBundle:Entity e WHERE e.id = :id")
->setParameter('id', $id);
return $query->getArrayResult();
}
If all that doesn't fit you should go see the PHP documentation about ArrayAccess interface.
It retrieves the attributes this way : echo $entity['Attribute'];
PHP 8 allows us to cast object to array:
$var = (array)$someObj;
It is important for object to have only public properties otherwise you will get weird array keys:
<?php
class bag {
function __construct(
public ?bag $par0 = null,
public string $par1 = '',
protected string $par2 = '',
private string $par3 = '')
{
}
}
// Create myBag object
$myBag = new bag(new bag(), "Mobile", "Charger", "Cable");
echo "Before conversion : \n";
var_dump($myBag);
// Converting object to an array
$myBagArray = (array)$myBag;
echo "After conversion : \n";
var_dump($myBagArray);
?>
The output is following:
Before conversion :
object(bag)#1 (4) {
["par0"]=> object(bag)#2 (4) {
["par0"]=> NULL
["par1"]=> string(0) ""
["par2":protected]=> string(0) ""
["par3":"bag":private]=> string(0) ""
}
["par1"]=> string(6) "Mobile"
["par2":protected]=> string(7) "Charger"
["par3":"bag":private]=> string(5) "Cable"
}
After conversion :
array(4) {
["par0"]=> object(bag)#2 (4) {
["par0"]=> NULL
["par1"]=> string(0) ""
["par2":protected]=> string(0) ""
["par3":"bag":private]=> string(0) ""
}
["par1"]=> string(6) "Mobile"
["�*�par2"]=> string(7) "Charger"
["�bag�par3"]=> string(5) "Cable"
}
This method has a benefit comparing to Serialiser normalizing -- this way you can convert Object to array of objects, not array of arrays.
I had the same issue and tried the 2 other answers. Both did not work very smoothly.
The $object = (array) $object; added alot of extra text in my key
names.
The serializer didn't use my active property because it did not have is in front of it and is a boolean. It also changed the sequence of my data and the data itself.
So I created a new function in my entity:
/**
* Converts and returns current user object to an array.
*
* #param $ignores | requires to be an array with string values matching the user object its private property names.
*/
public function convertToArray(array $ignores = [])
{
$user = [
'id' => $this->id,
'username' => $this->username,
'roles' => $this->roles,
'password' => $this->password,
'email' => $this->email,
'amount_of_contracts' => $this->amount_of_contracts,
'contract_start_date' => $this->contract_start_date,
'contract_end_date' => $this->contract_end_date,
'contract_hours' => $this->contract_hours,
'holiday_hours' => $this->holiday_hours,
'created_at' => $this->created_at,
'created_by' => $this->created_by,
'active' => $this->active,
];
// Remove key/value if its in the ignores list.
for ($i = 0; $i < count($ignores); $i++) {
if (array_key_exists($ignores[$i], $user)) {
unset($user[$ignores[$i]]);
}
}
return $user;
}
I basicly added all my properties to the new $user array and made an extra $ignores variable that makes sure properties can be ignored (in case you don't want all of them).
You can use this in your controller as following:
$user = new User();
// Set user data...
// ID and password are being ignored.
$user = $user->convertToArray(["id", "password"]);
Simple XMLElement Object
(
[IpStatus] => 1
[ti_pid_20642] => SimpleXmlElement Object
(
I have a SimpleXMLElment in above format and this XML is generated at run time and it's node values like ti_pid_20642 are partly dnymaic, for example ti_pid_3232, ti-pid_2323, ti_pid_anyumber.
My question is how can I get these nodes values and it's children using PHP?
To get all node names that are used in an XML string with SimpleXML you can use the SimpleXMLIterator:
$tagnames = array_keys(iterator_to_array(
new RecursiveIteratorIterator(
new SimpleXMLIterator($string)
, RecursiveIteratorIterator::SELF_FIRST
)
));
print_r($tagnames);
Which could give you exemplary (you did not give any XML in your question, Demo):
Array
(
[0] => IpStatus
[1] => ti_pid_20642
[2] => dependend
[3] => ti-pid_2323
[4] => ti_pid_anyumber
[5] => more
)
If you have problems to provide a string that contains valid XML, take your existing SimpleXMLelement and create an XML string out of it:
$string = $simpleXML->asXML();
However, if you like to get all tagnames from a SimpleXML object but you don't want to convert it to a string, you can create a recursive iterator for SimpleXMLElement as well:
class SimpleXMLElementIterator extends IteratorIterator implements RecursiveIterator
{
private $element;
public function __construct(SimpleXMLElement $element) {
parent::__construct($element);
}
public function hasChildren() {
return (bool)$this->current()->children();
}
public function getChildren() {
return new self($this->current()->children());
}
}
The usage of it would be similar (Demo):
$it = new RecursiveIteratorIterator(
new SimpleXMLElementIterator($xml), RecursiveIteratorIterator::SELF_FIRST
);
$tagnames = array_keys(iterator_to_array($it));
It just depends on what you need.
This becomes less straight forward, with namespaced elements. Depending if you want to get the local names only or the namspace names or even the namespace URIs with the tagnames.
The given SimpleXMLElementIterator could be changed to support the iteration over elements across namespaces, by default simplexml only offers traversal over elements in the default namespace:
/**
* SimpleXMLElementIterator over all child elements across namespaces
*/
class SimpleXMLElementIterator extends IteratorIterator implements RecursiveIterator
{
private $element;
public function __construct(SimpleXMLElement $element) {
parent::__construct(new ArrayIterator($element->xpath('./*')));
}
public function key() {
return $this->current()->getName();
}
public function hasChildren() {
return (bool)$this->current()->xpath('./*');
}
public function getChildren() {
return new self($this->current());
}
}
You would then need to check for the namespace per each element- As an example a modified XML document making use of namespaces:
<root xmlns="namspace:default" xmlns:ns1="namespace.numbered.1">
<ns1:IpStatus>1</ns1:IpStatus>
<ti_pid_20642>
<dependend xmlns="namspace:depending">
<ti-pid_2323>ti-pid_2323</ti-pid_2323>
<ti_pid_anyumber>ti_pid_anyumber</ti_pid_anyumber>
<more xmlns:ns2="namspace.numbered.2">
<ti_pid_20642 ns2:attribute="test">ti_pid_20642</ti_pid_20642>
<ns2:ti_pid_20642>ti_pid_20642</ns2:ti_pid_20642>
</more>
</dependend>
</ti_pid_20642>
</root>
Combined with the update SimpleXMLIterator above the following example-code demonstrates the new behavior:
$xml = new SimpleXMLElement($string);
$it = new RecursiveIteratorIterator(
new SimpleXMLElementIterator($xml), RecursiveIteratorIterator::SELF_FIRST
);
$count = 0;
foreach ($it as $name => $element) {
$nsList = $element->getNamespaces();
list($ns, $nsUri) = each($nsList);
printf("#%d: %' -20s %' -4s %s\n", ++$count, $name, $ns, $nsUri);
}
Output (Demo):
#1: IpStatus ns1 namespace.numbered.1
#2: ti_pid_20642 namspace:default
#3: dependend namspace:depending
#4: ti-pid_2323 namspace:depending
#5: ti_pid_anyumber namspace:depending
#6: more namspace:depending
#7: ti_pid_20642 namspace:depending
#8: ti_pid_20642 ns2 namspace.numbered.2
Have fun.
How?
More to the point...
this:
$url = 'http://php.net/manual/en/class.domelement.php';
$client = new Zend_Http_Client($url);
$response = $client->request();
$html = $response->getBody();
$dom = new Zend_Dom_Query($html);
$result = $dom->query('div.note');
Zend_Debug::dump($result);
gives me this:
object(Zend_Dom_Query_Result)#867 (7) {
["_count":protected] => NULL
["_cssQuery":protected] => string(8) "div.note"
["_document":protected] => object(DOMDocument)#79 (0) {
}
["_nodeList":protected] => object(DOMNodeList)#864 (0) {
}
["_position":protected] => int(0)
["_xpath":protected] => NULL
["_xpathQuery":protected] => string(33) "//div[contains(#class, ' note ')]"
}
And I cannot for the life of me figure out how to do anything with this.
I want to extract the various parts of the retrieved data (that being the div with the class "note" and any of the elements inside it... like the text and urls) but cannot get anything working.
Someone pointed me to the DOMElement class over at php.net but when I try using some of the methods mentioned, I can't get things to work. How would I grab a chunk of html from a page and go through it grabbing the various parts? How do I inspect this object I am getting back so I can at least figure out what is in it?
Hjälp?
The Iterator implementation of Zend_Dom_Query_Result returns a DOMElement object for each iteration:
foreach ($result as $element) {
var_dump($element instanceof DOMElement); // always true
}
From the $element variable, you can use any DOMElement method:
foreach ($result as $element) {
echo 'Element Id: '.$element->getAttribute('id').PHP_EOL;
if ($element->hasChildNodes()) {
echo 'Element has child nodes'.PHP_EOL;
}
$aNodes = $element->getElementsByTagName('a');
// etc
}
You can also access the document element, or you can use Zend_Dom_Query_Result to do so:
$document1 = $element->ownerDocument;
$document2 = $result->getDocument();
var_dump($document1 === $document2); // true
echo $document1->saveHTML();