Php Web Example
— print (last updated: Mar 16, 2009) print

Select font size:
Download the PhpBooks.zip archive. Install it, like others, as a Php project with existing sources.

The goal of this project is to illustrate a web-based application similar to the MVC Example project.

Description

Like the related Java project, it is assumed that MySQL is installed and you have a test database accessible to the guest user with empty password. If you need to create or re-create the books table, open a shell, navigate the the project's setup folder and run the table creation script:
mysql -u guest test < books-create.sql 

Php PDO

You will need the Php PDO database API installed as described in the Apache/MySQL/Php document for either Windows or Linux(Ubuntu).

Dojo

You will need the Dojo toolkit installed as described in the Php AJAX + Dojo document.

MVC

As in MVC Example, this example web application achieves the isolation of database inside the model by having all data accesses go through a Books class. Creating a systematic separation of the controller from the view is not really possible with some overarching scheme created by a Web framework. The problem is that the web activations are independent of each other; they don't easily go through a "single point of entry." In Web frameworks this single point of entry is called a front controller or dispatcher.

Our code only suggests a controller/view separation in one of two ways:

Features

This project focuses heavily on the use of AJAX-based server-side calls. The data sent to server-side programs is taken from named elements in forms, and so if you would like to rewrite one or more features in a more standard form-based style the necessary form structure should be in place. There are three portions of a standard CRUD (Create, Read, Update, Delete) application expressed in this example:
  1. Read portion: display book titles in a selection list allowing the user to select one and obtain the details
  2. Create portion: add a book
  3. Update portion: this effectively combines the Read and Create portions. It is incomplete.
For each portion there are three files:

Security Issues

Writing a web application brings security concerns to the forefront since it has a whole new level of accessiblity. Here are some points:
  1. Authentication: Adding authentication means controlling just who can do what to what portions. Often you want anyone to use the Read portion, a more restricted group to Create/Update/Delete (sometimes you want to separate the security levels for these features).

    Web applications such as this example are poorly stuctured for such restriction since there is no single point of entry as mentioned previously. You either have to duplicate authentication code in all relevant scripts or, using some "webserver-based magic," let the server do the authentication for you, or force all relevant scripts to "go through" a single script.

    Web frameworks already have the single point of entry built into the structure and thus provide a more natural way to do authentication.
  2. Database Access Integrity: Your web application accesses a database through web-accessible interface functions prescribed in the model. These interface functions could be used independently of the web application and given unintended inputs. This is the nature of the common attack called SQL injection which attempts to compromise a database by "injecting" further SQL into slots intended for data values. In our application the public member functions of the Books class serve as the only entry point to the database, What we do or suggest doing is to somehow validate the incoming parameters.

Testing/Debugging

Please refer to the section by this same name in the Php AJAX + Dojo document. A reminder of the key points:
  1. Use Firefox and install Firebug.
  2. Use console logging and alerts. Make liberal use of:
    console.log( ... )
    
    to print out anything useful to help debug.
  3. Be careful about spelling of object properties. You can also make use of "user-defined" object properties to hold state information, e.g., if you want to "toggle" some element between two states.
  4. Test the Php handlers "by hand". For example, in this project, try
    http://localhost/default/PhpBooks/handlers/read-book.php?id=1
    
    You'll see the expected output of the book in JSON format. This URL is one example of what is being called implicitly in this code from js/read.js:
    function getDisplay(shouldIsync) {
      dojo.xhrGet(
      {
        url: 'handlers/read-book.php',
        form: "list",
        ...
    
    Even partial information can help give you confidence in the effectiveness of an AJAX handler, such as this:
    http://localhost/default/PhpBooks/handlers/update-book.php?id=1&title=foo
    
    Of course, don't try to go get too complicated with the values, which would have to be url-encoded by hand.

Common Portion

A common trick is to forward or redirect the default file, index.php to another script of choice. In our case we forward to the read.php view script with this code:

index.php
<?php require_once "read.php";
Alternatively, we could use redirection which creates a second activation. It would be done like this:
<?php
header( "location: read.php" );
The nav.php script, included in each of the three view scripts, creates the navigational choices. These choices are necessary in this application because we use the JavaScript location.replace operation to avoid stacking.

nav.php
<table class="nav"> <tr> <? $links = array( "read.php" => "Read", "create.php" => "Create", "update.php" => "Update", ); ?> <? foreach( $links as $href => $label ): ?> <td><a href="javascript:location.replace('<?=$href?>')"><?=$label?></a></td> <? endforeach ?> </tr> </table>
Finally, the mystyle.css file is used by each view script via the HTML link element.

css/mystyle.css
body { padding: 10px; } table.nav td { padding-right: 20px; }

Model Portion

We attempt to mimick the style used in the MVC Example project. Php is able to parse init (.ini) files in much the same way that Java parses .properties files.

models/db/database.ini
url="mysql:host=localhost;dbname=test" user=guest password=

models/db/Database.php
<?php class Database { public function connect() { $ini = parse_ini_file("database.ini"); $url = $ini['url']; $user = $ini['user']; $password = $ini['password']; $cx = new PDO($url, $user, $password); // force exception handling for errors $cx->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION ); return $cx; } }

models/Book.php
<?php class Book { public $id, $title, $type, $qty; function __construct($hash = null) { if (is_array($hash)) { $this->id = $hash['id']; $this->title = $hash['title']; $this->type = $hash['type']; $this->qty = $hash['qty'];; } } }

models/Books.php
<?php require_once "db/Database.php"; require_once "Book.php"; class Books { private $table, $db; public function __construct() { $this->table = strtolower(get_class()); $this->db = new Database(); } private function validate_field_names($fields) { // if $fields is not an array, or the values are not // one of "id", "title", "type", "qty", // throw new Exception("invalid field names in "); } public function fetchAll($fields = null) { $table = $this->table; $cx = $this->db->connect(); if (!isset($fields)) { $fieldstr = "*"; // assume all fields if called without parameter } else { $this->validate_field_names($fields); $fieldstr = join(",", $fields); } $sql = "SELECT $fieldstr FROM $table"; $resultSet = $cx->query($sql); /* // alternative approach using a prepared statement $sth = $cx->prepare($sql); $sth->execute(); $resultSet = $sth; */ $allbooks = array(); foreach($resultSet as $row) { $allbooks[] = new Book($row); } return $allbooks; } public function fetch($id) { $id = (int) $id; // casting makes it an int, even if it started as junk $table = $this->table; $cx = $this->db->connect(); // since $id is a simple integer value, we can just insert it $sql = "SELECT * FROM $table WHERE id=$id"; $row = $cx->query($sql)->fetch(); /* // alternative approach using a prepared statement $sql = "SELECT * FROM $table WHERE id=?"; $sth = $cx->prepare($sql); $sth->execute(array($id)); $row = $sth->fetch(); */ return new Book($row); } private function validate_type($type) { if (!in_array($type, array("paper", "cloth"))) throw new Exception("invalid type"); } private function validate_qty($qty) { if ( (int)$qty != $qty || $qty < 0 ) throw new Exception("Invalid qty"); } public function add($book) { $table = $this->table; $cx = $this->db->connect(); $sql = "INSERT INTO $table (title,type,qty) VALUES (?, ?, ?)"; $sth = $cx->prepare( $sql ); $this->validate_type($book->type); $this->validate_qty($book->qty); // if you attempt to insert a duplicate title, it's an error, // but an anticipated error, so we want to control exactly // what exception is thrown // turn off exception-generation $cx->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT ); // do it $num = $sth->execute( array( $book->title, $book->type, $book->qty, ) ); // turn exception-generation back on $cx->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION ); // test the output of result of insertion to see if any affected rows if ($num == 0) { throw new Exception("Duplicate title"); } } }

Read Portion


read.php
<?php session_start(); $id = $_SESSION['read_id']; // session id specific to usage in this script require_once "models/Books.php"; try { $books = new Books(); $all = $books->fetchAll( array("id", "title") ); } catch(Exception $x) { require_once "handlers/error.php"; exit(); } $page_title = "Read"; ?> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <title><?=$page_title?></title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <link rel="stylesheet" href="css/mystyle.css" /> <script type="text/javascript" src="/dojolib/dojo/dojo.js" djConfig="parseOnLoad:true"></script> <script type="text/javascript" src="js/read.js"></script> <script type="text/javascript"> dojo.addOnLoad( function() { getDisplay(true) } ) </script> </head> <body name="body"> <?php require_once "nav.php"; ?> <h2><?=$page_title?></h2> <form id="list"> <select onchange="getDisplay(false)" name="id"> <option value="">------</option> <? foreach($all as $book): ?> <? $isSelected = ($id == $book->id) ? "selected": ""; ?> <option value="<?=$book->id?>" <?= $isSelected ?> > <?= htmlspecialchars($book->title) ?> </option> <? endforeach ?> </select> </form> <table border="1" cellpadding="5" id="book"> <tr> <td>id:</td> <td id="book.id"></td> </tr> <tr> <td>title:</td> <td id="book.title"></td> </tr> <tr> <td>type:</td> <td id="book.type"></td> </tr> <tr> <td>qty:</td> <td id="book.qty"></td> </tr> </table> <p>Stuff after display table.</p> </body> </html>

js/read.js
function getDisplay(shouldIsync) { dojo.xhrGet( { url: 'handlers/read-book.php', form: "list", // form passes parameter "id" content: { sess_key: "read_id" }, // additional parameter "sess_key" handleAs: "json", sync: shouldIsync, load: function (response) { //--------------------------------------- // extra, more complex, functionality if (!dojo.byId("list").id.value) { dojo.byId('book').style.visibility = "hidden" return; } dojo.byId('book').style.visibility = "visible" //--------------------------------------- dojo.byId('book.id').innerHTML = response.id dojo.byId('book.title').innerHTML = response.title dojo.byId('book.type').innerHTML = response.type dojo.byId('book.qty').innerHTML = response.qty }, error: function (response, ioArgs) { if (ioArgs.xhr.status == 420) { // expected error alert( "error: " + ioArgs.xhr.responseText ) } else { //unexpected error alert("error: " + response) } } } ); }

handlers/read-book.php
<?php session_start(); require_once "../models/Books.php"; $id = $_GET['id']; $sess_key = $_GET['sess_key']; if (isset($sess_key)) { $_SESSION[$sess_key] = $id; // session id specific to source } try { $books = new Books(); $book = $books->fetch($id); } catch (Exception $x) { header("HTTP/1.0 420 My Own Error Code" ); die( $x->getMessage() ); } $title = htmlspecialchars($book->title); $type = $book->type; $qty = $book->qty; echo <<<DATA { id: "$id", title: "$title", type: "$type", qty: "$qty" } DATA;

Create Portion


create.php
<?php $page_title = "Create"; ?> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <title><?=$page_title?></title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <link rel="stylesheet" href="css/mystyle.css" /> <script type="text/javascript" src="/dojolib/dojo/dojo.js" djConfig="parseOnLoad:true"></script> <script type="text/javascript" src="js/create.js"></script> </head> <body> <?php require "nav.php"; ?> <h2><?=$page_title?></h2> <form id="book"> <table cellpadding="5" > <tr> <td>title:</td> <td><input type='text' size="60" name="title" /></td> </tr> <tr> <td>type:</td> <td> <input type="radio" name="type" value="paper" id="book.paper" checked /> <label for="book.paper">paper</label> <input type="radio" name="type" value="cloth" id="book.cloth" /> <label for="book.cloth">cloth</label> </td> </tr> <tr> <td>qty:</td> <td><input type='text' size="5" name="qty" /></td> </tr> <tr> <td></td> <td><button onclick="addBook();return false;">Add</button></td> </tr> </table> </form> </body> </html>

js/create.js
function addBook() { dojo.xhrPost( { url: 'handlers/create-book.php', form: "book", load: function() { alert("success") dojo.byId('book').title.value = "" dojo.byId('book').qty.value = "" dojo.byId('book.paper').checked = true }, error: function (response, ioArgs) { if (ioArgs.xhr.status == 420) { // expected error alert( "error: " + ioArgs.xhr.responseText ) } else { //unexpected error alert("error: " + response) } } } ); }

handlers/create-book.php
<?php require_once "../models/Books.php"; $books = new Books(); $param = array_merge( $_GET, $_POST ); try { $title = trim($param['title']); $type = trim($param['type']); $qty = trim($param['qty']); if ($title == "") { throw new Exception("cannot have empty title"); } if (!preg_match("/^\d+$/",$qty)) { throw new Exception("qty format incorrect"); } $books->add( new Book( array("title" => $title, "type" => $type, "qty" => $qty,) ) ); } catch(Exception $x) { header("HTTP/1.0 420 My Own Error Code" ); die( $x->getMessage() ); }

Update Portion


update.php
<?php require_once "models/Books.php"; try { $books = new Books(); $all = $books->fetchAll( array("id", "title") ); } catch(Exception $x) { require_once "handlers/error.php"; exit(); } $page_title = "Update"; ?> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <title><?=$page_title?></title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <link rel="stylesheet" href="css/mystyle.css" /> <script type="text/javascript" src="/dojolib/dojo/dojo.js" djConfig="parseOnLoad:true"></script> <script type="text/javascript" src="js/update.js"></script> <script type="text/javascript"> dojo.addOnLoad( function() { getDisplay(true) } ) </script> </head> <body> <?php require "nav.php"; ?> <h2><?=$page_title?></h2> <form id="list"> <select onchange="getDisplay(false);return false;" name="id" id="list.id"> <option value="">------</option> <? foreach($all as $book): ?> <option value="<?= $book->id ?>" > <?= htmlspecialchars($book->title) ?> </option> <? endforeach ?> </select> </form> <form id="book"> <input type="hidden" name="id" /> <table cellpadding="5" > <tr> <td>id:</td><td id="book.display.id"></td> </tr> <tr> <td>title:</td> <td><input type='text' size="60" name="title" /></td> </tr> <tr> <td>type:</td> <td> <input type="radio" name="type" value="paper" id="book.paper" checked /> <label for="book.paper">paper</label> <input type="radio" name="type" value="cloth" id="book.cloth" /> <label for="book.cloth">cloth</label> </td> </tr> <tr> <td>qty:</td> <td><input type='text' size="5" name="qty" /></td> </tr> <tr> <td></td> <td><button id="book.button" onclick="updateBook();return false;">Update</button></td> </tr> </table> </form> </body> </html>

js/update.js
function getDisplay(shouldIsync) { dojo.xhrGet( { url: 'handlers/read-book.php', form: "list", handleAs: "json", sync: shouldIsync, load: function (response) { if (!dojo.byId("list").id.value) { dojo.byId('book.button').disabled = true clear_form_fields(); return; } dojo.byId('book.button').disabled = false dojo.byId('book').id.value = response.id dojo.byId('book.display.id').innerHTML = response.id dojo.byId('book').title.value = response.title if (response.type == "paper") { dojo.byId('book.paper').checked = true } else { dojo.byId('book.cloth').checked = true } dojo.byId('book').qty.value = response.qty }, error: function (response) { alert("error: " + response) } } ); } function updateBook() { dojo.xhrPost( { url: 'handlers/update-book.php', form: "book", load: function(response) { console.log(response) alert(response) clear_form_fields(); }, error: function (response) { alert("error: " + response) } } ); } function clear_form_fields() { dojo.byId('book.display.id').innerHTML = "" dojo.byId('book').title.value = "" dojo.byId('book').qty.value = "" dojo.byId('book.paper').checked = true dojo.byId('book.button').disabled = true dojo.byId('list.id').options[0].selected = true }

handlers/update-book.php
<?php require_once "../models/Books.php"; $param = array_merge( $_GET, $_POST ); print_r($param);


© Robert M. Kline