Hostingheaderbarlogoj
Join InMotion Hosting for $3.49/mo & get a year on Tuts+ FREE (worth $180). Start today.
Advertisement

Create a PHP5 Framework - Part 2

by
Gift

Want a free year on Tuts+ (worth $180)? Start an InMotion Hosting plan for $3.49/mo.

With the basic structure for our Framework in place, it is time to start adding functionality to it. In this tutorial we will create a template manager and database handler, bringing us a step closer to a powerful Framework fit for use for almost any project. If you haven't already, be sure to review Part 1 of this series first!

MVC: Tweak the structure

In the first part of this tutorial, we created a folder called controllers to store the business logic for our applications. As daok pointed out in a comment, this isn't the best place for all of the business logic, and that a model should be used to store this logic. Previously, I have always used the database itself as the model in the majority of my applications, however, seperating this out a little more will make our framework even more powerful, and easier to extend.

So, what is MVC? MVC is a design pattern (as was the Singleton and Registry patterns we looked at in part 1), and it stands for Model View Controller, and the aim of this pattern is to seperate the business logic, user interface actions and the user interface from one another. Although we are not going to do anything with our models and controllers just yet, let's update our frameworks folder structure to include the "models" folder. The model will contain the main business logic, and the controller will deal with user interaction (e.g. submitting data, such as a comment). NB: Our __autoload function does not need to be changed.

Database Handler

Most websites and web applications which make use of PHP also make use of a database engine, such as MySQL. If we keep all of our database related functions in the same place, then we can (in theory) easily change the database engine we use. We can also make certain operations easier, such as inserting records, updating records or deleting records from the database. It can also make it easier when dealing with multiple database connections.

So...what should our database handler do:

  • Manage connections to the database
  • Try to provide some level of abstraction from the database
  • Cache queries so we can use them later
  • Make common database operations easier

Let's look at the code for our database handler, then we will discuss it afterwards.

<?php

/**
 * Database management and access class
 * This is a very basic level of abstraction
 */
class database {
	
	/**
	 * Allows multiple database connections
	 * probably not used very often by many applications, but still useful
	 */
	private $connections = array();
	
	/**
	 * Tells the DB object which connection to use
	 * setActiveConnection($id) allows us to change this
	 */
	private $activeConnection = 0;
	
	/**
	 * Queries which have been executed and then "saved for later"
	 */
	private $queryCache = array();
	
	/**
	 * Data which has been prepared and then "saved for later"
	 */
	private $dataCache = array();
	
	/**
	 * Record of the last query
	 */
	private $last;
	
	
	/**
	 * Hello
	 */
    public function __construct()
    {
    	
    }
    
    /**
     * Create a new database connection
     * @param String database hostname
     * @param String database username
     * @param String database password
     * @param String database we are using
     * @return int the id of the new connection
     */
    public function newConnection( $host, $user, $password, $database )
    {
    	$this->connections[] = new mysqli( $host, $user, $password, $database );
    	$connection_id = count( $this->connections )-1;
    	if( mysqli_connect_errno() )
    	{
    		trigger_error('Error connecting to host. '.$this->connections[$connection_id]->error, E_USER_ERROR);
		} 	
    	
    	return $connection_id;
    }
    
    /**
     * Close the active connection
     * @return void
     */
    public function closeConnection()
    {
    	$this->connections[$this->activeConnection]->close();
    }
    
    /**
     * Change which database connection is actively used for the next operation
     * @param int the new connection id
     * @return void
     */
    public function setActiveConnection( int $new )
    {
    	$this->activeConnection = $new;
    }
    
    /**
     * Store a query in the query cache for processing later
     * @param String the query string
     * @return the pointed to the query in the cache
     */
    public function cacheQuery( $queryStr )
    {
    	if( !$result = $this->connections[$this->activeConnection]->query( $queryStr ) )
    	{
		    trigger_error('Error executing and caching query: '.$this->connections[$this->activeConnection]->error, E_USER_ERROR);
		    return -1;
		}
		else
		{
			$this->queryCache[] = $result;
			return count($this->queryCache)-1;
		}
    }
    
    /**
     * Get the number of rows from the cache
     * @param int the query cache pointer
     * @return int the number of rows
     */
    public function numRowsFromCache( $cache_id )
    {
    	return $this->queryCache[$cache_id]->num_rows;	
    }
    
    /**
     * Get the rows from a cached query
     * @param int the query cache pointer
     * @return array the row
     */
    public function resultsFromCache( $cache_id )
    {
    	return $this->queryCache[$cache_id]->fetch_array(MYSQLI_ASSOC);
    }
    
    /**
     * Store some data in a cache for later
     * @param array the data
     * @return int the pointed to the array in the data cache
     */
    public function cacheData( $data )
    {
    	$this->dataCache[] = $data;
    	return count( $this->dataCache )-1;
    }
    
    /**
     * Get data from the data cache
     * @param int data cache pointed
     * @return array the data
     */
    public function dataFromCache( $cache_id )
    {
    	return $this->dataCache[$cache_id];
    }
    
    /**
     * Delete records from the database
     * @param String the table to remove rows from
     * @param String the condition for which rows are to be removed
     * @param int the number of rows to be removed
     * @return void
     */
    public function deleteRecords( $table, $condition, $limit )
    {
    	$limit = ( $limit == '' ) ? '' : ' LIMIT ' . $limit;
    	$delete = "DELETE FROM {$table} WHERE {$condition} {$limit}";
    	$this->executeQuery( $delete );
    }
    
    /**
     * Update records in the database
     * @param String the table
     * @param array of changes field => value
     * @param String the condition
     * @return bool
     */
    public function updateRecords( $table, $changes, $condition )
    {
    	$update = "UPDATE " . $table . " SET ";
    	foreach( $changes as $field => $value )
    	{
    		$update .= "`" . $field . "`='{$value}',";
    	}
    	   	
    	// remove our trailing ,
    	$update = substr($update, 0, -1);
    	if( $condition != '' )
    	{
    		$update .= "WHERE " . $condition;
    	}
    	
    	$this->executeQuery( $update );
    	
    	return true;
    	
    }
    
    /**
     * Insert records into the database
     * @param String the database table
     * @param array data to insert field => value
     * @return bool
     */
    public function insertRecords( $table, $data )
    {
    	// setup some variables for fields and values
    	$fields  = "";
		$values = "";
		
		// populate them
		foreach ($data as $f => $v)
		{
			
			$fields  .= "`$f`,";
			$values .= ( is_numeric( $v ) && ( intval( $v ) == $v ) ) ? $v."," : "'$v',";
		
		}
		
		// remove our trailing ,
    	$fields = substr($fields, 0, -1);
    	// remove our trailing ,
    	$values = substr($values, 0, -1);
    	
		$insert = "INSERT INTO $table ({$fields}) VALUES({$values})";
		$this->executeQuery( $insert );
		return true;
    }
    
    /**
     * Execute a query string
     * @param String the query
     * @return void
     */
    public function executeQuery( $queryStr )
    {
    	if( !$result = $this->connections[$this->activeConnection]->query( $queryStr ) )
    	{
		    trigger_error('Error executing query: '.$this->connections[$this->activeConnection]->error, E_USER_ERROR);
		}
		else
		{
			$this->last = $result;
		}
		
    }
    
    /**
     * Get the rows from the most recently executed query, excluding cached queries
     * @return array 
     */
    public function getRows()
    {
    	return $this->last->fetch_array(MYSQLI_ASSOC);
    }
    
    /**
     * Gets the number of affected rows from the previous query
     * @return int the number of affected rows
     */
    public function affectedRows()
    {
    	return $this->$this->connections[$this->activeConnection]->affected_rows;
    }
    
    /**
     * Sanitize data
     * @param String the data to be sanitized
     * @return String the sanitized data
     */
    public function sanitizeData( $data )
    {
    	return $this->connections[$this->activeConnection]->real_escape_string( $data );
    }
    
    /**
     * Deconstruct the object
     * close all of the database connections
     */
    public function __deconstruct()
    {
    	foreach( $this->connections as $connection )
    	{
    		$connection->close();
    	}
    }
}
?>

Before discussing this in more detail, I should point out that this database handler is very basic. We could provide complete abstraction by not executing queries directly, but instead constructing queries based on paramaters to a query function, and then executing it.

Our delete, insert and update record methods make it easier to perform some common tasks (as I mentioned above we could extend this to do much much more), by only providing information such as the table name, an array of fields and coresponding values, limit values and conditions. Queries can also be "cached" so that we can do things with them later. I find this feature (as well as the ability to "cache" arrays of data) is very handy when combined with a template manager, as we can easily iterate through rows of data and populate it into our templates with little fuss, as you will see when we look at the template manager.

// insert record
$registry->getObject('db')->insertRecords( 'testTable', array('name'=>'Michael' ) );
// update a record
$registry->getObject('db')->updateRecords( 'testTable', array('name'=>'MichaelP' ), 'ID=2' );
// delete a record (well, upto 5 in this case)
$registry->getObject('db')->deleteRecords( 'testTable', "name='MichaelP'", 5 );

We can also work with multiple database connections relatively easily, so long as we switch between the appropriate connections when we need to (although this won't work when caching queries and retrieving them via our template manager without further work), for example, the code snippet below would allow us to delete records from two databases.

// our second database connection (let's assume we already have a connection to our main DB)
$newConnection = $registry->getObject('db')->newConnection('localhost', 'root', 'password', 'secondDB');
// delete from the primary db connection
$registry->getObject('db')->deleteRecords( 'testTable', "name='MichaelP'", 5 );
// change our active db connection, to allow future queries to be on the second connection
$registry->getObject('db')->setActiveConnection( $newConnection );
// delete from the secondary db connection
$registry->getObject('db')->deleteRecords( 'testTable', "name='MichaelP'", 5 );
// revert the active connection so future queries are on the primary db connection
$registry->getObject('db')->setActiveConnection( 0 );

How might we want to extend this class?

  • Full abstraction
  • Make use of inheritance, create an interface and have database classes inherit from it, each for different database engines
  • Store the connection ID's along with the query when caching queries
  • Improve data sanitizing, depending on the type of data we wish to sanitize

Template Manager

The template manager will handle all of the output, it needs to be able to work with various different template files, replace placeholders (I call them tags) with data and iterate through parts of the template with multiple rows of data from the database.

To make things easier, we will make use of a page class to contain the content related to the page, this also makes it easier for us to extend this and add features to it later. The template manager will manage this object.

<?php

// prevent this file being called directly
if ( ! defined( 'PCAFW' ) ) 
{
	echo 'This file can only be called via the main index.php file, and not directly';
	exit();
}

/**
 * Template manager class
 */
class template {

	private $page;
	
	/**
	 * Hello!
	 */
    public function __construct() 
    {
	    include( APP_PATH . '/PCARegistry/objects/page.class.php');
	    $this->page = new Page();

    }
    
    /**
     * Add a template bit onto our page
     * @param String $tag the tag where we insert the template e.g. {hello}
     * @param String $bit the template bit (path to file, or just the filename)
     * @return void
     */
    public function addTemplateBit( $tag, $bit )
    {
		if( strpos( $bit, 'skins/' ) === false )
		{
		    $bit = 'skins/' . PCARegistry::getSetting('skin') . '/templates/' . $bit;
		}
		$this->page->addTemplateBit( $tag, $bit );
    }
    
    /**
     * Put the template bits into our page content
     * Updates the pages content
     * @return void
     */
    private function replaceBits()
    {
	    $bits = $this->page->getBits();
	    foreach( $bits as $tag => $template )
	    {
		    $templateContent = file_get_contents( $bit );
		    $newContent = str_replace( '{' . $tag . '}', $templateContent, $this->page->getContent() );
		    $this->page->setContent( $newContent );
	    }
    }
    
    /**
     * Replace tags in our page with content
     * @return void
     */
    private function replaceTags()
    {
	    // get the tags
	    $tags = $this->page->getTags();
	    // go through them all
	    foreach( $tags as $tag => $data )
	    {
		    if( is_array( $data ) )
		    {
			  
			    if( $data[0] == 'SQL' )
			    {
				    // it is a cached query...replace DB tags
				    $this->replaceDBTags( $tag, $data[1] );
			    }
			    elseif( $data[0] == 'DATA' )
			    {
				     // it is some cached data...replace data tags
				    $this->replaceDataTags( $tag, $data[1] );
			    }
	    	}
	    	else
	    	{	
		    	// replace the content	    	
		    	$newContent = str_replace( '{' . $tag . '}', $data, $this->page->getContent() );
		    	// update the pages content
		    	$this->page->setContent( $newContent );
	    	}
	    }
    }
    
    /**
     * Replace content on the page with data from the DB
     * @param String $tag the tag defining the area of content
     * @param int $cacheId the queries ID in the query cache
     * @return void
     */
    private function replaceDBTags( $tag, $cacheId )
    {
	    $block = '';
		$blockOld = $this->page->getBlock( $tag );
		
		// foreach record relating to the query...
		while ($tags = PCARegistry::getObject('db')->resultsFromCache( $cacheId ) )
		{
			$blockNew = $blockOld;
			// create a new block of content with the results replaced into it
			foreach ($tags as $ntag => $data) 
	       	{
	        	$blockNew = str_replace("{" . $ntag . "}", $data, $blockNew); 
	        }
	        $block .= $blockNew;
		}
		$pageContent = $this->page->getContent();
		// remove the seperator in the template, cleaner HTML
		$newContent = str_replace( '<!-- START ' . $tag . ' -->' . $blockOld . '<!-- END ' . $tag . ' -->', $block, $pageContent );
		// update the page content
		$this->page->setContent( $newContent );
	}
    
	/**
     * Replace content on the page with data from the cache
     * @param String $tag the tag defining the area of content
     * @param int $cacheId the datas ID in the data cache
     * @return void
     */
    private function replaceDataTags( $tag, $cacheId )
    {
	    $block = $this->page->getBlock( $tag );
		$blockOld = $block;
		while ($tags = PCARegistry::getObject('db')->dataFromCache( $cacheId ) )
		{
			foreach ($tags as $tag => $data) 
	       	{
		       	$blockNew = $blockOld;
	        	$blockNew = str_replace("{" . $tag . "}", $data, $blockNew); 
	        }
	        $block .= $blockNew;
		}
		$pageContent = $this->page->getContent();
		$newContent = str_replace( $blockOld, $block, $pageContent );
		$this->page->setContent( $newContent );
    }
    
    /**
     * Get the page object
     * @return Object 
     */
    public function getPage()
    {
	    return $this->page;
    }
    
    /**
     * Set the content of the page based on a number of templates
     * pass template file locations as individual arguments
     * @return void
     */
    public function buildFromTemplates()
    {
	    $bits = func_get_args();
	    $content = "";
	    foreach( $bits as $bit )
	    {
		    
		    if( strpos( $bit, 'skins/' ) === false )
		    {
			    $bit = 'skins/' . PCARegistry::getSetting('skin') . '/templates/' . $bit;
		    }
		    if( file_exists( $bit ) == true )
		    {
			    $content .= file_get_contents( $bit );
		    }
		    
	    }
	    $this->page->setContent( $content );
    }
    
    /**
     * Convert an array of data (i.e. a db row?) to some tags
     * @param array the data 
     * @param string a prefix which is added to field name to create the tag name
     * @return void
     */
    public function dataToTags( $data, $prefix )
    {
	    foreach( $data as $key => $content )
	    {
		    $this->page->addTag( $key.$prefix, $content);
	    }
    }
    
    public function parseTitle()
    {
	    $newContent = str_replace('<title>', '<title>'. $page->getTitle(), $this->page->getContent() );
	    $this->page->setContent( $newContent );
    }
    
    /**
     * Parse the page object into some output
     * @return void
     */
    public function parseOutput()
    {
	    $this->replaceBits();
	    $this->replaceTags();
	    $this->parseTitle();
    }
    
    
    
}
?>

So, what exactly does this class do?

Creates our page object, and bases it from template files, the page object contains the content and information which is needed to make-up the HTML of the page. We then buildFromTemplate('templatefile.tpl.php', 'templatefile2.tpl.php') to get the initial content for our page, this method takes any number of template files as its arguments, and stitches them together in order, useful for header, content and footer templates.

Manages the content associated with the page by helping the page object maintain a record of data to be replaced into the page, and also additional template bits which need to be incorporated into the page (addTemplateBit('userbar','usertoolsbar.tpl.php')).

Adds data and content to the page by performing various replace operations on the page content, including retrieving results from a cached query and adding them to the page.

The template file needs to mark within itself where a cached query needs to be retrieved and the data from the query replaced. When the template manager encounters a tag to replace which is a query, it gets the chunk of the page where it needs to iterate through by calling getBlock('block') on the page object. This chunk of content is then copied for each record in the query, and has tags within it replaced with the results from the query. We will take a look at how this looks in the template later in this tutorial.

Template Manager: Page

The page object is managed by the template manager, and it used to contain all of the details related to the page. This leaves the template manager free to manage, while making it easier for us to extend the functionality of this at a later date.

<?php

/**
 * This is our page object
 * It is a seperate object to allow some interesting extra functionality to be added
 * Some ideas: passwording pages, adding page specific css/js files, etc
 */
class page {

	// room to grow later?
	private $css = array();
	private $js = array();
	private $bodyTag = '';
	private $bodyTagInsert = '';
	
	// future functionality?
	private $authorised = true;
	private $password = '';
	
	// page elements
	private $title = '';
	private $tags = array();
	private $postParseTags = array();
	private $bits = array();
	private $content = "";
	
	/**
	 * Constructor...
	 */
    function __construct() { }
    
    public function getTitle()
    {
    	return $this->title;
    }
    
    public function setPassword( $password )
    {
    	$this->password = $password;
    } 
    
    public function setTitle( $title )
    {
	    $this->title = $title;
    }
    
    public function setContent( $content )
    {
	    $this->content = $content;
    }
    
    public function addTag( $key, $data )
    {
	    $this->tags[$key] = $data;
    }
    
    public function getTags()
    {
	    return $this->tags;
    }
    
    public function addPPTag( $key, $data )
    {
	    $this->postParseTags[$key] = $data;
    }
    
    /**
     * Get tags to be parsed after the first batch have been parsed
     * @return array
     */
    public function getPPTags()
    {
	    return $this->postParseTags;
    }
    
    /**
     * Add a template bit to the page, doesnt actually add the content just yet
     * @param String the tag where the template is added
     * @param String the template file name
     * @return void
     */
    public function addTemplateBit( $tag, $bit )
    {
	    $this->bits[ $tag ] = $bit;
    }
    
    /**
     * Get the template bits to be entered into the page
     * @return array the array of template tags and template file names
     */
    public function getBits()
    {
	    return $this->bits;
    }
    
    /**
     * Gets a chunk of page content
     * @param String the tag wrapping the block ( <!-- START tag --> block <!-- END tag --> )
     * @return String the block of content
     */
    public function getBlock( $tag )
    {
		preg_match ('#<!-- START '. $tag . ' -->(.+?)<!-- END '. $tag . ' -->#si', $this->content, $tor);
		
		$tor = str_replace ('<!-- START '. $tag . ' -->', "", $tor[0]);
		$tor = str_replace ('<!-- END '  . $tag . ' -->', "", $tor);
		
		return $tor;
    }
    
    public function getContent()
    {
	    return $this->content;
    }
  
}
?>

How can this class be extended and improved?

  • PostParseTags: You may wish to have tags replaced after most of the page has been parsed, maybe content in the database contains tags which need to be parsed.
  • Passworded pages: Assign a password to a page, check to see if the user has the password in a cookie or a session to allow them to see the page.
  • Restricted pages (although we need our authentication components first!)
  • Altering the
  • Dynamically adding references to javascript and css files based on the page or application.

Load core objects

Now that we have some objects which our registry is going to store for us, we need to tell the registry which objects these are. I've created a method in the PCARegistry object called loadCoreObjects which (as it says) loads the core objects. This means can can just call this from our index.php file to load the registry with these objects.

public function storeCoreObjects()
{
	$this->storeObject('database', 'db' );
	$this->storeObject('template', 'template' );
}

This method can be altered later to encorporate the other core objects the registry should load, of course there may be objects which we want our registry to manage, but only depending on the application the framework is used for. These objects would be loaded outside of this method.

Some Data

So that we can demonstrate the new features added to our framework, we need a database to make use of the database handler, and some of the template management functions (where we replace a block of content with the rows in the database).

The demonstration site we will make with our framework by the end of this series of tutorials is a website with a members directory, so let's make a very basic database table for members profiles, containing an ID, name, and email address.

Obviously, we need a few rows of data in this table!

A quick template

In order for anything to be displayed, we need a basic template, where we will list the data from our members table.

<html>
<head>
	<title> Powered by PCA Framework</title>
</head>
<body>
<h1>Our Members</h1>
<p>Below is a list of our members:</p>
<ul>
<!-- START members -->
<li>{name} {email}</li>
<!-- END members -->
</ul>
</body>
</html>

The START members and END members HTML comments denote the members block (which is obtained via the getBlock() method on the page), this is where the template manager will iterate through the records in the database and display them.

Framework in use

Now, we need to bring this all together, with our index.php file:

// require our registry
require_once('PCARegistry/pcaregistry.class.php');
$registry = PCARegistry::singleton();

// store those core objects
$registry->storeCoreObjects();

// create a database connection
$registry->getObject('db')->newConnection('localhost', 'root', '', 'pcaframework');

// set the default skin setting (we will store these in the database later...)
$registry->storeSetting('default', 'skin');

// populate our page object from a template file
$registry->getObject('template')->buildFromTemplates('main.tpl.php');

// cache a query of our members table
$cache = $registry->getObject('db')->cacheQuery('SELECT * FROM members');

// assign this to the members tag
$registry->getObject('template')->getPage()->addTag('members', array('SQL', $cache) );

// set the page title
$registry->getObject('template')->getPage()->setTitle('Our members');

// parse it all, and spit it out
$registry->getObject('template')->parseOutput();
print $registry->getObject('template')->getPage()->getContent();

If we now view this page in our web browser, the results of the query are displayed on the page:

Coming in part 3...

In part three we will take a slight detour from the development side of our Framework, and look at how to design with our framework in mind, and how to slice up HTML templates so that they are suitable for our framework. When we start to build our first application with our framework, we will look in more detail at some of the workings of these classes. Finally, thank you for your comments last time!

  • Subscribe to the NETTUTS RSS Feed for more daily web development tuts and articles.


Advertisement