问题
解析器抱怨你的文本在元素标签中包含命名空间,更具体地说是标签<o:p> 上的前缀(其中o: 是前缀)。好像是some kind of formatting for Word。
重现问题
为了重现这个问题,我不得不挖掘一下,因为不是 PHPWord 引发了异常,而是 PHPWord 正在使用的DOMDocument。下面的代码使用的是 PHPWord 正在使用的same parsing method,并且应该输出有关代码的所有警告和通知。
# Make sure to display all errors
ini_set("display_errors", "1");
error_reporting(E_ALL);
$html = '<o:p>Foo <o:b>Bar</o:b></o:p>';
# Set up and parse the code
$doc = new DOMDocument();
$doc->loadXML($html); # This is the line that's causing the warning.
# Print it back
echo $doc->saveXML();
分析
对于格式良好的 HTML 结构,可以在声明中包含命名空间,从而告诉解析器这些前缀实际上是什么。但由于它似乎只是要被解析的 HTML 代码的一部分,所以这是不可能的。
可以提供DOMXPath with the namespace,以便PHPWord 可以使用它。不幸的是,API 中的DOMXPath isn't public 因此不可能。
相反,似乎最好的方法是从标签中去除前缀,并让警告消失。
编辑 2018-10-04:我后来发现了一种将前缀保留在标签中并且仍然使错误消失的方法,但是执行不是最佳的。如果有人可以提出更好的解决方案,请随时编辑我的帖子或发表评论。
解决方案
根据分析,解决方案是去掉前缀,反过来我们必须对代码进行预解析。 Since PHPWord is using DOMDocument,我们也可以使用它,并确保我们不需要安装任何(额外的)依赖项。
PHPWord 正在用loadXML 解析 HTML,这是一个抱怨格式化的函数。在这种方法中可以抑制错误消息,我们将在两种解决方案中都必须这样做。这是由passing an additional parameter 到loadXML 和loadHTML 函数中完成的。
方案一:预解析为 XML 并去掉前缀
第一种方法将 html 代码解析为 XML 并递归遍历树并删除标记名称上出现的任何前缀。
我创建了一个可以解决这个问题的类。
class TagPrefixFixer {
/**
* @desc Removes all prefixes from tags
* @param string $xml The XML code to replace against.
* @return string The XML code with no prefixes in the tags.
*/
public static function Clean(string $xml) {
$doc = new DOMDocument();
/* Load the XML */
$doc->loadXML($xml,
LIBXML_HTML_NOIMPLIED | # Make sure no extra BODY
LIBXML_HTML_NODEFDTD | # or DOCTYPE is created
LIBXML_NOERROR | # Suppress any errors
LIBXML_NOWARNING # or warnings about prefixes.
);
/* Run the code */
self::removeTagPrefixes($doc);
/* Return only the XML */
return $doc->saveXML();
}
private static function removeTagPrefixes(DOMNode $domNode) {
/* Iterate over each child */
foreach ($domNode->childNodes as $node) {
/* Make sure the element is renameable and has children */
if ($node->nodeType === 1) {
/* Iterate recursively over the children.
* This is done before the renaming on purpose.
* If we rename this element, then the children, the element
* would need to be moved a lot more times due to how
* renameNode works. */
if($node->hasChildNodes()) {
self::removeTagPrefixes($node);
}
/* Check if the tag contains a ':' */
if (strpos($node->tagName, ':') !== false) {
print $node->tagName;
/* Get the last part of the tag name */
$parts = explode(':', $node->tagName);
$newTagName = end($parts);
/* Change the name of the tag */
self::renameNode($node, $newTagName);
}
}
}
}
private static function renameNode($node, $newName) {
/* Create a new node with the new name */
$newNode = $node->ownerDocument->createElement($newName);
/* Copy over every attribute from the old node to the new one */
foreach ($node->attributes as $attribute) {
$newNode->setAttribute($attribute->nodeName, $attribute->nodeValue);
}
/* Copy over every child node to the new node */
while ($node->firstChild) {
$newNode->appendChild($node->firstChild);
}
/* Replace the old node with the new one */
$node->parentNode->replaceChild($newNode, $node);
}
}
要使用代码,只需调用TagPrefixFixer::Clean 函数。
$xml = '<o:p>Foo <o:b>Bar</o:b></o:p>';
print TagPrefixFixer::Clean($xml);
输出
<?xml version="1.0"?>
<p>Foo <b>Bar</b></p>
解决方案 2:预解析为 HTML
我注意到,如果您使用loadHTML 而不是loadXML,那么PHPWord is using 会在将 HTML 加载到类中时自行删除前缀。
这段代码明显更短。
function cleanHTML($html) {
$doc = new DOMDocument();
/* Load the HTML */
$doc->loadHTML($html,
LIBXML_HTML_NOIMPLIED | # Make sure no extra BODY
LIBXML_HTML_NODEFDTD | # or DOCTYPE is created
LIBXML_NOERROR | # Suppress any errors
LIBXML_NOWARNING # or warnings about prefixes.
);
/* Immediately save the HTML and return it. */
return $doc->saveHTML();
}
要使用此代码,只需调用cleanHTML 函数
$html = '<o:p>Foo <o:b>Bar</o:b></o:p>';
print cleanHTML($html);
输出
<p>Foo <b>Bar</b></p>
解决方案 3:保留前缀并添加命名空间
在将数据输入解析器之前,我尝试使用给定的Microsoft Office namespaces 包装代码,这也将解决问题。具有讽刺意味的是,我还没有找到一种方法来使用 DOMDocument 解析器添加命名空间而不实际引发原始警告。所以 - 这个解决方案的执行有点笨拙,我不建议使用它,而是自己构建。但你明白了:
function addNamespaces($xml) {
$root = '<w:wordDocument
xmlns:w="http://schemas.microsoft.com/office/word/2003/wordml"
xmlns:wx="http://schemas.microsoft.com/office/word/2003/auxHint"
xmlns:o="urn:schemas-microsoft-com:office:office">';
$root .= $xml;
$root .= '</w:wordDocument>';
return $root;
}
要使用此代码,只需调用 addNamespaces 函数
$xml = '<o:p>Foo <o:b>Bar</o:b></o:p>';
print addNamespaces($xml);
输出
<w:wordDocument
xmlns:w="http://schemas.microsoft.com/office/word/2003/wordml"
xmlns:wx="http://schemas.microsoft.com/office/word/2003/auxHint"
xmlns:o="urn:schemas-microsoft-com:office:office">
<o:p>Foo <o:b>Bar</o:b></o:p>
</w:wordDocument>
然后可以将此代码提供给 PHPWord 函数 addHtml 而不会引起任何警告。
可选解决方案(已弃用)
在之前的回复中,这些是作为(可选)解决方案提供的,但为了解决问题,我将让它们出现在下面。请记住,这些都不推荐,应谨慎使用。
关闭警告
由于它“只是”一个警告而不是致命的停止异常,因此您可以关闭警告。您可以通过在脚本顶部包含此代码来执行此操作。然而,这仍然会减慢您的应用程序,最好的方法始终是确保没有警告或错误。
// Show the default reporting except from warnings
error_reporting(E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED & ~E_WARNING);
这些设置来自default reporting level。
使用正则表达式
在将regex on your text 保存到数据库之前,或者在获取它以供此函数使用之后,(可能)可以删除(大部分)带有regex on your text 的命名空间。由于它已经存储在数据库中,最好在从数据库中获取它之后使用下面的代码。正则表达式可能会遗漏一些事件,或者在最坏的情况下会弄乱 HTML。
正则表达式:
$text_after = preg_replace('/[a-zA-Z]+:([a-zA-Z]+[=>])/', '$1', $text_before);
示例:
$text = '<o:p>Foo <o:b>Bar</o:b></o:p>';
$text = preg_replace('/[a-zA-Z]+:([a-zA-Z]+[=>])/', '$1', $text);
echo $text; // Outputs '<p>Foo <b>Bar</b></p>'