My last PHP project was almost a year and a half ago. I started working in PHP eversince college. Moved on to Java, Python and now, Ruby. I must say, PHP was the longest scripting language I used in my entire professional programming career. Maybe because it was the easiest language to learn (and abuse).
So many resources on the Web have fluttered around for so long, even after frameworks were born. There came CakePHP, Zend, Prado, Symfony, etc. There were other tools too to help in making PHP a truly object orientd scripting language. Some of my few favorites were: Smarty and AdoDB.
I've quit on PHP, but I do not abhor it. I still understand the old businesses still running their cash-cow apps in PHP and is not yet looking forward to trusting Ruby on Rails or even other alternatives. The business is always driven by finances and resources. Sometimes, the best technological decisions elude the managers' table during meetings.. hence, the prevalence of a lot of legacy PHP applications. With this understanding, I still believe that these businesses would very much benefit upgrading their systems and even the skill sets of their own developers. Even if they wanted to remain with PHP, there are a lot of options out there that do not involve complete development of applications from the ground up. Some would even prefer built systems that allow plugins for upgrade--like Drupal, Joomla, and even Wordpress.
There are cons of using such built systems (dependency on plugin upgrades, general hacking, and a becoming a prey to overly customizing them), and so, I myself would not recommend using them if you won't be able to protect your site from these downside.
Though its been quite a while since I last worked on PHP, it became a challenge for me to just do this mini store application just for fun of measuring how long I could work on such a request given that I have been out of touch from the PHP world, and that the requirement could also involve AJAX.. and it would be from the ground up!
This would be a very simple application though needing a database and javascript support for AJAX. Let me now guide you to how it was done.
First, create the database with the following schema:
CREATE TABLE areas (
id int(11) NOT NULL auto_increment,
country_id int(11) NOT NULL,
`name` varchar(255) NOT NULL,
PRIMARY KEY (id),
KEY country_id_index (country_id)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
CREATE TABLE countries (
id int(11) NOT NULL auto_increment,
`name` varchar(255) NOT NULL,
PRIMARY KEY (id)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
CREATE TABLE stores (
id int(11) NOT NULL auto_increment,
`name` varchar(255) NOT NULL,
contact_person varchar(255) NOT NULL,
contact_info varchar(255) NOT NULL,
address varchar(255) NOT NULL,
email varchar(40) NOT NULL,
website varchar(255) NOT NULL,
area_id int(11) NOT NULL,
PRIMARY KEY (id),
KEY name_index (`name`),
KEY area_id_index (area_id)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
Next, let's prepare the main file. We are going to need a stores.php, getareas.php, getstores.php, search.php, ajax.js and include files for connecting to the database and query functions.
Stores.php
The most important parts of this file are the section handlers. We'll need handles for: (1) search results, (2) areas list, (3) stores list. These should have distinct ids that you'll have to note.
Getareas.php Getstores.php Search.php";
echo "NameContact PersonContact InfoAddressEmailWebsiteCountryArea";
foreach($results as $row){
echo "";
echo "" . $row['name'] . "" . $row['contact_person'] . "" . $row['contact_info'] . "" . $row['address'] . "" . $row['email'] . "" . $row['website'] . "";
echo "<a onclick="\"javascript:showAreas('"">" . $row['country_name'] . "</a>";
echo "";
echo "<a onclick="\"javascript:showStores('"">" . $row['area_name'] . "</a>";
echo "";
echo "";
}
echo "";
}else{
echo "Whooops! No store matched your search criteria.";
}
?>
Ajax.js
var xmlhttp;
var areaHandle;
var storeHandle;
function showSearchResults(str)
{
xmlhttp=GetXmlHttpObject();
if (xmlhttp==null){
alert ("Browser does not support HTTP Request");
return;
}
var url="search.php";
url=url+"?search="+str;
url=url+"&sid="+Math.random();
xmlhttp.onreadystatechange=stateChangedSearch;
xmlhttp.open("GET",url,true);
xmlhttp.send(null);
}
function showAreas(str)
{
xmlhttp=GetXmlHttpObject();
if (xmlhttp==null){
alert ("Browser does not support HTTP Request");
return;
}
areaHandle = str;
var url="getareas.php";
url=url+"?q="+str;
url=url+"&sid="+Math.random();
xmlhttp.onreadystatechange=stateChangedAreas;
xmlhttp.open("GET",url,true);
xmlhttp.send(null);
}
function showStores(str)
{
xmlhttp=GetXmlHttpObject();
if (xmlhttp==null){
alert ("Browser does not support HTTP Request");
return;
}
storeHandle = str;
var url="getstores.php";
url=url+"?q="+str;
url=url+"&sid="+Math.random();
xmlhttp.onreadystatechange=stateChangedStores;
xmlhttp.open("GET",url,true);
xmlhttp.send(null);
}
function stateChangedSearch()
{
switch(xmlhttp.readyState){
case 4:
document.getElementById('searchBox').innerHTML=xmlhttp.responseText;
break;
case 1:
document.getElementById("searchBox").innerHTML="Loading...
";
break;
}
}
function stateChangedAreas()
{
switch(xmlhttp.readyState){
case 4:
handleId = "areaHint";
document.getElementById(handleId).innerHTML=xmlhttp.responseText;
break;
case 1:
document.getElementById("areaHint").innerHTML="Loading...</pre>
";
break;
}
}
function stateChangedStores()
{
switch(xmlhttp.readyState){
case 4:
handleId = "storeHint";
document.getElementById(handleId).innerHTML=xmlhttp.responseText;
break;
case 1:
document.getElementById("storeHint").innerHTML="Loading...</pre>
";
break;
}
}
function showStoreDetails(storeId){
storeHandle = "storeDetail" + storeId;
if(document.getElementById(storeHandle).style.display=='block'){
document.getElementById(storeHandle).style.display = 'none';
}else{
document.getElementById(storeHandle).style.display = 'block';
}
}
function GetXmlHttpObject()
{
if (window.XMLHttpRequest)
{
// code for IE7+, Firefox, Chrome, Opera, Safari
return new XMLHttpRequest();
}
if (window.ActiveXObject)
{
// code for IE6, IE5
return new ActiveXObject("Microsoft.XMLHTTP");
}
return null;
}
Of course, it will be totally up to you how you will implement your query functions and other includes for this, but here is what I have:
includes/site.inc.php
function getCountries(){
$db = $GLOBALS['db'];
$sql = "select * from countries order by name asc";
$a = $db->CacheGetAll(3600,$sql);
return $a;
}
function getSearch($key){
$db = $GLOBALS['db'];
$sql = "select stores.name, stores.contact_person, stores.contact_info, stores.address, countries.name as country_name, countries.id as country_id, areas.name as area_name, areas.id as area_id from stores, countries, areas where (stores.name like '%{$key}%' or stores.address like '%{$key}%' or stores.contact_person like '%{$key}%' or stores.contact_info like '%{$key}%' or stores.email like '%{$key}%' or stores.website like '%{$key}%') and stores.area_id = areas.id and areas.country_id = countries.id order by stores.name, stores.address asc";
$a = $db->GetAll($sql);
return $a;
}
function getAreasOfCountry($countryId){
$db = $GLOBALS['db'];
$sql = "select areas.*, countries.name as country_name from areas, countries where areas.country_id = countries.id and areas.country_id = $countryId order by areas.name asc";
$a = $db->CacheGetAll(3600,$sql);
return $a;
}
function getStoresOfArea($areaId){
$db = $GLOBALS['db'];
$sql = "select * from stores where area_id=$areaId order by name asc";
$a = $db->CacheGetAll(3600,$sql);
return $a;
}
To briefly explain, the stores.php page displays a search form at the top and lists all available countries at the left side. For better display, the list of countries, areas and stores are clipped and set overflow to automatic only. Each country is displayed a link which then triggers and ajax call to get the areas for this specific country. This link points to a javascript method that invokes AJAX and returns the results of the invoked file to the handle specified in our stores.php page.
The list of areas are displayed in the next pane. The AJAX method displays a loading message while waiting for the results. Added effects were used from the use of prototype and scriptaculous for eye candy only. If you don't want to implement prototype/scriptaculous, you can remove the following files from being loaded in the head section of your stores.php:
and, use these lines to display the names of the country/area:
in stores.php:
echo "" . $country['name'] . "";
in getareas.php:
echo "" . $area['name'] . "";
You're officially good to go. There are other tips you can use too. If you're interested with personalizing your ajax-loader.gif, you can visit this site and create your own. And if however, you have a huge amount of information to be stored, make sure you have the correct indices for your database.
ALTER TABLE `areas` ADD INDEX `country_id_index` ( `country_id` );
ALTER TABLE `stores` ADD INDEX `store_name_index` ( `name` );
ALTER TABLE `stores` ADD INDEX `store_area_id_index` ( `area_id` );
This mini store app plus a CMS to maintain it (both from scratch) took me a whole deal of 5hours' work. I must admit, it was a bit hard for me to go back to working in PHP when I was so overjoyed with Ruby. I knew I could have done this all in Ruby on Rails in a little less than 5hours' work and with much lesser code. I still think that for an application to be neat, less cluttered and specifically working in DRY, it would take so much effort not like in Rails. And that's the reason why I don't want to go back to PHP again.
But do help yourself, I saved an archive of this code for your consumption. Download here or here. I'm pretty sure you'll put better evolution to this short but sweet app. Its very crude, and most styles where just hard coded for the benefit of fulfilling a demo for the client's request. You may want to visit the store playground here.
Thanks and Enjoy!