I am writing a basic templating class for my own project. The basic usage is this:
$template = new Template('template_file.php');
$template->assignVariable('pageTitle', 'Home page');
$template->render();
Contents of ‘template_file.php’:
<?php print $pageTitle; ?>
This is what template class does step by step:
- Stores variables in a private array when assignVariable method is called
- When render method is called, extracts stored variables, includes template file in a
ob_start()andob_end_clean()block. Stores output in a variable withob_get_contents()and then prints stored output.
I know this is a very simple templating class but works as expected. The question is should I delegate the including the template file to another class? I had this question when I was writing the unit tests for this class. I thought that file system interaction should be encapsulated. What do you think? If you think that it should not, how can I mock including a file in my tests?
Maybe I just pass the contents of the template file to the class like this:
$templateContent = file_get_contents('template_file.php');
$template = new Template($templateContent);
...
Edit: I decided to encapsulate the input process of template class for the sake of writing better unit tests and encapsulation. But as johannes pointed out, I needed to use eval() for that purpose which seemed not right. Johannes pointed me to the direction of stream wrappers for mocking the including in unit tests. But that inspired a new idea on me. Here is what I am going to do; I will continue to use include() in my template class but this time with stream wrappers. I will pass protocol handler to my template class while initializing it. This way I can create my own stream wrappers for fetching template data from database or using a local variable. Here are the examples:
$template = new Template('file://template_file.php');
stream_wrapper_register('database', 'My_Database_Stream');
$template = new Template('database://templates/3'); // templates table, row id 3
stream_wrapper_register('var', 'My_Var_Stream');
$myTemplate = '<?php print "Hello world!"; ?>';
$template = new Template('var://myTemplate');
I have already implement custom stream wrapper for local variables. Here it is:
class My_Var
{
protected $position;
protected $variable;
function stream_open($path, $mode, $options, &$openedPath) {
$url = parse_url($path);
global $$url['host'];
$this->variable = $$url['host'];
$this->position = 0;
return true;
}
public function stream_read($count) {
$ret = substr($this->variable, $this->position, $count);
$this->position = strlen($ret);
return $ret;
}
public function stream_eof() {
return $this->position >= strlen($this->variable);
}
}
stream_wrapper_register('var', 'My_Var');
$myvar = '<?php print "mert"; ?>';
include 'var://myvar';
exit;
By passing the contents using file_get_contents() and such you have to use eval() for execution which is bad in multiple ways. One of the most relevant here is that an opcode cache can’t cache the file then. Doing an include(‘template_file.php’); let’s APC or others cache the compiled script.