For essentially the same reasons that globals are bad, code should be isolated into local scopes (basically, functions); this is part of separating concerns. Instead of having code output the menu at a global level, create a view: a function or class that outputs it. You can either call the function/instantiate the class in the main controller script, or in some other menu controller script included in the main script. That way, you can pass any custom classes to the appropriate function/set the appropriate property instead of mucking about with global variables or superglobals. Also, you won't need a test for every element when outputting the element's classes.
You'll need to decide on a standard way of creating identifiers for each type of object you need to deal with (e.g. pages, links and menu items), and a way of mapping the identifier to each object given the type. URLs are identifiers, so you could use the path (e.g. "/eng/about/history") or part of the path (e.g. "about/history") as an identifier. The downside to this is that URL paths include characters that aren't legal in HTML identifiers. You could use a unique page name (e.g. "history") that would be a valid HTML ID and include a mapping from the ID to URLs.
For example, an outline off using a function; views/sitemenu.php:
PHP Code:
<?php
function displaySiteMenu($current) {
static $items = array(
...
);
$items[$current]['class'] .= ' current';
foreach ($items as $id => $item) {
...
}
}
fragments/sitemenu.php:
PHP Code:
<?php
include_once('views/sitemenu.php');
displaySiteMenu(url2id($_SERVER['REQUEST_URI']));
If you don't want to construct the menu programmatically, you can instead define the menu in an HTML fragment file (or a document in some other data structuring language), load it with an HTML (or whatever) parser, modify it as necessary & output. This approach is particularly useful if there are designers who know HTML but not PHP working on a project (which I'm guessing isn't currently true of your project). There isn't much of a performance impact over the original approach. Of course, at this point you may as well pick an existing MVC framework to use rather than implementing one yourself (unless the point is self-education).
PHP Code:
abstract class View {
abstract function __toString();
/* output this view */
function display() {
echo (string)$this;
}
/* prepare for output */
abstract function generate();
}
/* produces HTML. If you have views that create HTML programmatically,
* refactor some of HTMLView into an HTMLTemplate class that loads the
* HTML fragment.
* You could also add methods to HTMLView that allows hierarchical views,
* where one view can be a child of another.
* Example implementation based on DOMDocument.
*/
abstract class HTMLView extends View {
protected $_doc, # HTML parser, e.g. DOMDocument
$_path, # document traverser, e.g. DOMXPath
$_file; # HTML fragment
function __construct($file) {
parent::__construct();
$this->_file = $file;
$this->generate();
}
function __toString() {
/* Pass root node to prevent DOCTYPE declaration, <html> and
* <body> from being included in result. Requires PHP >= 5.3.6
*/
return $this->_doc->saveHTML($this->_doc->documentElement);
}
function generate() {
if (! $this->_doc) {
$this->_doc = new DOMDocument;
$this->doc->validateOnParse = TRUE; # so getElementById will work
$this->_load();
$this->_revise();
}
}
/* load HTML fragment */
protected function _load() {
$this->_doc->loadHTMLFile($this->_file);
$this->_path = new DOMXPath($this->_doc);
# so you can use php functions in XPath expressions
$this->path->registerNamespace("php", "http://php.net/xpath");
$this->path->registerPhpFunctions();
}
/* modify loaded HTML */
abstract protected function _revise();
}
class HTMLMenuView extends HTMLView {
public $current;
protected function _revise() {
$this->_markCurrentItem();
...
}
protected function _markCurrentItem() {
# "/path/to/" could simply be "//" to search all <dd>
$dds = $this->_path->query("/path/to/dd[@id='{$this->current}_menu_item']");
if ($dds->length) {
# assume node is an element
$classes = $dds->item(0)->getAttribute('class');
$dds->item(0)->setAttribute('class', $classes . ' current');
}
}
}