I want to add multiple attributes to a child in SimpleXMLElement so it will look like this:
<data>
<photo>
<file size="3309519" size="JPG">P1270081</file>
</photo>
</data>
As it is right now in my code, I can only add one attribute per child as the code below shows.
$xml = new SimpleXMLElement('<data/>');
$photo = $xml->addChild('photo');
$photo->addChild('file', 'P1270081')->addAttribute('size', '3309519');
$photo->addChild('uploaded', '2013-09-01 15:23:10')->addAttribute('by', 'edgren');
If I change the third line to $photo->addChild('file', 'P1270081')->addAttribute('size', '3309519')->addAttribute('type', 'JPG'); I'm getting this error message:
Fatal error: Call to a member function addAttribute() on a non-object in ...
I am new to creating XML files on the fly with SimpleXMLElement so I don't know how I shall fix this issue. What should I do to fix it?
addAttribute returns void. If you want to add more attributes you have to something like this:
$file = $photo->addChild('file', 'P1270081');
$file->addAttribute('size', '3309519');
$file->addAttribute('type', 'JPG');
$photo->addChild(..) // returns the created XML component .. and you can chain a action(one) directly one it.
But addAttribute(..) returns nothing .. so you get an error if you try to chain event after it.
$photo = $xml->addChild('photo');
$photo->addChild('file', 'P1270081')->addAttribute('size', '3309519');
$theNewChild = $photo->addChild('uploaded', '2013-09-01 15:23:10')
$theNewChild ->addAttribute('by', 'edgren');
$theNewChild ->addAttribute('type', 'JPG');
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>
<ret2:formFields xsi:type="ret1:FormFieldsType" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
I want to know how can I do the following? The namespace of children are getting changed by setting the xsi:type attribute.
My Code:
$ret2FormFields = $ret2FileBody->addChild('ret2:formFields', null, 'http://www.w3.org/2001/XMLSchema-instance');
$ret2FormFields->addAttribute("xsi:type", "ret1:FormFieldsType", 'http://www.w3.org/2001/XMLSchema-instance');
$ret2FormFields->addChild('ret1:isReverseReplace', false);
$ret2FormFields->addChild('ret1:payDayDate', '2018-04-10'); /** #todo date will be dynamic */
Expected XML:
<ret2:formFields xsi:type="ret1:FormFieldsType" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<ret1:isReverseReplace>false</ret1:isReverseReplace>
<ret1:payDayDate>2018-04-10</ret1:payDayDate>
</ret2:formFields>
My Incorrect XML:
<ret2:formFields xmlns:ret2="http://www.w3.org/2001/XMLSchema-instance" ret2:type="ret1:FormFieldsType">
<ret1:isReverseReplace>false</ret1:isReverseReplace>
<ret1:payDayDate>2018-04-10</ret1:payDayDate>
</ret2:formFields>
I am stuck how to change the children namespaces ret1 without changing the parent namespace ret2
You are passing the wrong namespace to addChild here:
addChild('ret2:formFields', null, 'http://www.w3.org/2001/XMLSchema-instance');
This tells SimpleXML that you want the element to have prefix ret2 and namespace URI http://www.w3.org/2001/XMLSchema-instance, so it will generate:
<ret2:formFields xmlns:ret2="http://www.w3.org/2001/XMLSchema-instance">
You don't show in your example what the prefix ret2 has been assigned elsewhere in the document, but that's what you need to provide, e.g.:
addChild('ret2:formFields', null, 'http://example.org/mynamespaces/ret2');
I am creating an element with this code. Setting the ID as it should be set. (Do I need the validateonparse?):
$sectionContainer = $dom->createElement('div', $section);
$sectionContainer->setAttribute("id", $section);
$sectionContainer->setIdAttribute("id", TRUE);
$dom->validateOnParse = true;
$divup->parentNode->insertBefore($sectionContainer, $divup);
echo 'avant'."</br>";
echo $section;
print_r($dom->getElementById($section)->getAttribute('id'));
echo 'apres'."</br>";
But I get this:
Fatal error: Uncaught Error: Call to a member function getAttribute() on null
So it can't locate the element I just created, why?
Hey_ there,
the problem is not with $dom->validateOnParse = true;.
Your error comes from this line of code - $sectionContainer->setIdAttribute("id", TRUE);.
If you want to set id attribute of DOMElement like <div id="myId"></div> you only need $domElement->setAttribute("id", "myId"); which you already have.
DOMElement::setIdAttribute is actually indicating that this attribute is unique identifier which has nothing to do with the id="myId" attribute
(PHP 5, PHP 7)
DOMElement::setIdAttribute — Declares the attribute
specified by name to be of type ID
Description
public void DOMElement::setIdAttribute ( string $name , bool $isId )
Declares the attribute name to be of type ID.
Well it is not very clear what is type ID, but I can say that it is something different from id attribute of html element.
Check demo HERE
I'm trying to create a rss feed and one element is
<content:encoded></content:encoded>
But, when i use this code:
$item->addChild('content:encoded',htmlspecialchars($itemdata->description));
I get this as a result:
<encoded> .................. </encoded>
I don't get the content namespace, and how would I be able to?
As you can see in the documentation, you need to provide the namespace URI as 3rd argument of addChild() to create element in namespace correctly :
$item->addChild(
'content:encoded',
htmlspecialchars($itemdata->description),
'namespace-URI-for-content-prefix-here'
);
Quick demo :
$raw = '<root xmlns:content="mynamespace"></root>';
$item = new SimpleXMLElement($raw);
$item->addChild(
'content:encoded',
'foo bar baz',
'mynamespace'
);
echo $item->asXML();
eval.in demo
output :
<?xml version="1.0"?>
<root xmlns:content="mynamespace"><content:encoded>foo bar baz</content:encoded></root>
I'm familiar with the DOMDocument::importNode method for importing a tree of nodes from some other document element.
However, what I was wondering is if I can automatically change the namespace prefix on a tree of nodes as I import them, that is, specify a new prefix for all nodes of that namespace.
Say the nodes, in their existing document, all have names like "name", "identity", and so on. When importing them into my new document they will be alongside other namespaces, so I'd like them to appear as "nicnames:name", "nicnames:identity" and so on. I'd like to be able to change this prefix programmatically so that in another context I may be able to import them as, for instance, "myprefix:name", "myprefix:identity" depending on the document they're imported into.
Edit: as per the explanation in my answer, I figured out I don't actually need to do this. I was misunderstanding namespaces in XML.
You probably have to write your own import code then. E.g.
function importNS(DOMNode $target, DOMNode $source, $fnImportElement, $fnImportAttribute) {
switch($source->nodeType) {
case XML_ELEMENT_NODE:
// invoke the callback that creates the new DOMElement node
$newNode = $fnImportElement($target->ownerDocument, $source);
if ( !is_null($newNode) && !is_null($source->attributes) ) {
foreach( $source->attributes as $attr) {
importNS($newNode, $attr, $fnImportElement, $fnImportAttribute);
}
}
break;
case XML_ATTRIBUTE_NODE:
// invoke the callback that creates the new DOMAttribute node
$newNode = $fnImportAttribute($target->ownerDocument, $source);
break;
default:
// flat copy
$newNode = $target->ownerDocument->importNode($source);
}
if ( !is_null($newNode) ) {
// import all child nodes
if ( !is_null($source->childNodes) ) {
foreach( $source->childNodes as $c) {
importNS($newNode, $c, $fnImportElement, $fnImportAttribute);
}
}
$target->appendChild($newNode);
}
}
$target = new DOMDocument;
$target->loadxml('<foo xmlns:myprefix="myprefixUri"></foo>');
$source = new DOMDocument;
$source->loadxml('<a>
<b x="123">...</b>
</a>');
$fnImportElement = function(DOMDocument $newOwnerDoc, DOMElement $e) {
return $newOwnerDoc->createElement('myprefix:'.$e->localName);
};
$fnImportAttribute = function(DOMDocument $newOwnerDoc, DOMAttr $a) {
// could use namespace here, too....
return $newOwnerDoc->createAttribute($a->name);
};
importNS($target->documentElement, $source->documentElement, $fnImportElement, $fnImportAttribute);
echo $target->savexml();
prints
<?xml version="1.0"?>
<foo xmlns:myprefix="myprefixUri"><myprefix:a>
<myprefix:b x="123">...</myprefix:b>
</myprefix:a></foo>
I discovered I'd misunderstood XML namespaces. They are actually much better than I thought they were.
I'd thought that each XML namespace used in a single document had to have a different namespace prefix. This is not true.
You can use different namespaces throughout your document even without namespace prefixes, just by including the xmlns attribute where appropriate, and that xmlns attribute only has effect for that element and its descendants, overriding the namespace for that prefix which may have been set higher up the tree.
For example, to have one namespace within another, you don't have to do:
<record xmlns="namespace1">
<person:surname xmlns:person="namespace2">Smith</person:surname>
</record>
You can just do
<record xmlns="namespace1">
<surname xmlns="namespace2">Smith</person>
</record>
Namespace prefixes are a good shortcut in certain situations, but not necessary when just including one document inside another of a different namespace.