Reorder a nested HTML list in PHP

Recently I was working on website where I had to re-order a nested list (part of a navigation menu) – unfortunately I only had access to fragment of HTML so I couldn’t just manipulate the arrays from which it was built. The menu was compiled from various arrays and months within years sometimes came out all wrong.

So I thought, I’d just treat it as a bit of XML (which obviously it is) and re-order it using PHPs native XML handling classes. XML is one of those things that I use frequently, but never really do anything with, and finding a solution took me rather longer than I had expected. It is a mixture of simpleXML and XMLDom.

One of the main problems was the lack any real examples.

If anyone can suggest a more elegant solution, I would love to hear it.

Here is my code:

The UL to re-order

As you can see the month names are in the wrong order

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<ul id="menu">
    <li class="first"><a href="/news/q/date/2011/">2011</a></li>
    <li class="current last">
        <a href="/news/q/date/2010/">2010</a>
            <ul>
                <li class=""><a href="/news/q/date/2010/07/">July</a></li>
                <li class=""><a href="/news/q/date/2010/06/">June</a></li>    
                <li class=""><a href="/news/q/date/2010/11/">November</a></li>
                <li class=" last"><a href="/news/q/date/2010/10/">October</a></li>
                <li class=""><a href="/news/q/date/2010/09/">September</a></li>
                <li class=""><a href="/news/q/date/2010/08/">August</a></li>
                <li class="first"><a href="/news/q/date/2010/12/">December</a></li>
                <li class=""><a href="/news/q/date/2010/05/">May</a></li>
           </ul>
    </li>
</ul>

My (woeful) solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
 
$xml = simplexml_load_string($string);
 
// pull a node tree as an Array out using simpleXML xpath
$trees = $xml->xpath('/ul/li/ul');
 
$array = array();
$order = array();
 
$i = 0;
 
// we only need to delve into XML if there are any nested <ul>s
if(isset($trees[0])){
 
	foreach($trees[0] as $var){
 
		// store each node in an indexed array
		$array[$i] = $var; 
		// store the month number in an index array
		// based on the text node value of the <a> tag
		$order[$i] = date('m', strtotime((string) $var->a)); 
 
		$i++;
	}
 
	// sort the month number array descending, but maintaining the keys
	arsort($order); 
 
	// create a new XML Dom object to manipulate stuff
	$dom = new DomDocument();
	// create a holder node <ul>
	$ul = $dom->createElement('ul');
 
	// iterate through the array of simpleXML objects 
	// based on the order in which their keys appear in the re-ordered array
	foreach($order as $key => $value){
		// get the simpleXML objects into a string
		$node = dom_import_simplexml($array[$key]);
		// get the string into an actual DOM node
		$node = $dom->importNode($node, true);
		// append
		$ul->appendChild($node);	
	}
 
	//$dom->appendChild($ul);
 
	// unset the contents of the original <ul> node that we have resorted
	$parent = $trees[0]->xpath( 'parent::*' );
	$parent[0]->ul = NULL;
 
	// turn our simpleXML object into a DOM object
	$ixml = dom_import_simplexml($xml);
	$new = new DOMDocument('1.0');
	$ixml = $new->importNode($ixml, true);
	$ixml = $new->appendChild($ixml);
 
	// fire up a DOM xpath object
	$xpath = new DomXpath($new);
	// pull a node tree out using simpleXML xpath
	$tree = $xpath->query('/ul/li/ul');
 
	// add our newly created DOM node conatining the re-ordered <ul> after the existing node
	$tree->item(0)->parentNode->appendChild($new->importNode($ul, true));
	// delete the original empty node
	$tree->item(0)->parentNode->removeChild($tree->item(0));
 
	echo $new->saveHTML();
 
} else {
 
	echo $string;
 
}

Messing about with Tarzan and the Amazon AAWS web service

At the moment I building a couple of applications running of the Amazon AWS web services – in a nutshell e-commerce sites that utilize products available via Amazon or companies selling through Amazon and then let you check out using Amazon’s secure checkout (the site owner gets commission on each product sold).

The system is great, but it is huge and there are an awful of options. The documentation is good, but quite hard to navigate and sometimes quite confusing, basically it just boils down to reading the API Docs.

I’ve been using the Tarzan AWS framework / toolkit / library to deal with the hassle of connecting to and being authenticated by the Amazon system. Tarzan is a great system, but again it boils down to reading the API as there aren’t really any tutorials.

Finding the OfferListingId

Products sold via Amazon are identified by all kinds of data including ISBN codes, ASIN (Amazon ID numbers), OfferListingIds etc. I guess the trick is knowing which one to use in given circumstances.

One real gotcha that held me up (I had to post on the Tarzan group to find the answer) was that Tarzan is designed to use the OfferListingId of a product to identify it at key points (e.g. adding a product to the remote cart) – my understanding of this is that it is because every item has an OfferListingId – but not every number has an ASIN (this could be wrong and it could be because of another reason entirely).

My problem was that I could not find the OfferListingIds of the products I wanted to add to the cart, and so was trying to use the ASIN. After an age of debugging and trying to figure out what the hell was wrong, I looked at the actual Tarazn code and discovered that it was set up to only deal with OfferListingIds – so I changed it to work with ASINs and the rest of my code started working how it was supposed to (i.e. I could now add items to my remote cart).

Ryan Parman (the guy who wrote Tarzan) kindly explained that the OfferListingId is only contained in certain responses from Amazon – I had been requesting a result set which did not include the OfferListingId – problem solved. It turns out that the OfferListingId is returned if you request a ‘Large’ or ‘Offers’ response group, I had been using the ‘Medium’ group thinking that less data to work with would make my life easier. Silly me.

Serialize SimpleXML problems

When you add your first product to the remote Amazon shopping cart you have to use the create_cart() method. This creates a cart object and adds product(s) to it. Part of the response is a CartId which you need to use to access the cart again in future requests. The logical place for this is a Session.

My responses from Amazon via Tarzan pop out as SimpleXML objects, very handy and easy to work with. Unless you forget that PHP won’t let you serialize native objects… There are various solutions but my quick and dirty solution was to use type casting to change the SimpleXML object to a string e.g.

1
2
3
4
5
6
7
8
9
10
11
 
$response = $aaws->cart_create($item, $opt, AAWS_LOCALE_UK);
 
    if(isset($response->body->Cart->CartId)){
 
        $this->Session->write('Cart.CartId', (string)$response->body->Cart->CartId);
	$this->Session->write('Cart.URLEncodedHMAC', (string)$response->body->Cart->URLEncodedHMAC);
	$this->Session->write('Cart.CartItemId', (string)$response->body->Cart->CartItems->CartItem->CartItemId);
 
 
}

And it works like a treat.

(This is also worth a read: Serialize and Unserialize SimpleXML in php)