6CC001 Workshop - week 04

From mi-linux
Revision as of 13:56, 30 November 2010 by 0607197 (talk | contribs)
Jump to navigationJump to search

Main Page >> Advanced Web Technologies >> Workbook >> Week 04

Your job today is to implement a blog similar to this one:

The different steps are:

  1. Step 1 - Create and populate database
  2. Step 2 - Create the index.php file, front controller and registry class
  3. Step 3 - Handling user request and creating appropriate command
  4. Step 4 - Accessing the database
  5. Step 5 - Creating our first view
  6. Step 6 - Adding messages
  7. Step 7 - Deleting messages

Let's take it one step at a time... and remember: read and understand the code before pasting it into your text editor!

Step 1 - Create and populate database

First, let’s create a very simple table to store our blog messages.

The following SQL code will create the table and populate it with a couple of dummy records. Simply paste it and run it in phpmyadmin.

CREATE TABLE IF NOT EXISTS `messages` (
  `id` int(11) NOT NULL auto_increment,
  `title` varchar(255) NOT NULL,
  `message` text NOT NULL,
  `date_added` datetime NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 AUTO_INCREMENT=19 ;

INSERT INTO `messages` (`id`, `title`, `message`, `date_added`) VALUES
(1, 'Test 1', 'Hello world!', '2009-07-27 15:06:29'),
(2, 'Test 2', 'Hiya', '2009-07-27 15:06:38');

Step 2 - Create the index.php file, front controller and registry class

As explained in the lecture, our application has a single entry point that handles all user requests: index.php

Well, index.php doesn’t really do much. It simply creates and runs the Front Controller object:

// index.php

// The index simply calls the front controller
require("Controller.php");
Controller::run();

Next, let's have a look at the Controller class itself.

Index.php calls the run function, which creates an instance of the Front Controller, and then runs a couple of internal functions:

  • init() does some initial set-up. In our case, it saves the database details to the registry.
  • handleRequest() does the actual work. Let’s keep it simple for now.
// Controller.php

require_once("Registry.php");

//############################################################################
// Front Controller class
//############################################################################
class Controller
{
  private function __construct(){}
  
  //############################################################################
  // Main "run" function
  //############################################################################    
  static function run()
  {
    $instance = new Controller;
    $instance->init();
    $instance->handleRequest();
  }
  
  //############################################################################
  // Init config stuff, e.g. database location, etc.
  //############################################################################   
  function init()
  {
    Registry::set("database_dsn", "mysql:host=localhost;dbname=YOURDBNAME");
    Registry::set("database_login", "YOURUSER");
    Registry::set("database_password", "YOURPASSWORD");
  }
  
  //############################################################################
  // Handles request from user
  //############################################################################   
  function handleRequest()
  {
    print("Hello world!");
  }
}

The Registry class is a simple front-end for an array. You can pass it values using the set() function, and retrieve them later using the get() function.

// Registry.php

//############################################################################
// Registry class, used to store values to be used across whole application
//############################################################################
class Registry
{
  private static $values = array();
  
  //############################################################################
  // Store a value
  //############################################################################    
  static function set($key, $val)
  {
    self::$values[$key] = $val;
  }
  
  //############################################################################
  // Get a value
  //############################################################################    
  static function get($key)
  {
    if(isset(self::$values[$key]))
    {
      return self::$values[$key];
    }
    return null;
  }
  
}

Checkpoint

It is still early days!

If you browse to the location of your index.php file, you should see "Hello world!". It doesn’t seem much, but we are already creating our Front Controller, and getting it to save our database connection details to the Registry.

We now need to handle the actual user request, i.e. display the page that the user asked for.

Step 3 - Handling user request and creating appropriate command

When you create a Request object, it simply populates itself with all the values contained in the $_REQUEST PHP super global variable. So anything passed by the user via $_POST, $_GET etc. will be stored in this object.

You can access the values above via the getProperty function.

// Request.php

//############################################################################
// Request class
//############################################################################
class Request
{
  private $properties;

  //############################################################################
  // constructor
  //############################################################################ 
  function __construct()
  {
    $this->init();
  }
  
  //############################################################################
  // Gets all parameters from GET, POST etc...
  //############################################################################   
  function init()
  {
    $this->properties = $_REQUEST;
    return;
  }
  
  //############################################################################
  // Functions to "set" and "get" properties
  //############################################################################   
  function getProperty($key)
  {
    if(isset($this->properties[$key]))
    {
      return $this->properties[$key];
    }
  }
  function setProperty($key, $val)
  {
    $this->properties[$key] = $val;
  }
}

Another very important tool needed by our Front Controller is the Command Resolver.

Put simply, it works like this: If the user types “index.php?cmd=BlogAddForm” in the address bar, then the command resolver will create and return a “Command_BlogAddForm” object.

It does a bit more than that, obviously. It first checks that the class exists, and that it is a valid class (i.e. it extends the Command object). If something goes wrong, it returns a default Command (Command_BlogIndex in our case).

// CommandResolver.php

require_once("Command.php");
require_once("Command_BlogIndex.php");

//############################################################################
// CommandResolver class
//############################################################################
class CommandResolver
{
  private static $base_cmd;
  private static $default_cmd;
  
  //############################################################################
  // Constructor
  //############################################################################    
  function __construct()
  {
    if(!self::$base_cmd)
    {
      // To check if requested command is a valid command
      self::$base_cmd = new ReflectionClass("Command");
      
      // Default command to be returned if an error occurs
      self::$default_cmd = new Command_BlogIndex();
    }
  }
  
  //############################################################################
  // getCommand creates the appropriate command class, 
  // depending on parameter passed in URL
  //############################################################################    
  function getCommand(Request $request)
  {
    // Get "cmd" parameter from request
    $cmd = $request->getProperty('cmd');

    // If no command passed in, use default one    
    if(!$cmd)
      return self::$default_cmd;
      
    $filepath = "Command_{$cmd}.php";
    $classname = "Command_{$cmd}";
    
    // Check if file containing command class exists
    if(file_exists($filepath))
    {
      require_once($filepath);
      
      // Check if command class exists
      if(class_exists($classname))
      {
        $cmd_class = new ReflectionClass($classname);
        
        // Check of class is a valid command
        if($cmd_class->isSubClassOf(self::$base_cmd))
        {
          // Return new instance of command
          return $cmd_class->newInstance();
        }
      }
    }
    
    // Couldn't find command, return default command
    return clone self::$default_cmd;
  }
}

Now that we have our Front Controller and Command Resolver, let’s create our first command!

First, we need an abstract Command class, from which all our commands will derive. It acts as an interface contract, and says “any class that wishes to be a Command must extend me and implement a doExecute() method.

Here is the code:

// Command.php

//############################################################################
// Command class
//############################################################################
abstract class Command
{
  // Make constructor final so sub-classes can't add parameters to it
  final function __construct(){}
  
  //############################################################################
  // Execute command
  //############################################################################   
  function execute(Request $request)
  {
    // General setup for all commands
    // ...
    
    // Then call specific command code
    $this->doExecute($request);
  }
  
  // Specific command code, to be implemented by each sub-class
  abstract function doExecute(Request $request);
}

Now let’s create our first command. Remember, it needs to extend the Command class.

As you can see, we are keeping the actual code simple for now… a simple print statement will do!

// Command_BlogIndex.php

//############################################################################
// SearchVenueCommand class
//############################################################################

require_once("ManagerMessage.php"); // Added By Daniel Rhys Hardy 0607197


class Command_BlogIndex extends Command
{
  //############################################################################
  // doExecute
  //############################################################################ 
  function doExecute(Request $request)
  {
    print("Blog Index command");
  }
}

Right, we have a Command Resolver, and our first Command. Let’s update our Controller to take all this into account. The handleRequest function now:

  1. Creates a Request objects (remember, it contains all the values passed via $_GET etc.)
  2. Create a Command Resolver
  3. Passes the Request to the Command Resolver, and gets an actual command back.
  4. Finally, it executes the command.
// Controller.php

  //############################################################################
  // Handles request from user
  //############################################################################   
  function handleRequest()
  {
    // Create object that handles request from user
    $request = new Request();
    
    // Create object that decides which command to create depending on request
    $cmd_r = new CommandResolver();
    
    // This function return the appropriate command
    $cmd = $cmd_r->getCommand($request);
    
    // Execute command
    $cmd->execute($request);
  }

Oh, also don't forget to add these at the top of your controller file:

require_once("Request.php");
require_once("CommandResolver.php");

Checkpoint

Your website should now display "Blog Index command".

Again I know, it’s not exactly an overwhelming result, but your application is now doing a lot more! As well as creating a Front Controller and a Registry, it also creates an appropriate Command and runs it!

Now let’s get our command to actually do interesting stuff.

Step 4 - Accessing the database

And by “doing stuff”, we obviously mean “get data from the database”!

First let's create our Manager class. It contains all the core database functionality: connecting to the server, parsing SQL queries, that sort of things.

It is defined as “abstract”, because it is not meant to be used directly. It serves as a base for more specialized Manager classes to be built on.

Anyway, here is the code:

// Manager.php

require_once("Registry.php");

//############################################################################
// Manager class
//############################################################################
abstract class Manager
{
  static $DB;
  static $stmts = array();
  
  //############################################################################
  // Constructor
  //############################################################################   
  function __construct()
  {
    // Get DB details from registry
    $database_dsn = Registry::get("database_dsn");
    $database_login = Registry::get("database_login");
    $database_password = Registry::get("database_password");
    
    // Create connection
    $pdo = new PDO($database_dsn, $database_login, $database_password);
    self::$DB = $pdo;

    // Set connection attributes    
    self::$DB->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
  }

  //############################################################################
  // prepareStatement: prepares anc caches SQL statement
  //############################################################################   
  function prepareStatement($stmt_s)
  {
    // Check if statement is already in cache
    if(isset(self::$stmts[$stmt_s]))
    {
      return self::$stmts[$stmt_s];
    }
    
    // Prepare stement, store in cache and return
    $stmt_handle = self::$DB->prepare($stmt_s);
    self::$stmts[$stmt_s] = $stmt_handle;
    return $stmt_handle;
  }
  
  //############################################################################
  // doStatement: runs SQL statement
  //############################################################################   
  protected function doStatement($stmt_s, $values_a)
  {
    $sth = $this->prepareStatement($stmt_s);
    
    // Closes the cursor, enabling the statement to be executed again.
    $sth->closeCursor();
    
    // Runs the statement
    $db_result = $sth->execute($values_a);
    return $sth;
  }
}

Next, let’s create our Manager class responsible for handling our blog messages. It is called ManagerMessage and “extends” our standard Manager class. It defines the SQL queries needed to handle messages, and adds functions such as getAllMessages and addMessage.

The code:

// ManagerMessage.php

require_once("Manager.php");

//############################################################################
// ManagerMessage class
//############################################################################
class ManagerMessage extends Manager
{
  static $add_message = "INSERT INTO messages(title,message,date_added) values(?,?,?)";
  static $list_messages = "SELECT id,title,message,date_added FROM messages ORDER BY date_added DESC";
  static $search_messages = "SELECT id,title,message,date_added FROM messages WHERE title LIKE ? ORDER BY date_added DESC";
  static $delete_message = "DELETE FROM messages WHERE id = ?";
  
  //############################################################################
  // addMessage: adds a message to the database
  //############################################################################ 
  function addMessage($title, $message)
  {
    $values = array($title, $message, date("Y/m/d H:i:s"));
    $this->doStatement(self::$add_message, $values);
  }
  
  //############################################################################
  // getAllMessages: selects all messages from the database
  //############################################################################ 
  function getAllMessages()
  {
    $stmt = $this->doStatement(self::$list_messages, null);
    $result = $stmt->fetchAll();
    return $result;
  }
  
  //############################################################################
  // searchMessages: searches for a message
  //############################################################################ 
  function searchMessages($keywords)
  {
    $values = array($keywords);
    $stmt = $this->doStatement(self::$search_messages, $values);
    $result = $stmt->fetchAll();
    return $result;
  }
  
  //############################################################################
  // deleteMessage: deletes a message
  //############################################################################ 
  function deleteMessage($id)
  {
    $values = array($id);
    $stmt = $this->doStatement(self::$delete_message, $values);
  }  
  
}

Now we can implement the doExecute function in our command (Command_BlogIndex), so it calls our newly written ManagerMessage class:

// Command_BlogIndex.php

  //############################################################################
  // doExecute
  //############################################################################ 
  function doExecute(Request $request)
  {
    // Create manager object
    $manager = new ManagerMessage();

    // Get data from database object
    $data = $manager->getAllMessages();
      
    // Dump data array to screen
    print_r($data);
  }

Checkpoint

Your website should now be displaying the data from the database in the form an an array:

Array ( [0] => Array ( [id] => 18 [0] => 18 [title] => Test 2 [1] => Test 2 [message] => Hiya [2] => Hiya [date_added] => 2009-07-27 15:06:38 [3] => 2009-07-27 15:06:38 ) [1] => Array ( [id] => 17 [0] => 17 [title] => Test 1 [1] => Test 1 [message] => Hello world! [2] => Hello world! [date_added] => 2009-07-27 15:06:29 [3] => 2009-07-27 15:06:29 ) )

We're nearly there!

Step 5 - Creating our first view

All we need now is to pass the data above to a view. Let's create all our views in a "views" sub-folder.

<!-- views/index.php -->

<?require_once("ViewHelper.php");?>

<?ViewHelper::DisplayHeader("Blog index");?>

<h1>Blog index</h1>

<p>You can <a href="index.php?cmd=BlogAddForm">add messages</a> !</p>

<form action="index.php" method="post">
 Search: <input name="search"> 
 <input type="submit" value="Search!"> 
</form>

<?
 foreach($data as $message)
 echo("<div class=\"message\">
 <h2>{$message['title']}</h2>
 <p>{$message['message']}</p>
 <a href=\"index.php?cmd=BlogDelete&id={$message['id']}\">Delete this message</a>
 </div>");
?>

<?ViewHelper::DisplayFooter();?>

The code itself it pretty simple. It loops through the data array using a foreach statement.

Note that instead of duplication the <HEAD> section in all our views, we have decided to move it to a helper function. All our views can then include the helper file and call the DisplayHeader and DisplayFooter function:

// views/ViewHelper.php

//############################################################################
// ViewHelper
//############################################################################
class ViewHelper
{
 //############################################################################
 // Displays HTML Header
 //############################################################################ 
 static function DisplayHeader($pageTitle = "")
 {
 echo("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">");
 echo("<html>");
 echo(" <head>");
 echo(" <title>{$pageTitle}</title>");
 echo(" <link type=\"text/css\" href=\"views/style.css\" rel=\"stylesheet\">");
 echo(" </head>");
 echo(" <body>");
 }
 
 //############################################################################
 // Displays HTML Footer
 //############################################################################ 
 static function DisplayFooter()
 {
 echo(" </body>");
 echo("</html>"); 
 }
}

All you need now is to include your view from your command:

// Command_BlogIndex.php

 //############################################################################
 // doExecute
 //############################################################################ 
 function doExecute(Request $request)
 {
 // Create manager object
 $manager = new ManagerMessage();

 // Get data from database object
 $data = $manager->getAllMessages();
 
 // Include view
 include("views/index.php");
 }

Note: I'll let you create your own style sheet, mine is boring anyway :)

Checkpoint

The home page of your blog should now be working!!!

Image:blog.gif

Step 6 - Adding messages

Next, let’s add a command to add blog messages. You might have noticed above that our “add messages” link (see above) points to “index.php?cmd=BlogAddForm”

So first, you need a new command called “Command_BlogAddForm”. It doesn’t do much, it simply displays the view containing the form. Here is the code:

// Command_BlogAddForm.php

//############################################################################
// SearchVenueCommand class
//############################################################################
class Command_BlogAddForm extends Command
{
  //############################################################################
  // doExecute
  //############################################################################ 
  function doExecute(Request $request)
  {
    // Include view
    include("views/add_form.php");
  }
}

And here is the view containing the form:

<!-- views/add_form.php -->

<?require_once("view_helper.php");?>

<?ViewHelper::DisplayHeader("Add message");?>

<h1>Blog add form</h1>

<p>Please fill in the fields</p>

<form action="index.php?cmd=BlogAdd" method="post">
  Title:<br><input name="title">
  <br>
  Message:<br><textarea name="message"></textarea>
  <br>
  <input type="submit" value="Add">
</form>
    
<?ViewHelper::DisplayFooter();?>

Again note that the "action" attribute of the <FORM> above points to "index.php?cmd=BlogAdd". So we need another action responsible for reading the user data from the form, and creating a new record in the database... here it is!

// Command_BlogAdd.php

//############################################################################
// Command_BlogAdd class
//############################################################################
class Command_BlogAdd extends Command
{
  //############################################################################
  // doExecute
  //############################################################################ 
  function doExecute(Request $request)
  {
    // Get data from request
    $title = $request->getProperty('title');
    $message = $request->getProperty('message');
    
    // Create manager object
    $manager = new ManagerMessage();    
    
    // Add to database
    $manager->addMessage($title, $message);
    
    // Redirect to index
    header("location:index.php");
  }
}

Note: This action doesn't have a view. It simply redirects the user to the index of the forum.

Checkpoint

You should now be able to add new messages... try it!

Step 7 - Deleting messages

Finally, let’s create a new action in charge of message deletion.

Again if you browse the mouse over the “delete this message” links, you will see URLs such as “index.php?cmd=BlogDelete&id=19”

So we need a new Command_BlogDelete class. Here is the code:

// Command_BlogDelete.php

//############################################################################
// SearchVenueCommand class
//############################################################################
class Command_BlogDelete extends Command
{
  //############################################################################
  // doExecute
  //############################################################################ 
  function doExecute(Request $request)
  {
    // Get data from request
    $id = $request->getProperty('id');
    
    // Create manager object
    $manager = new ManagerMessage();    
    
    // Add to database
    $manager->deleteMessage($id);
    
    // Redirect to index
    header("location:index.php");
  }
}

Nothing too complicated here...

Checkpoint

You should now be able to delete messages.

Note: It is always better practice to ask users to confirm deletion first... but you can do that bit ;)