I have the following script snippet. Originally I did not realize to use getElementById that I needed to include createDocumentType, but now I get the error listed above. What am I doing wrong here? Thanks in advance!
...
$result = curl_exec($ch); //contains some webpage i am grabbing remotely
$dom = new DOMDocument();
$dom->createDocumentType('html', '-//W3C//DTD HTML 4.01 Transitional//EN', 'http://www.w3.org/TR/html4/loose.dtd');
$elements = $dom->loadHTML($result);
$e = $elements->getElementById('1');
...
Edit: Additional note, I verified the DOM is correct on the remote page.
DOMDocument does not have a method named createDocumentType, as you can see in the Manual. The method belongs to the DOMImplemetation class. It is used like this (taken from the manual):
// Creates an instance of the DOMImplementation class
$imp = new DOMImplementation;
// Creates a DOMDocumentType instance
$dtd = $imp->createDocumentType('graph', '', 'graph.dtd');
// Creates a DOMDocument instance
$dom = $imp->createDocument("", "", $dtd);
Since you want to load HTML into the document, you don't need to specify a document type, since it is determined from the imported HTML. You just have to have some id attributes, or a DTD that identifies an other attribute as an id. This is part of the HTML file, not the parsing PHP code.
$dom = new DOMDocument();
$dom->loadHTML($result);
$element = $dom->getElementById('my_id');
will do the job.
Related
I'm trying to figure out how parse an html page to get a forms action value, the labels within the form tab as well as the input field names. I took at look at php.net Domdocument and it tells me to get a childnode but all that does is give me errors that it doesnt exist. I also tried doing print_r of the variable holding the html content and all that shows me is length=1. Can someone show me a few samples that i can use because php.net is confusing to follow.
<?php
$content = "some-html-source";
$content = preg_replace("/&(?!(?:apos|quot|[gl]t|amp);|#)/", '&', $content);
$dom = new DOMDocument;
$dom->preserveWhiteSpace = FALSE;
$dom->loadHTML($content);
$form = $dom->getElementsByTagName('form');
print_r($form);
I suggest using DomXPath instead of getElementsByTagName because it allows you to select attribute values directly and returns a DOMNodeList object just like getElementsByTagName. The # in #action indicates that we're selecting by attribute.
$doc = new DOMDocument();
$doc->loadHTML($content);
$xpath = new DomXPath($doc);
$action = $xpath->query('//form/#action')->item(0);
var_dump($action);
Similarly, to get the first input
$action = $xpath->query('//form/input')->item(0);
To get all input fields
for($i=0;$i<$xpath->query('//form/input')->length;$i++) {
$label = $xpath->query('//form/input')->item($i);
var_dump($label);
}
If you're not familiar with XPath, I recommend viewing these examples.
<?xml version="1.0" encoding="UTF-8"?>
<AddProduct>
<auth><id>vendor123</id><auth_code>abc123</auth_code></auth>
</AddProduct>
What am I doing wrong to get : Fatal error: Call to undefined method DOMNodeList::getElementsByTagName()
$xml = $_GET['xmlRequest'];
$dom = new DOMDocument();
#$dom->loadXML($xml);
$xpath = new DOMXPath($dom);
$auth = $xpath->query('*/auth');
$id = $auth->getElementsByTagName('id')->item(0)->nodeValue;
$code = $auth->getElementsByTagName('auth_code')->item(0)->nodeValue;
You could retrieve the data (in the XML you posted) you want using XPath only:
$id = $xpath->query('//auth/id')->item(0)->nodeValue;
$code = $xpath->query('//auth/auth_code')->item(0)->nodeValue;
You are also calling getElementsByTagName() on $auth (DOMXPath), as #Ohgodwhy pointed out in the comments, which is causing the error. If you want to use it, you should call it on $dom.
Your XPath expression returns the auth child of the current (context) node. Unless your XML file is different, it's clearer to use one of:
/*/auth # returns auth nodes two levels below root
/AddProduct/auth # returns auth nodes in below /AddProduct
//auth # returns all auth nodes
This is what I came up with after reviewing php's documentation (http://us1.php.net/manual/en/class.domdocument.php, http://us1.php.net/manual/en/domdocument.loadxml.php, http://us3.php.net/manual/en/domxpath.query.php, http://us3.php.net/domxpath)
$dom = new DOMDocument();
$dom->loadXML($xml);
$id = $dom->getElementsByTagName("id")->item(0)->nodeValue;
$code = $dom->getElementsByTagName("auth_code")->item(0)->nodeValue;
As helderdarocha and Ohgodwhy pointed out, the getElementByTagName is a DOMDocument method not a DOMXPath method. I like helderdarocha's solution that only uses XPath, the solution I posted accomplishes the same thing but only uses the DOMDocument.
I am using the function below, but not sure about it is always stable/secure... Is it?
When and who is stable/secure to "reuse parts of the DOMXpath preparing procedures"?
To simlify the use of the XPath query() method we can adopt a function that memorizes the last calls with static variables,
function DOMXpath_reuser($file) {
static $doc=NULL;
static $docName='';
static $xp=NULL;
if (!$doc)
$doc = new DOMDocument();
if ($file!=$docName) {
$doc->loadHTMLFile($file);
$xp = NULL;
}
if (!$xp)
$xp = new DOMXpath($doc);
return $xp; // ??RETURNED VALUES ARE ALWAYS STABLE??
}
The present question is similar to this other one about XSLTProcessor reuse.
In both questions the problem can be generalized for any language or framework that use LibXML2 as DomDocument implementation.
There are another related question: How to "refresh" DOMDocument instances of LibXML2?
Illustrating
The reuse is very commom (examples):
$f = "my_XML_file.xml";
$elements = DOMXpath_reuser($f)->query("//*[#id]");
// use elements to get information
$elements = DOMXpath_reuser($f)->("/html/body/div[1]");
// use elements to get information
But, if you do something like removeChild, replaceChild, etc. (example),
$div = DOMXpath_reuser($f)->query("/html/body/div[1]")->item(0); //STABLE
$div->parentNode->removeChild($div); // CHANGES DOM
$elements = DOMXpath_reuser($f)->query("//div[#id]"); // INSTABLE! !!
extrange things can be occur, and the queries not works as expected!!
When (what DOMDocument methods affect XPath?)
Why we can not use something like normalizeDocument to "refresh DOM" (exist?)?
Only a "new DOMXpath($doc);" is allways secure? need to reload $doc also?
DOMXpath is affected by the load*() methods on DOMDocument. After loading a new xml or html, you need to recreate the DOMXpath instance:
$xml = '<xml/>';
$dom = new DOMDocument();
$dom->loadXml($xml);
$xpath = new DOMXpath($dom);
var_dump($xpath->document === $dom); // bool(true)
$dom->loadXml($xml);
var_dump($xpath->document === $dom); // bool(false)
In DOMXpath_reuser() you store a static variable and recreate the xpath depending on the file name. If you want to reuse an Xpath object, suggest extending DOMDocument. This way you only need pass the $dom variable around. It would work with a stored xml file as well with xml string or a document your are creating.
The following class extends DOMDocument with an method xpath() that always returns a valid DOMXpath instance for it. It stores and registers the namespaces, too:
class MyDOMDocument
extends DOMDocument {
private $_xpath = NULL;
private $_namespaces = array();
public function xpath() {
// if the xpath instance is missing or not attached to the document
if (is_null($this->_xpath) || $this->_xpath->document != $this) {
// create a new one
$this->_xpath = new DOMXpath($this);
// and register the namespaces for it
foreach ($this->_namespaces as $prefix => $namespace) {
$this->_xpath->registerNamespace($prefix, $namespace);
}
}
return $this->_xpath;
}
public function registerNamespaces(array $namespaces) {
$this->_namespaces = array_merge($this->_namespaces, $namespaces);
if (isset($this->_xpath)) {
foreach ($namespaces as $prefix => $namespace) {
$this->_xpath->registerNamespace($prefix, $namespace);
}
}
}
}
$xml = <<<'ATOM'
<feed xmlns="http://www.w3.org/2005/Atom">
<title>Test</title>
</feed>
ATOM;
$dom = new MyDOMDocument();
$dom->registerNamespaces(
array(
'atom' => 'http://www.w3.org/2005/Atom'
)
);
$dom->loadXml($xml);
// created, first access
var_dump($dom->xpath()->evaluate('string(/atom:feed/atom:title)', NULL, FALSE));
$dom->loadXml($xml);
// recreated, connection was lost
var_dump($dom->xpath()->evaluate('string(/atom:feed/atom:title)', NULL, FALSE));
The DOMXpath class (instead of XSLTProcessor in your another question) use reference to given DOMDocument object in contructor. DOMXpath create libxml context object based on given DOMDocument and save it to internal class data. Besides libxml context its saves references to originalDOMDocument` given in contructor arguments.
What that means:
Part of sample from ThomasWeinert answer:
var_dump($xpath->document === $dom); // bool(true)
$dom->loadXml($xml);
var_dump($xpath->document === $dom); // bool(false)
gives false after load becouse of $dom already holds pointer to new libxml data but DOMXpath holds libxml context for $dom before load and pointer to real document after load.
Now about query works
If it should return XPATH_NODESET (as in your case) its make a node copy - node by node iterating throw detected node set(\ext\dom\xpath.c from 468 line). Copy but with original document node as parent. Its means that you can modify result but this gone away you XPath and DOMDocument connection.
XPath results provide a parentNode memeber that knows their origin:
for attribute values, parentNode returns the element that carries them. An example is //foo/#attribute, where the parent would be a foo Element.
for the text() function (as in //text()), it returns the element that contains the text or tail that was returned.
note that parentNode may not always return an element. For example, the XPath functions string() and concat() will construct strings that do not have an origin. For them, parentNode will return None.
So,
There is no any reasons to cache XPath. It do not anything besides xmlXPathNewContext (just allocate lightweight internal struct).
Each time your modify your DOMDocument (removeChild, replaceChild, etc.) your should recreate XPath.
We can not use something like normalizeDocument to "refresh DOM" because of it change internal document structure and invalidate xmlXPathNewContext created in Xpath constructor.
Only "new DOMXpath($doc);" is allways secure? Yes, if you do not change $doc between Xpath usage. Need to reload $doc also - no, because of it invalidated previously created xmlXPathNewContext.
(this is not a real answer, but a consolidation of comments and answers posted here and related questions)
This new version of the question's DOMXpath_reuser function contains the #ThomasWeinert suggestion (for avoid DOM changes by external re-load) and an option $enforceRefresh to workaround the problem of instability (as related question shows the programmer must detect when).
function DOMXpath_reuser_v2($file, $enforceRefresh=0) { //changed here
static $doc=NULL;
static $docName='';
static $xp=NULL;
if (!$doc)
$doc = new DOMDocument();
if ( $file!=$docName || ($xp && $doc !== $xp->document) ) { // changed here
$doc->load($file);
$xp = NULL;
} elseif ($enforceRefresh==2) { // add this new refresh mode
$doc->loadXML($doc->saveXML());
$xp = NULL;
}
if (!$xp || $enforceRefresh==1) //changed here
$xp = new DOMXpath($doc);
return $xp;
}
When must to use $enforceRefresh=1 ?
... perhaps an open problem, only little tips and clues...
when DOM submited to setAttribute, removeChild, replaceChild, etc.
...? more cases?
When must to use $enforceRefresh=2 ?
... perhaps an open problem, only little tips and clues...
when DOM was subject to indexes inconsistences, etc. See this question/solution.
...? more cases?
I am having a strange behavior in my script. That has me confused
Script 1.
$dom = new DOMDocument();
$dom->loadHTMLFile("html/signinform.html");//loads file here
$form = $dom->getElementsByTagName("form")->item(0);
$div = $dom->createElement("div");
$dom->appendChild($div)->appendChild($form);
echo $dom->saveHTML();
Script 2.
$dom = new DOMDocument();
$div = $dom->createElement("div");
$dom->loadHTMLFile("html/signinform.html");//loads file here
$form = $dom->getElementsByTagName("form")->item(0);
$dom->appendChild($div)->appendChild($form);
echo $dom->saveHTML();
Script 1 works without problem. It shows the form. However Script 2 throws the following error: Fatal error: Uncaught exception 'DOMException' with message 'Wrong Document Error' in C:\Users
Could someone explain to me why the mere changing of position of the loadHTMLFile function results in such error? Thanks
You have added an element to the DOM (div) and then attempted to load a file to be parsed and its DOM structure used.
Load the file first if you intend to use one.
For DOM manipulation you do not need to insert an already existing element so doing something like this: $dom->appendChild($form) only reinserts the same form element, when you pull an element using $dom->getElementsByTag("form")->item(0) it becomes it's own DOM object which you can reference directly and append to. A proper example would be:
$dom = new DOMDocument();
$dom->loadHTMLFile("assets/dom_document-form.html");
$div = $dom->createElement("div");
$form = $dom->getElementsByTagName("form")->item(0);
$form->appendChild($div);
echo $dom->saveHTML();
One should append directly to the object they pulled from the DOM instead and load the document first.
To help aid your initial questions too:
Append directly to element that you pulled as it references the object.
new DOMDocument can be used to create multiple documents.
using DOMDocument::createElement before loadHTMLFile creates 2 DOMDocuments.
Using DomDocument::createDocumentFragment acts the same and creates it's own DOM.
If you would like to keep your code the same and create two DomDocuments then you should use DomDocument::importNode, an example of this would be:
$dom = new DOMDocument();
$div = $dom->createElement("div");
$dom->loadHTMLFile("assets/dom_document-form.html");
$node = $dom->importNode($div);
$form = $dom->getElementsByTagName("form")->item(0);
$form->appendChild($node);
echo $dom->saveHTML();
A little new to PHP parsing here, but I can't seem to get PHP's DOMDocument to return what is clearly an identifiable node. The HTML loaded will come from the 'net so can't necessarily guarantee XML compliance, but I try the following:
<?php
header("Content-Type: text/plain");
$html = '<html><body>Hello <b id="bid">World</b>.</body></html>';
$dom = new DOMDocument;
$dom->preserveWhiteSpace = false;
$dom->validateOnParse = true;
/*** load the html into the object ***/
$dom->loadHTML($html);
var_dump($dom);
$belement = $dom->getElementById("bid");
var_dump($belement);
?>
Though I receive no error, I only receive the following as output:
object(DOMDocument)#1 (0) {
}
NULL
Should I not be able to look up the <b> tag as it does indeed have an id?
The Manual explains why:
For this function to work, you will need either to set some ID attributes with DOMElement->setIdAttribute() or a DTD which defines an attribute to be of type ID. In the later case, you will need to validate your document with DOMDocument->validate() or DOMDocument->validateOnParse before using this function.
By all means, go for valid HTML & provide a DTD.
Quick fixes:
Call $dom->validate(); and put up with the errors (or fix them), afterwards you can use $dom->getElementById(), regardless of the errors for some reason.
Use XPath if you don't feel like validing: $x = new DOMXPath($dom); $el = $x->query("//*[#id='bid']")->item(0);
Come to think of it: if you just set validateOnParse to true before loading the HTML, if would also work ;P
.
$dom = new DOMDocument();
$html ='<html>
<body>Hello <b id="bid">World</b>.</body>
</html>';
$dom->validateOnParse = true; //<!-- this first
$dom->loadHTML($html); //'cause 'load' == 'parse
$dom->preserveWhiteSpace = false;
$belement = $dom->getElementById("bid");
echo $belement->nodeValue;
Outputs 'World' here.
Well, you should check if $dom->loadHTML($html); returns true (success) and I would try
var_dump($belement->nodeValue);
for output to get a clue what might be wrong.
EDIT:
http://www.php-editors.com/php_manual/function.domdocument-get-element-by-id.html - it seems that DOMDocument uses XPath internally.
Example:
$xpath = xpath_new_context($dom);
var_dump(xpath_eval_expression($xpath, "//*[#ID = 'YOURIDGOESHERE']"));