Skip to content

Commit

Permalink
- Moves actual sorting logic to separate class.
Browse files Browse the repository at this point in the history
 - Adds some unit tests for the DomDocumentSorter and XmlSorter classes.
  • Loading branch information
Potherca committed Dec 20, 2013
1 parent f98586c commit 2ccc90c
Show file tree
Hide file tree
Showing 5 changed files with 496 additions and 92 deletions.
108 changes: 108 additions & 0 deletions lib/class.DomDocumentSorter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<?php

class DomDocumentSorter
{
/**
* @param DOMDocument $p_oDocument
*
* @return DOMDocument
*/
static public function sort(DOMDocument $p_oDocument)
{
$oDocument = clone $p_oDocument;

$oSorter = new self();
$oSorter->sortDomDocument($oDocument);

return $oDocument;
}

/**
* @param \DOMDocument $p_oDomDocument
*/
public function sortDomDocument(DOMDocument $p_oDomDocument)
{
foreach ($p_oDomDocument->childNodes as $t_oDomElement) {
$this->sortDomElement($t_oDomElement, $p_oDomDocument);
}
}

/**
* Replace childNodes by sorted childNodes
*
* @param DOMElement $p_oDomElement
* @param DOMDocument $p_oDocument
*/
protected function sortDomElement(DOMElement $p_oDomElement)
{
$this->sortAttributes($p_oDomElement);

if ($p_oDomElement->hasChildNodes()) {
$oChildNodes = $p_oDomElement->childNodes;
$aChildren = array();
foreach ($oChildNodes as $t_sIndex => $t_oDomElement) {
if ($t_oDomElement instanceof DOMText) {
// Remove empty whitespace
$sText = trim($t_oDomElement->textContent);
if(empty($sText)){
$p_oDomElement->removeChild($t_oDomElement);
}
} else if($t_oDomElement instanceof DOMElement) {
$aChildren[] = clone $t_oDomElement;
$p_oDomElement->removeChild($t_oDomElement);
} else {
var_dump(get_class($t_oDomElement));
}
unset($t_oDomElement);
}

// @FIXME: Things seem to go wrong round about here.
// Either the sorting is wrong or text-nodes muddle things up or the
// original nodes do not get properly removed and messes up things
// when replacement get appended.
$bSorted = usort($aChildren, function (DOMElement $p_oLeft, DOMElement $p_oRight) {
return strcasecmp($p_oRight->tagName, $p_oLeft->tagName);
});

/*
* We can just add the first child right away
* Every child after that we compare to that node
*
* If the child's tag name is alphabetically lower we move down
* the chain (if there are any next sibling) until we find a sibling
* who's name is lower than the current one
*
* The same logic applies up the chang for a higher sorting name
*/

foreach ($aChildren as $t_oDomElement) {
$this->sortDomElement($t_oDomElement);
$p_oDomElement->appendChild(clone $t_oDomElement);
}

}
}

/**
* replace attributes with sorted attributes
* @param DOMElement $p_oDomNode
*/
protected function sortAttributes(DOMElement $p_oDomNode)
{
$oDOMNamedNodeMap = $p_oDomNode->attributes;
if( $oDOMNamedNodeMap instanceof DOMNamedNodeMap){
/* Remove all attributes and place them back in order */
$aAttributes = array();
foreach ($oDOMNamedNodeMap as $t_sNodeName => $t_oAttribute) {
/** @var DOMAttr $t_oAttribute */
$aAttributes[$t_oAttribute->name] = $t_oAttribute->value;
$p_oDomNode->removeAttribute($t_sNodeName);
}

ksort($aAttributes);
foreach ($aAttributes as $t_sAttributeName => $t_sNodeValue) {
$p_oDomNode->setAttribute($t_sAttributeName, $t_sNodeValue);
}
}
}
}
119 changes: 27 additions & 92 deletions lib/class.XmlSorter.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@ class XmlSorter
protected $m_aXmlErrors = array();
protected $m_sOriginalXml;

/**
* @return mixed
*/
public function getXml()
{
return $this->m_sOriginalXml;
}

/**
* @param mixed $p_sXml
*/
public function setXml($p_sXml)
{
$this->m_sOriginalXml = (string) $p_sXml;
}

/**
* @return array
*/
Expand All @@ -20,117 +36,36 @@ static public function forFile($p_sXmlPath)
} else {
$sXml = file_get_contents($p_sXmlPath);
$oSorter = new self($sXml);
$oSorter->setXml($sXml);

return $oSorter;
}
}

public function __construct($p_sXml)
public function __construct($p_sXml='')
{
$this->m_sOriginalXml = $p_sXml;
}

public function formatXml($p_sXml)
{
$mResult = false;

$oDocument = $this->loadXml($p_sXml);

if ($oDocument instanceof DOMDocument) {
$mResult = $oDocument->saveXML();
}

return $mResult;
}

public function sortXml()
{
$sXml = $this->m_sOriginalXml;
$aSortedXml = $this->xmlStringToSortedArray($sXml);
$sXml = $this->arrayToXml($aSortedXml);
$sFormatXml = $this->formatXml($sXml);

return $sFormatXml;
}

protected function arrayToXml(array $p_aSubject)
public function sortXml($p_sXml='')
{
$sRoot = array_keys($p_aSubject);
$sRoot = $sRoot[0];
//@FIXME: Get name of original root node and use that instead of "root"
$p_oXml = new SimpleXMLElement('<?xml version="1.0"?><' . $sRoot . '></' . $sRoot . '>');
$oXml = $this->arrayToXmlRecursive($p_aSubject, $p_oXml);

return $oXml->asXml();
}

/**
* @url http://pastebin.com/pYuXQWee
* @see http://stackoverflow.com/a/5965940/153049
*/
protected function arrayToXmlRecursive($p_aSubject, SimpleXMLElement $p_oXml)
{
foreach ($p_aSubject as $t_sKey => $t_mValue) {
// @FIXME: There's a bug here with attributes set on nodes with no content
$t_sKey = (is_numeric($t_sKey) ? 'item':'') . $t_sKey; // Uncomment if you need to fix numeric keys in the array
if (is_array($t_mValue)) {
if ($t_sKey === '@attributes') {
ksort($t_mValue);
foreach ($t_mValue as $t_sAttributeName => $t_sAttributeValue) {
$p_oXml->addAttribute(
$t_sAttributeName,
$t_sAttributeValue
);
}
} else {
$oSubNode = $p_oXml->addChild($t_sKey);
$this->arrayToXmlRecursive($t_mValue, $oSubNode);
}
} else {
$p_oXml->addChild($t_sKey, $t_mValue);
}
}

return $p_oXml;
}

protected function xmlToArray($p_sXml)
{
$oXml = simplexml_load_string($p_sXml);
$oJson = '{"' . $oXml->getName() . '" : ' . json_encode($oXml) .'}';
$aXML = json_decode($oJson, true);

return $aXML;
}

protected function arraySortByKeysRecursive($p_aSubject)
{
ksort($p_aSubject);
foreach ($p_aSubject as $t_sKey => $t_mValue) {
if (is_array($t_mValue)) {
$p_aSubject[$t_sKey] = $this->arraySortByKeysRecursive(
$t_mValue
);
}
if (empty($p_sXml)) {
$sXml = $this->getXml();
} else {
$sXml = $p_sXml;
}

return $p_aSubject;
}
$oDocument = $this->xmlStringToDomDocument($sXml);

protected function xmlStringToSortedArray($p_sXmlString)
{
$aXml = $this->xmlToArray($p_sXmlString);
$aXml = $this->arraySortByKeysRecursive($aXml);

return $aXml;
return DomDocumentSorter::sort($oDocument)->saveXML();
}

/**
* @param $sXml
*
* @return array
* @return DOMDocument
*/
protected function loadXml($sXml)
protected function xmlStringToDomDocument($sXml)
{
$oDocument = new DOMDocument('1.0');
$oDocument->preserveWhiteSpace = false;
Expand Down
28 changes: 28 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
backupGlobals="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
cacheTokens="true"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
forceCoversAnnotation="true"
mapTestClassNameToCoveredClassName="true"
printerClass="PHPUnit_TextUI_ResultPrinter"
processIsolation="false"
stopOnError="false"
stopOnFailure="false"
stopOnIncomplete="false"
stopOnSkipped="false"
strict="true"
testSuiteLoaderClass="PHPUnit_Runner_StandardTestSuiteLoader"
verbose="true"
>
<testsuites>
<testsuite name="XmlDiff Test Suite">
<directory suffix=".php">./tests/</directory>
</testsuite>
</testsuites>
</phpunit>
Loading

0 comments on commit 2ccc90c

Please sign in to comment.