In this two-part tutorial, we will be building a simple website with PHP and MySQL, using the Model-View-Controller (MVC) pattern. Finally, with the help of the jQuery Mobile framework, we will turn it into a touch-friendly mobile website, that works on any device and screen size.
In this first part, we concentrate on the backend, discussing the database and MVC organization. Next time, we will be writing the views and integrating jQuery Mobile.
The File Structure
As we will be implementing the MVC pattern (in effect writing a simple micro-framework), it is natural to split our site structure into different folders for the models, views and controllers. Don’t let the number of files scare you – although we are using a lot of files, the code is concise and easy to follow.
The Directory Structure
The Database Schema
Our simple application operates with two types of resources – categories and products. These are given their own tables – jqm_categories, and jqm_products. Each product has a category field, which assigns it to a category.
jqm_categories Table Structure
The categories table has an ID field, a name and a contains column, which shows how many products there are in each category.
jqm_products Table Structure
The product table has a name, manufacturer, price and a category field. The latter holds the ID of the category the product is added to.
You can find the SQL code to create these tables in tables.sql in the download archive. Execute it in the SQL tab of phpMyAdmin to have a working copy of this database. Remember to also fill in your MySQL login details in config.php.
The Models
The models in our application will handle the communication with the database. We have two types of resources in our application – products and categories. The models will expose an easy to use method –find()
which will query the database behind the scenes and return an array with objects.
Before starting work on the models, we will need to establish a database connection. I am using the PHP PDO class, which means that it would be easy to use a different database than MySQL, if you need to.
includes/connect.php
08 | "mysql:host=$db_host;dbname=$db_name;charset=UTF-8" , |
13 | $db ->query( "SET NAMES 'utf8'" ); |
14 | $db ->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); |
16 | catch(PDOException $e ) { |
17 | error_log ( $e ->getMessage()); |
18 | die ( "A database error was encountered" ); |
This will put the $db connection object in the global scope, which we will use in our models. You can see them below.
includes/models/category.model.php
09 | public static function find( $arr = array ()){ |
13 | $st = $db ->prepare( "SELECT * FROM jqm_categories" ); |
16 | $st = $db ->prepare( "SELECT * FROM jqm_categories WHERE id=:id" ); |
19 | throw new Exception( "Unsupported property!" ); |
26 | return $st ->fetchAll(PDO::FETCH_CLASS, "Category" ); |
Both models are simple class definitions with a single static method – find(). In the fragment above, this method takes an optional array as a parameter and executes different queries as prepared statements.
In the return declaration, we are using the fetchAll method passing it the PDO::FETCH_CLASS constant. What this does, is to loop though all the result rows, and create a new object of the Category class. The columns of each row will be added as public properties to the object.
This is also the case with the Product model:
includes/models/product.model.php
06 | public static function find( $arr ){ |
10 | $st = $db ->prepare( "SELECT * FROM jqm_products WHERE id=:id" ); |
12 | else if ( $arr [ 'category' ]){ |
13 | $st = $db ->prepare( "SELECT * FROM jqm_products WHERE category = :category" ); |
16 | throw new Exception( "Unsupported property!" ); |
21 | return $st ->fetchAll(PDO::FETCH_CLASS, "Product" ); |
The return values of both find methods are arrays with instances of the class. We could possibly return an array of generic objects (or an array of arrays) in the find method, but creating specific instances will allow us to automatically style each object using the appropriate template in the views folder (the ones that start with an underscore). We will talk again about this in the next part of the tutorial.
There, now that we have our two models, lets move on with the controllers.
Computer Store with PHP, MySQL and jQuery Mobile
The controllers
The controllers use the find() methods of the models to fetch data, and render the appropriate views. We have two controllers in our application – one for the home page, and another one for the category pages.
includes/controllers/home.controller.php
04 | public function handleRequest(){ |
07 | $content = Category::find(); |
10 | 'title' => 'Welcome to our computer store' , |
Each controller defines a handleRequest() method. This method is called when a specific URL is visited. We will return to this in a second, when we discuss index.php.
In the case with the HomeController, handleRequest() just selects all the categories using the model’s find() method, and renders the home view (includes/views/home.php) using our render helper function (includes/helpers.php), passing a title and the selected categories. Things are a bit more complex inCategoryController:
includes/controllers/category.controller.php
03 | class CategoryController{ |
04 | public function handleRequest(){ |
05 | $cat = Category::find( array ( 'id' => $_GET [ 'category' ])); |
08 | throw new Exception( "There is no such category!" ); |
12 | $categories = Category::find(); |
15 | $products = Product::find( array ( 'category' => $_GET [ 'category' ])); |
19 | render( 'category' , array ( |
20 | 'title' => 'Browsing ' . $cat [0]->name, |
21 | 'categories' => $categories , |
22 | 'products' => $products |
The first thing this controller does, is to select the category by id (it is passed as part of the URL). If everything goes to plan, it fetches a list of categories, and a list of products associated with the current one. Finally, the category view is rendered.
Now lets see how all of these work together, by inspecting index.php:
index.php
06 | require_once "includes/main.php" ; |
10 | if ( $_GET [ 'category' ]){ |
11 | $c = new CategoryController(); |
13 | else if ( empty ( $_GET )){ |
14 | $c = new HomeController(); |
16 | else throw new Exception( 'Wrong page!' ); |
22 | render( 'error' , array ( 'message' => $e ->getMessage())); |
This is the first file that is called on a new request. Depending on the $_GET parameters, it creates a new controller object and executes its handleRequest() method. If something goes wrong anywhere in the application, an exception will be generated which will find its way to the catch clause, and then in the error template.
One more thing that is worth noting, is the very first line of this file, where we require main.php. You can see it below:
main.php
06 | require_once "includes/config.php" ; |
07 | require_once "includes/connect.php" ; |
08 | require_once "includes/helpers.php" ; |
09 | require_once "includes/models/product.model.php" ; |
10 | require_once "includes/models/category.model.php" ; |
11 | require_once "includes/controllers/home.controller.php" ; |
12 | require_once "includes/controllers/category.controller.php" ; |
16 | header( 'Cache-Control: max-age=3600, public' ); |
17 | header( 'Pragma: cache' ); |
18 | header( "Last-Modified: " . gmdate ( "D, d M Y H:i:s" ,time()). " GMT" ); |
19 | header( "Expires: " . gmdate ( "D, d M Y H:i:s" ,time()+3600). " GMT" ); |
This file holds the require_once declarations for all the models, controllers and helper files. It also defines a few headers to enable caching in the browser (PHP disables caching by default), which speeds up the performance of the jQuery mobile framework.
Stay tuned for part two!
With this the first part of the tutorial is complete! Stay tuned for next week’s addition, where we will be writing the views and incorporate jQuery Mobile. Feel free to share your thoughts and suggestions in the comment section below.
http://tutorialzine.com/2011/08/jquery-mobile-product-website/