A bit or two on templates (Feb 8th, 2019)

A bit or two on templates
---------------------------
February 8th, 2019


It's time to set a list of things to do or add to the cms in the near future. I won't put them exactly in the order they will be dealt with, but more or less I think these will set a basic set of features the cms should have for normal usage:

1. Separate templates on subfolders and add some simple template configuration option in the configuration file;
2. Define a generic mechanism for content generation that can be derived in specific forms for articles, menus, contact forms, media and so on. Then implement some of these;
3. Use quarkCMS for bitnova site. Only by using it we'll discover missing features and unexpected issues;
4. For each content generator we'll have to handle configuration files or sections, some administration interface may become handy to do this stuff;
5. Create or use an existing wysiwyg editor for articles and do changes in it to allow code inclusion (maybe even with syntax highlighting);
6. Add a simple router to map various features and sections through readable urls;
7. Create a data layer abstraction so all content and config files can be retrieved either from disk, database or webservice

We'll start with the first point on the list, but first I would point out a few trivial things that need to be corrected in the existing CMS routines, such as:

function run()
{
	$content_id = -1;
	$lang_id = -1;

	$content_id = $_GET["content_id"];
	$lang_id = $_GET["lang_id"];

	//  rest of run code
}

When I wrote these lines in 2009 I wasn't going to be bothered by warnings generated by PHP because it could not retrieve non existing values from the url parameters. Actually the code is safe, because the $content_id and $lang_id variables are initialized before attempting to overwrite their values with GET parameters. However this is not optimal, involves writing warnings to logs or changes in the php.ini options and it can be prone to possible behavior changes in the future versions of the PHP interpretter. So an updated code should look something like this:

function run()
{
	$content_id = -1;
	if (isset($_REQUEST['content_id']) $content_id = $_REQUEST['content_id'];

	$lang_id = -1;
	if (isset($_REQUEST['lang_id']) $lang_id = $_REQUEST['lang_id'];

	//  rest of run code
}

Similar checks can be added in the loadContentDefs function instead of expecting the interpreter to assign default values to non existing fields in the parsed xml object.

Now on to the main topic, we need to decide what information we want to store in the definition file with regard to templates. I would argue that if we store all template related definitions in a specific folder and if all template folders are stored inside a single templates folder then all we need to specify in the definitions file is the path to the template we want to use. That's simple enough, add a single tag <template>relative/path/to/template</template> and we're done. The change in the loadContentDefs routine is also very simple:

function loadContentDefs()
{
	if (file_exists('content.xml'))
	{
		$xml = simplexml_load_file('content.xml');
                
		//  load site descriptor tags
		//  ...

		//  load template path
		if (isset($xml->template)) $this->template_path = $xml->template;
	
		//  ... load other stuff ...
	}
	else die ("Cannot load content definitions.");
}

When actually loading the template we need to look if the $template_path points to a folder or to an actual file. If it is a folder then we try to locate a probable file containing the template which can be either template.html or template.php, index.html or index.php. At least for now, we'll later decide if we need this to be more restricted or on the contrary.

function loadTemplate()
{
	//  locate the template file
	if (!file_exists($this->template_path)) return 'template not found';

	$template_file = $this->template_path;            
	if (is_dir($template_file))
	{
		//  attempt to find an actual code file
		$template_file .= DIRECTORY_SEPARATOR; //  reduce string concatenations during next checks
		if (file_exists($template_file.'template.html')) $template_file .= 'template.html';
		else if (file_exists($template_file.'template.php')) $template_file .= 'template.php';
		else if (file_exists($template_file.'index.html')) $template_file .= 'index.html';
		else if (file_exists($template_file.'index.php')) $template_file .= 'index.php';
		else return 'template not found';
	}

And here is the important bit of this topic: when loading the template it would be useful to be able to have a look at important bits we need to place in it. Instead of inserting php calls in a template like <?php TQuarkCMS::instance()->generateSomething(); ?> we could identify instead placeholder tags like <quark:generateSomething/> and replace them with generated content. At first glance this appear to be an overhead, because in order to do so we need either to do a search and replace or some parsing which may kill the performance on large template files, but there are also very good reasons to follow this. The simplest method to do this is to leave php to do the normal interpretation of the included file (and execution of any php snippet there might be) and catch the result in an output buffer, so the loadTemplate() function will continue like this:

	//  execute template file to a buffer
	ob_start();
	include $template_file;
	$buffer = ob_get_contents();
	ob_end_clean();
	
	return $buffer;
}

Here we first call for ob_start() which starts an output buffer, then we include the template file just we would normally do and then retrieve the contents of the output buffer with ob_get_contents(). ob_end_clean() does what it says, cleans the buffer and closes it. The output that we get should only contain client side script (html, js, css) because all php tags have already been processed by the interpreter. Now we can search or parse the result for placeholder tags and call any number of content generators and here are those good reasons:

1. We can have a varying number of content generator functions or objects referenced by placeholder tags so we can build a plugin mechanism for new functions or objects at any time and thus adding new functionality to the cms. When we don't find a corresponding content generator for a placeholder tag we can simply delete the tag without placing anything in its place;
2. We can extend this mechanism to any type of content, such as images, videos, forms, product previews, user comments and so on. There is no need to predefined generator functions in the TQuarkCMS class anymore;
3. In turn, for each generated content, we can repeat the same process of searching for custom tags and replacing them again with corresponding generated content and so on until we can't find any more custom tags. This will enable nested content, like having article text included in contact forms, formulas, menus, media or ads included in articles text etc;
4. It simplifies the syntax for the normal user in general and especially for content designers and it can be integrated in wysiwyg editors relatively easy (well, we'll see about that).

If we think a bit, this content nesting through placeholder tags actually can define the core functionality of the cms. That's it for now, we'll see about placeholder tags searching at a later time. Moved the template.html and style.css files in a subfolder named bitSimpleCMS as a reference to the framework's origin and thus created space for future templates.

Script executed in: ms
Page rendered in: ms
quarkCMS