Skip to content
This post
Filed neatly under:
Code
Meticulously tagged with:
, , , , , , , , , , , , , , , , , ,
Share this
  • Digg
  • Facebook
  • Twitter
  • Delicious
  • Newsvine
  • StumbleUpon
Related posts
Taxonomy
Archive
Short
Exchange

Work related: Little Chef site relaunch

04Oct20091655

Little Chef homepage

The longest run­ning and most high-profile web­site I have the pleas­ure of work­ing on is for Little Chef. With such a recog­nis­able brand and in a period of increased com­pany activ­ity, the site is increas­ing its role as the primary com­mu­nic­a­tion with cus­tom­ers. With a recent aes­thetic refresh which only select parts of the site were quick to fol­low, the remainder was still stoic­ally in the old style — updat­ing the rest was a decept­ively large task and exposed the oppor­tun­ity to rec­tify some of the nig­gling obstruc­tions that had grown with the site. What on the sur­face was just a visual update was in a fact a more far-reaching upgrade.

“if these were the most com­plic­ated aspects of the site the rebuild would have been sim­pler and drastic­ally more straightforward.”

Rebuild­ing an exist­ing site always starts with the best inten­tions — glassy eyed optim­ism see­ing only improve­ments and never pit­falls, but exper­i­ence has taught tem­per­ance rather than ambi­tious extra­vag­ance. Ambi­val­ence is quick to set in: on the one hand there is a full and detailed spe­cific­a­tion avail­able in the form of the cur­rently used site, while on the other it soon becomes rap­idly appar­ent that with his­tory comes refine­ment that may not lend itself to rapid recon­struc­tion. Strik­ing a bal­ance between recon­struct­ing for improve­ment and the silent threat of fea­ture creep is the key to a timely and suc­cess­ful pro­ject.

Styles

With a visual change came the aus­pi­cious chance to update all of the archaic stylesheets. Most content-driven sites tend to have a cent­ral sheet with com­mon styles used across dif­fer­ent pages — these usu­ally con­sist of header, footer, side­bar and default con­tent styles; the Little Chef site doesn’t suit this kind of organ­isa­tion due in part to the uniquely designed sec­tions, very few styles carry across dif­fer­ent pages which meant when I ori­gin­ally cre­ated the styles, spe­cificity rules played a big part in craft­ing dif­fer­ent sec­tions. The biggest issue with this is that its near impossible to start with a “blank can­vas”, most com­mon iden­ti­fi­ers (#content, #sidebarRight etc.) carry some default bag­gage with them. Les­son learned, the default stylesheet now only defines the header and footer with the primary con­tent area left up to sec­tion spe­cific stylesheets to define; this is not flaw­less, espe­cially when deal­ing with Inter­net Explorer spe­cific styles. Ordin­ar­ily only a single stylesheet for each browser should be needed across the site, how­ever styles which were impli­cit in sec­tion spe­cific sheets would need to be expli­cit in a site-wide sheet. For instance, if there are two sec­tion spe­cific stylesheets, each one can use class iden­ti­fi­ers or IDs without fear of clash­ing, how­ever a site-wide one would need an extra layer of spe­cificity for these ele­ments to be tar­geted. The only recourse was an iden­ti­fier on the BODY ele­ment for each sec­tion which means the browser spe­cific style rules always need to be pre­fixed (some­times called “warts”) which can be tiresome:

body#friends #sidebarRight { padding-top: 5.8em; }
body#friends #sidebarRight .login button { margin-right: -14px; }

/* news.css */
body#news .newsJump { height: 1%; width: 555px; }

/* feedback.css */
body#feedback .intro { height: 1%; width: 555px; }
body#feedback form legend { margin-left: -0.7em; }

Inter­net Explorer 6 once again proved the most obtuse of browsers, primar­ily its idio­syn­crasies sur­round­ing the use of ID and class spe­cifiers on a single ele­ment e.g. #content.sectionName. Bizar­rely the spe­cific ele­ment can­not be styled bey­ond the first instance, how­ever any child ele­ments can. So if a #content rule exists, all #content rules regard­less of any sub­sequent class spe­cific rules will have the same styles, how­ever if there is #content .elementOne and #content.sectionName .elementOne, the rules act as they should. It’s an infuri­at­ingly vul­gar arrange­ment but one that is not entirely unwork­able — lam­ent­ably most of the “fixes” involve pos­i­tion­ing ele­ments abso­lutely so that IE6’s fast-and-loose inter­pret­a­tion of the box model is mitigated.

To match the new stylesheets, more or less every single page’s markup was re-examined. The his­tory of the site meant what had begun innoc­u­ously had mutated with repeated updates and innu­mer­able addi­tions and modi­fic­a­tions; many sec­tions were sim­pli­fied or wholly recre­ated, the Menu being the largest example of the latter.

Frame­work

The biggest change by far though is the use of the Zend Frame­work for the entirety of the site. More than just a grav­it­a­tion towards the new and shiny, the site ideally would have been built with a frame­work from the out­set had it not been for exten­u­at­ing factors denot­ing oth­er­wise. Ori­gin­ally con­ceived for a PHP4 host, the site ori­gin­ally used indi­vidual PHP scripts and an ensemble of dif­fer­ent lib­rar­ies and con­fig­ur­a­tions; as is often the case, this worked but showed its fra­gil­ity as the site grew — and most cru­cially — would become a hindrance as it grew fur­ther. Indeed, the plans for the Little Chef site are such that were it to remain as-is, the work required in the future would be even greater, espe­cially to main­tain any kind of quality.

Con­trol­ler is put to work with the router hand­ling the for­ward­ing of the old .php files to the rel­ev­ant con­trol­lers — so whereas now a URL may be aboutus/wifi, the old URL wifi.php and its shortened form wifi will also work. RSS is now provided through con­text switch­ing while user authen­tic­a­tion is handled using Auth and the action init() hook. Of course if these were the most com­plic­ated aspects of the site the rebuild would have been drastic­ally sim­pler and more straightforward.

Route finder

Little Chef route finder

The largest amount of time was spent on the most pop­u­lar area of the site, the “Find a Little Chef” ser­vice which integ­rates with the Microsoft Map­Point web ser­vice. The full details of the imple­ment­a­tion are for another time, suf­fice to say that it was neither quick nor routine to enable PHP to talk to the ser­vice in a man­age­able way. The most try­ing part of the pro­cess was the num­ber of dif­fer­ent states that are pos­sible from only expos­ing two dif­fer­ent pieces of func­tion­al­ity: find a route from one loc­a­tion to another with a cor­ridor search for res­taur­ants and a radial search for res­taur­ants around a spe­cific point. Using the former for this example, there is the best case scen­ario where both loc­a­tions are suc­cess­fully found by the ser­vice, a route is cal­cu­lated between them and res­taur­ants and avail­able and dis­played appro­pri­ately; bey­ond this state how­ever is a mul­ti­tude of dif­fer­ent cases, all of which require a dis­pro­por­tion­ate amount of atten­tion. For instance, if one out of two of the loc­a­tions is ambigu­ous, a screen should be dis­played offer­ing dif­fer­ent choices — this is well explored beha­viour present on a num­ber of dif­fer­ent map­ping ser­vices. The prob­lem arises how­ever in how to do a sub­sequent search once a loc­a­tion is chosen.

Using the query “Barns­ley” as a con­crete example — there are three dif­fer­ent “Barnsley“s in the UK. Map­Point help­fully provides a “Dis­play­Name” prop­erty for a loc­a­tion which means dis­play­ing a list of the dif­fer­ent “Barnsley“s avail­able is easy, how­ever the only mean­ing­ful inform­a­tion returned by Map­Point that is sub­sequently use­ful is the lat­it­ude and lon­git­ude. The dis­play name can’t be used to per­form a search as one of the names returned is “Barns­ley, Eng­land, United King­dom” which is still ambigu­ous, an Entity ID is returned how­ever this is not usable as the primary Map­Point data sources do not allow entity ID searches, only user data sources are. To do a route search, estab­lished loc­a­tions must be used rather than just lat­it­ude and lon­git­ude points which means that once a choice has been made and the lat­it­ude and lon­git­ude passed back to the ser­vice, a new search has to be done to find the closest route-able loc­a­tion which, dis­ap­point­ingly enough, can fail des­pite the ser­vice return­ing a valid loc­a­tion not one query prior. It’s a start­lingly back­ward way of doing things and just one of a cata­logue of trau­mat­ic­ally debil­it­at­ing obstacles to imple­ment­a­tion com­poun­ded by the drought of qual­ity doc­u­ment­a­tion avail­able which means that altern­at­ives and real-world imple­ment­a­tions bey­ond the inter­min­able FourthCof­fee examples are non-existent.

Friends

Little Chef signup

The Friends sec­tion, hav­ing seen over 36,000 sign ups since its incep­tion, was given an equal amount of atten­tion, most of it devoted to the “Sign up” form. Form takes away a lot of the pain but does have some notice­able gaps. I don’t use the auto­matic out­put dec­or­at­ors of a form, primar­ily as past exper­i­ence has shown that every form is unique in one way or another that auto­matic gen­er­a­tion can’t and shouldn’t handle. The first of these was a com­bin­a­tion field for dates which is reused and exten­ded in the Feed­back sec­tion with a time imple­ment­a­tion — this is only pos­sible if one man­ages to stumble across the $con­text para­meter to a form field’s val­id­a­tion method. Either not avail­able in earlier frame­work releases or oth­er­wise more clev­erly hid­den away, $con­text is an array of all the other data passed to a form which allows for more cap­able val­id­at­ors and form ele­ments. For a date field, out­put­ting as three drop-down boxes or a JavaS­cript gen­er­ated cal­en­dar con­trol makes no dif­fer­ence as the val­id­a­tion method can gather any other fields and present back a single, valid date:

class Lib_Form_Element_Date extends Zend_Form_Element_Xhtml {
	public function init() { $this->addValidator(new Lib_Validate_Date()); }

	public function isValid($value, $context = null) {
		if(is_array($value) && array_key_exists("day", $value) &&
			array_key_exists("month", $value) && array_key_exists("year", $value))
		{
			$value = strftime("%F", mktime(0, 0, 0, $value["month"], $value["day"], $value["year"]));
			$this->setValue($value);
		}

		return parent::isValid($value, $context);
	}
}
class Lib_Validate_Date extends Zend_Validate_Abstract {
	const INVALID_DATE = "invalidDate";

	protected $_messageTemplates = array(
		self::INVALID_DATE => "\"%value%\" is not a correct date"
	);

	public function isValid($value, $context = null) {
		$this->_setValue($value);

		if(is_string($value) && (strpos($value, "-") !== false)) {
			list($y, $m, $d) = explode("-", $value);
			if(!checkdate(intval($m), intval($d), intval($y))) {
				$this->_error = self::INVALID_DATE;
				return false;
			}
		} else {
			if(strtotime($value) === false) {
				$this->_error = self::INVALID_DATE;
				return false;
			}
		}

		return true;
	}
}

This is per­haps the simplest use of the para­meter but $con­text opens up other pos­sib­il­it­ies as well, namely for val­id­at­ors which base their res­ult on the val­ues of other fields. The most com­mon use would be a pass­word and con­firm­a­tion fields, without $con­text it’s impossible to provide a val­id­ator for the rule that both must be identical. This case can be made more gen­eric with a Field­Match val­id­ator which can match any num­ber of fields to each other in the case that such a situ­ation would arise.

A more com­plex example is one where val­id­at­ors are spe­cific­ally applied only if the value of another fields is as spe­cified. The easi­est way to think about this is the “Other” option for drop downs, if this is selec­ted then a user should fill in the free-text field provided. Again this case can be boiled down to a gen­eric “Con­tin­gent” val­id­ator which is con­struc­ted with a list of fields, a list of trig­ger matches and a list of val­id­at­ors to apply in the event of a field match. Per­haps easier to demon­strate than to describe:

$form->addElement("select", "title", array(
  "required" => true,
  "multiOptions" => array("Mr" => "Mr", "Mrs" => "Mrs", "Other" => "Other")
));
$form->addElement("text", "titleother", array(
  "allowEmpty" => false,
  "validators" => array(new Lib_Validate_Contingent("title", "Other", new Zend_Validate_NotEmpty()))
));
class Lib_Validate_Contingent extends Zend_Validate_Abstract {
	protected $_fields = array(), $_matches = array(), $_validators = array();

	public function isValid($value, $context = null) {
		foreach($this->_fields AS $k => $field) {
			if((is_array($this->_matches[$k]) && in_array($context[$field], $this->_matches[$k])) ||
				$context[$field] == $this->_matches[$k])
			{
				foreach($this->_validators AS $validator) {
					if(!$validator->isValid($value, $context)) {
						$this->_messageTemplates = $validator->getMessageTemplates();
						$this->_messageVariables = $validator->getMessageVariables();
						$this->_value = $value;

						foreach($validator->getMessages() AS $k => $v) {
							$this->_error($k, $value);
						}

						return false;
					}
				}
			}
		}

		return true;
	}
}

In this way the Con­tin­gent val­id­ator is like a trig­ger — it doesn’t provide any val­id­a­tion mes­sages itself but feeds back the asso­ci­ated validator’s. Import­ant to note the “allowEmpty” set­ting for the field which means the val­id­ator is triggered even if the field itself is empty which in most cases is entirely the point. Ori­gin­ally this val­id­ator was attached to the other field (in the example above this would be “title”), how­ever this was logic­ally odd and per­plex­ing in some situ­ations as the val­id­a­tion mes­sages would then be bound to that field rather than the tar­get one. With this it’s entirely pos­sible to cre­ate com­plex, cas­cad­ing forms without for­go­ing the use of val­id­at­ors and rely­ing on unwieldy con­trol struc­tures.

Con­clu­sion

Both the rebuild itself and the launch, while not without their vex­ing moments, went bet­ter than I had expec­ted and the wealth of les­sons learned, com­pon­ents built and chal­lenges encountered more than makes up for the teeth-grinding suffered. There are a myriad of other, smal­ler com­plic­a­tions which don’t war­rant a full explor­a­tion, most of them borne out of the need for bet­ter debug­ging, espe­cially when deal­ing with external ser­vices. The true meas­ure of the work done now will be in the months and hope­fully the years to come when the site is sure to mature into some­thing per­haps drastic­ally dif­fer­ent from what it is now. 

Responses and trackbacks have been turned off for this post.
<
&rt;