Building a Custom PHP Framework with a custom template caching engine using Output Control functions | Abhi's Weblog
In this blog post I will build a custom PHP framework (MVC Architecture). Then go on to discuss in brief about the output control functions and finally show how to build a custom template caching engine using these functions for our framework.
We will choose a MVC architecture for our framework. Here is a basic directory structure for our custom framework:
/index.php
/.htaccess
/config.ini.php
/404.php
/view/test1.php
/view/test2 ...
.php
/model/model.class.php
/controller/controller.class.php
/log/logger.class.php
/log/log.log
/cache/cache.class.php
/cache/template.cache.class.php
/cache/template
/cache/template/test1.php.tmp
/cache/template/test2.php.tmp
The view, model, controller, log and cache directories contains the following framework modules respectively:
view directory contains our view level files. i.e. files containing our HTML, js, css code.
model directory contains the model class responsible for interacting with database and other storages
controller directory contains our controller class. Each incoming request is first received by the controller class constructor, which thereafter controls the flow of request in the framework
log directory contains our logger class. This class is auto loaded for every request providing a basic logger::log($log_message) logger method throughout the framework. This class logs all data in a file called log.log.
cache directory contains our cache class. For this blog tutorial, we will only write the template caching engine class. In production systems, we might have individual classes for other types of cache systems e.g. memcached (Read Memcached and “N” things you can do with it – Part 1[25] to know more about memcached and MySQL Query Cache, WP-Cache, APC, Memcache – What to choose[26] for a complete comparison lists of various other caching techniques.
Lets see in details, what all file each and every directory contain contains.
Root directory files
We have 4 files in our root directory, namely .htaccess, index.php, config.ini.php and 404.php in order of relevance. Lets look through the content of these files:
RewriteEngine on
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*) index.php
1st two lines essentially means that Switch on the apache rewrite module and set RewriteBase as / i.e. the root directory
Last 3 lines mean that, if incoming request is for a file or directory which physically exists under the root directory serve them otherwise route all other requests to index.php in the root directory
Hence now for an incoming request like http://localhost/test1.php[27]," class="reaction_link" rel="external">http://localhost/test1.php[27], apache will route the request to index.php in the root directory because there is no test1.php under the root directory. Cool, lets see what index.php has to offer.
index.php doesn’t do much except for including our core configuration file and controller class file. Controller class constructor is initiated as soon as the class file is included.
config.ini.php is our core configuration file. It provides the framework with an array called $config containing various information like: mysql database credentials, requested url details and various other global parameters. Lets see what all parameter does it provide us with.
array(
'name' = "http://".$_SERVER['HTTP_HOST'].'/',
'uri' = $_SERVER['REQUEST_URI'],
'url' = parse_url($_SERVER['REQUEST_URI']), // note it contains the parsed url contents
),
'mysql'=array(
'host' = 'localhost',
'name'= 'testdb',
'user' = 'root',
'pass' = 'password',
),
'cache'=array(
'template' = 'On', // template caching switched on by default
'memcached' = 'Off', // switch off memcached caching
),
);
?
$config['host'] array saves various parameter about the host itself, e.g. hostname, hosturi (the requested uri, hosturl (it contains the parse_url(hosturi)).
$config['mysql'] array contains mysql database parameters. However in this blog post we will not interact with databases.
$config['cache'] tells the framework what all caching modules are switched on.
404 Page
For this blog post, controller directory consists of a single class file. i.e. controller.class.php. We saw this being included by index.php in the root folder above. As soon as controller class is included, it’s constructor is invoked. Before we dig in more, lets see the controller class file:
controller.class.php
setTemplate();
}
}
$controller = new controller($config);
?
At the top, controller class includes the logger.class.php, cache.class.php and model.class.php files. At the bottom, the controller object is instantiated.
The constructor performs the following 5 tasks:
At first it generates a template name and a template path for the incoming request i.e. for http://localhost/[28], $config['template']['name']='index.php' and for http://localhost/test1.php[29]," class="reaction_link" rel="external">http://localhost/test1.php[29], $config['template']['name']='test1.php'.
Second it checks for 404. For the above generated template path, e.g. $config['template']['path']='view/test1.php', it checks whether this file exists inside root directory. If it doesn’t template path and names are set to 404.php
Thirdly, It invokes the template caching engine. i.e. $template_cache = new template_cache();
Forth, it includes the generated template path above i.e. include_once($config['template']['path']);
Fifth and finally, it caches the generated HTML, js, css code by the template file includes above. This is achieved by the following code, $template_cache-setTemplate();
Before we move our attention to, lets see in short the content of log and view directories.
Log directory contains our logger class. This class is auto loaded for each incoming request that is being routed to index.php in the root directory (as we saw above). The logger.class.php provides a static logger::log($log_message) method, which can be used throughout the framework for logging messages. We will be using it everywhere.
The logger class by default logs all data to a file called log.log.
View directory files
For this blog post, we have two simple test pages in view directory namely test1.php and test2.php, which can be access by typing http://localhost/test1.php[30]" class="reaction_link" rel="external">http://localhost/test1.php[30] and http://localhost/test2.php[31] respectively in the browser.
test1.php simply calls the model class method called model::test1data() (static method). This method extracts some dummy text from the database and returns it back.
Model directory files
Model directory contains the model class file. In production systems, model class file will provide various methods to select and insert data in the databases. However for this blog post we will simply return some static dummy test.
model.class.php
For this blog post, cache directory contains the main cache.class.php file which in turn includes various other cache classes e.g. template.cache.class.php
$value) {
// include all cache classes, which are swicted on
if($value == 'On') {
// naming convention is
include_once('cache/'.$key.'.cache.class.php');
}
}
?
PHP[32] is a very simple language. You can write a Hello World! code or calculate similarity between two strings (see similar_text()[33]), both with a single line of code. And hence there are a lot of fundamental concepts of PHP, which not only beginners but even some advanced coders can ignore. One such concept is Output Control in PHP[34].
The Output Control functions allow you to control when the output of your PHP script will be thrown to the browsers (console). i.e. You can pre-process the final html output (append, prepend, chip-chop, inserting ad-codes, url linking, keyword highlighting, template caching), which will otherwise be thrown on the browser. Interesting, isn’t it? Can you feel the power of Output Control functions?
ob_start[35]: This turns on output buffering. i.e. no output is sent from the script, instead the output is saved in an internal buffer. However output buffering doesn’t buffers your headers. ob_start() also takes an optional callback function name. The function is called when output buffer is flushed (see ob_flush()[36]) or cleaned (see ob_clean()[37]). We can access the this internal buffer using functions like ob_get_contents()[38]
ob_end_flush[39]: This function send the content of the buffer (if any) and turns off output buffering. We should always call functions like ob_get_contents()[40] before ob_end_flush(), since any changes after this functions will not reflect on the browser
We saw above some of the output control functions PHP has to offer. ob_start(), ob_get_contents() and ob_end_flush(); are the 3 functions we will use to create our custom template caching engine.
template.cache.class.php
init();
}
function init() {
// get template path
$this-template_cache_file = $this-generateTemplatePath();
// get template from cache if exists
$this-getTemplate();
// start output buffering
ob_start();
}
function generateTemplatePath() {
global $config;
// generate template file name
return $this-template_cache_dir.$config['template']['name'].$this-template_cache_file_ext;
}
function getTemplate() {
global $config;
// check if a cached template exists
if(file_exists($this-template_cache_file)) {
if(time() - filemtime($this-template_cache_file) template_cache_ttl) {
logger::log("Cache hit for template ".$config['template']['name']);
$content = file_get_contents($this-template_cache_file);
echo $content;
exit;
}
else {
logger::log("Cache stale for template ".$config['template']['name']);
return FALSE;
}
}
else {
logger::log("Cache miss for template ".$config['template']['name']);
return FALSE;
}
}
function setTemplate() {
global $config;
// get buffer
$content = ob_get_contents();
// save template
logger::log("Caching template ".$config['template']['name']);
$fh = fopen($this-template_cache_file, 'w');
fwrite($fh, $content);
fclose($fh);
// Flush the output buffer and turn off output buffering
ob_end_flush();
}
}
?
As we saw above in controller class, the template engine class was instantiated before including the actual template file. Template engine constructor do the following 3 tasks:
Generate a cached file name for the requested uri by calling the $this-generateTemplatePath(); method. e.g. if http://localhost/test1.php is the requested uri, test1.php.tmp is it’s static cached template
Secondly, it tries to fetch the cached template file by calling the method $this-getTemplate(); (read on for details of this method)
Finally it turns on output buffering by calling ob_start();
List of methods provided by template.cache.class.php are:
generateTemplatePath() method generates a cache file name for incoming request. By default extension of all cached files in “.tmp” and are stored under the /cache/template directory.
getTemplate() method do a number of tasks. First, it checks if a cached template exists for the requested uri. If it does not exists or if it is not a fresh cache (see $template_cache_ttl), this method simply returns control back to controller which go ahead and include the actual template file. However if the file exists and is fresh it reads the content of the file and throw back to browser. At this point control is no longer transferred back to the controller, hence saving various un-necessary processing and database calls.
setTemplate() method is called by controller after including the actual template file from under the view directory. Point to note is that, before getTemplate() returns control back to controller (in case of missed or stale cache), the template cache class constructor does switch on output buffering. And when setTemplate() method is called, we can access this buffer using output functions like ob_get_contents() and then save the template for next incoming request. Bingo!. Finally this method throw away the buffer to the browser using ob_end_flush();
To verify the flow of framework, I hit the url http://localhost/test1.php 3 times, with $template_cache_ttl = 10; (seconds).
Once after clearing the template cache folder
Once within next 10 seconds
And finally after 10 seconds
Here is how the log file looks like:
2009-08-16 19:50:49
Requested template name test1.php, path view/test1.php (1st REQUEST)
2009-08-16 19:50:49
Cache miss for template test1.php
2009-08-16 19:50:49
Returning test1data() from database
2009-08-16 19:50:49
Caching template test1.php
2009-08-16 19:50:54
Requested template name test1.php, path view/test1.php (2nd REQUEST)
2009-08-16 19:50:54
Cache hit for template test1.php
2009-08-16 19:51:03
Requested template name test1.php, path view/test1.php (3rd REQUEST)
2009-08-16 19:51:03
Cache stale for template test1.php
2009-08-16 19:51:03
Returning test1data() from database
2009-08-16 19:51:03
Caching template test1.php
Moving forward, What’s Next? Extending template.cache.class.php
Template cache class can be extended to do a lot more, other than caching the template files. For instance we might want to perform (chip-chop, append, prepend etc) a few tasks, before we cache the final template and throw back to the browser. Few tasks which look quite obvious to me are:
Short Codes: We can insert short codes in our HTML templates, which later on can be expanded into full fledged codes. e.g. For embedding a YouTube video, we can simply put something like [[YouTube yjPBkvYh-ss]] into test1.php. And in setTemplate() method we can call helper/plugin methods to process such short codes. More professionally, we can add hooks for various tasks we might want to perform before caching the template. Read How to add wordpress like add_filter hooks in your PHP framework[41] for a more professional approach.
Inserting page header and footer: Instead of including page header and footer inside test1.php, we can simply put our main code inside test1.php. Then before caching the template file, we can append and prepend header and footer modules to the buffer of each page. Thus avoiding including the same header and footer files across various pages.
HTML module caching: There are several instances where we can have a common module across all pages. For instance, I can have a events module across all my pages, which basically displays a calendar with various events for the week or month marked on it. The event details are extracted from the database. Since this module of mine is a static HTML chunk for atleast a week, I would like to have a difference cache for this module. Intelligently hooking up these modules with template caching engine, can allow us to do module level caching
I can probably write down 10-15
Hide
- 21 comments on this story
PRO