XTemplate and Controllers in Cotonti: Developer's Guide
The XTemplate templating engine has been the heart of the presentation system in Cotonti since its inception. From the earliest versions inherited from Seditio to the modern Verona 1.0.x branch, XTemplate remains the primary tool for separating HTML layout from PHP logic. This guide is intended for developers who want to deeply understand how XTemplate works, learn to create and modify templates, and use them in conjunction with controllers and plugins.
XTemplate is precisely a classic, not an outdated, templating engine. It has come a long way together with Cotonti and remains relevant, reliable, and deeply integrated into the core.
XTemplate blends perfectly with modern controllers, pagination, filters, and localization — a classic tool doesn’t need to be trendy or have a pompous name; it just needs to be convenient, simple, and reliable.
This article progresses from the general to the specific: we will start with basic concepts and gradually move to advanced techniques, including working with loops, localization, caching, and integration with modern Cotonti controllers. Each section includes code examples that you can use in your own projects.
1. XTemplate in the Cotonti Architecture
The Model-View-Controller (MVC) concept in Cotonti is not strictly implemented, but a clear separation of responsibilities is evident. XTemplate plays the role of the «View» — responsible for displaying data obtained from the model and prepared by the controller. It contains no business logic and can be easily replaced or extended by creating design themes.
Unlike many modern templating engines, XTemplate does not compile templates into PHP code; it operates in an interpretation mode at runtime. This provides simplicity and flexibility, though it may affect performance under very high load. However, for the vast majority of Cotonti-based websites, this approach is perfectly justified and has been proven over the years.
Before the advent of controllers, developers used the global variables $plugin_title, $plugin_subtitle, and $plugin_body, which were automatically substituted into the {PLUGIN_TITLE}, {PLUGIN_SUBTITLE}, and {PLUGIN_BODY} tags in the theme’s plugin.tpl file. This approach is also still applied today, but as Cotonti evolves, it is gradually giving way to controllers and a cleaner separation of code and presentation.
2. Basic Principles of XTemplate
XTemplate is an object‑oriented class whose instance is created at the moment of request processing. The main class file is located in system/xtemplate.php. It provides several key methods: assign(), parse(), and out(). Let’s examine them in more detail.
To start working with a template, you need to create an XTemplate object, passing the path to the template file. For example:
$t = new XTemplate(cot_tplfile('mymodule', 'plug'));
The cot_tplfile() function returns the full path to the file, taking into account the active design theme. The first argument is the file name without the extension; the second specifies the type: 'plug' for a plugin, 'module' for a module, 'core' for system templates. The system automatically searches for an overridden file in the theme first and then in the extension itself. This allows easy customization of the appearance without modifying the extension sources.
2.1. Tags and Their Purpose
A tag in XTemplate is a sequence of characters enclosed in curly braces, for example {MY_TAG}. Tags are case‑sensitive: {MY_TAG} and {my_tag} are different tags. They are interpreted by the system only during parsing (the parse() method), being replaced with values set via assign().
The assign() method can accept a single tag or an associative array of tags:
$t->assign('SINGLE_TAG', 'Value');
$t->assign([
'TITLE' => $title,
'CONTENT' => $text,
'IS_ADMIN' => Cot::$usr['isadmin']
]);
If a tag is not assigned a value, it will be removed from the output during parsing. This is an important feature that helps avoid empty constructs in the HTML.
2.2. Blocks and Parsing (parse)
A block in XTemplate is a named area of the template delimited by comments of the form <!-- BEGIN: BLOCK_NAME --> and <!-- END: BLOCK_NAME -->. Block names may contain any characters, but uppercase with underscores is typically used. Blocks can be nested, which allows for a hierarchical organization.
The outermost block is usually called MAIN. It is parsed automatically at the end of request processing if you are using a controller or a classic hook. All other blocks are parsed manually when you call $t->parse('MAIN.NESTED_BLOCK_NAME'). The dot in the name indicates the parent block. For example, MAIN.PAGE_ROW means that the block PAGE_ROW is inside the MAIN block.
A simple template example (mymodule.tpl):
<!-- BEGIN: MAIN -->
<h1>{PAGE_TITLE}</h1>
<table>
<!-- BEGIN: PAGE_ROW -->
<tr>
<td>{PAGE_ROW_ID}</td>
<td>{PAGE_ROW_TITLE}</td>
</tr>
<!-- END: PAGE_ROW -->
</table>
<!-- END: MAIN -->
PHP code to populate such a template with data from the database:
$t = new XTemplate(cot_tplfile('mymodule', 'plug'));
$t->assign('PAGE_TITLE', 'Page list');
$res = Cot::$db->query("SELECT page_id, page_title FROM $db_pages WHERE page_state = 0");
while ($row = $res->fetch()) {
$t->assign([
'PAGE_ROW_ID' => $row['page_id'],
'PAGE_ROW_TITLE' => htmlspecialchars($row['page_title'])
]);
$t->parse('MAIN.PAGE_ROW');
}
$t->parse('MAIN');
echo $t->text('MAIN');
Note that calling parse() inside the while loop forces XTemplate to immediately parse the PAGE_ROW block with the current tag values and append the result to the output buffer. If we did not call parse() inside the loop but only at the end, the tags would hold only the values of the last row.
3. Advanced Use of assign and Data Types
The assign() method is not limited to strings. You can pass numbers, boolean values, and even arrays if the template expects to handle them. However, XTemplate does not execute PHP code inside tags by default, so conditional logic uses conditional tags <!-- IF ... -->. The conditions themselves are processed by the Cotonti system at the rendering stage.
For example, to pass an admin flag and use it in a template:
$t->assign('IS_ADMIN', Cot::$usr['isadmin']); // true/false
In the template:
<!-- IF {IS_ADMIN} -->
<a href="{ADMIN_EDIT_URL}">Edit</a>
<!-- ENDIF -->
You can also use value modifiers in tags, for example {MY_DATA|strip_tags($this)} to remove HTML tags, {MY_DATE|cot_date('d.m.Y H:i', $this)} to format a date. The full list of available modifiers depends on the Cotonti version, but the core already includes several built‑in ones, and you can register your own through the coTemplate.custom.callback event (we will examine this mechanism later).
4. Modern Template Handling: Controllers and cot_generatePaginationTags
Starting with the Siena version, Cotonti began transitioning to a controller‑based architecture. In the current Verona 1.0.x, controllers are the standard way of handling requests for modules. XTemplate, meanwhile, has not lost its relevance — it remains the primary rendering engine within controller actions.
A typical controller action might look like this:
namespace cot\modules\pages\controllers;
use cot\controllers\BaseController;
class PageController extends BaseController
{
public function actionList(): string
{
$t = new XTemplate(cot_tplfile('pages.list', 'module'));
// data retrieval logic
// assign and parse
$t->parse('MAIN');
return $t->text('MAIN');
}
}
In this example, cot_tplfile() is now often called with the second parameter 'module', because the controller belongs to a module. The template itself should be located in the modules/pages/tpl/ directory or be overridden in the theme.
For generating pagination, Cotonti provides the cot_generatePaginationTags() function, which accepts the array returned by cot_pagenav() and returns an array of tags to assign to the template:
$pagenav = cot_pagenav('pages', $url_params, $d, $total, $perpage);
$t->assign(cot_generatePaginationTags($pagenav));
Now the template can use the standard PAGENAV block with the tags {PAGENAV_PAGES}, {PAGENAV_PREV}, {PAGENAV_NEXT}, etc.
5. Filters and Custom Callbacks in Templates
XTemplate supports filters that allow modifying a tag’s value at the output stage. The syntax is very simple: {TAG|filter:param}. Multiple filters can be combined: {TAG|filter1|filter2:arg}.
Built‑in Cotonti filters:
cot_date('format', $value) — formats a timestamp into a string;strip_tags($value) — removes HTML tags;htmlspecialchars($value) — escapes special characters;- as well as all standard PHP functions, if they are enabled in the configuration.
Developers can add their own filters by registering them through the coTemplate.custom.callback hook. An example of registering a simple myuppercase filter:
// in a plugin or init.php file
Cot::$hook->add('coTemplate.custom.callback', function ($callbacks) {
$callbacks['myuppercase'] = function ($value, $args) {
return mb_strtoupper($value, 'UTF-8');
};
return $callbacks;
});
Afterwards, you can write {SOME_VAR|myuppercase} in any template.
6. Templates in Hooks: Legacy and Modernity
Although controllers have become the standard, a large number of extensions are still built on the module and standalone hooks. In these, working with XTemplate is almost identical, except that the MAIN block is automatically parsed and output by the system through Cot::$out['module_body']. You don’t need to explicitly call out() or echo — just perform $t->parse('MAIN') and return control. However, developers should remember that when using the standalone hook, they must output the result themselves via $t->out('MAIN').
Example of a standalone plugin with XTemplate:
$t = new XTemplate(cot_tplfile('myplugin', 'plug'));
$t->assign('TITLE', 'Hello world');
$t->parse('MAIN');
$t->out('MAIN');
For modules built on the module hook, the output would look like this:
$t = new XTemplate(cot_tplfile('mymodule', 'module'));
// ... assign and parse ...
$t->parse('MAIN');
Cot::$out['module_body'] .= $t->text('MAIN');
7. Multilingualism in Templates
Localization in Cotonti is built on language arrays that are loaded via cot_langfile(). Translation strings are accessed in templates through the {PHP.L.string_key} tag. For example, {PHP.L.hello} will be replaced by the translation of the string from the language file. This is convenient for static text in the template, requiring no extra assign() calls in PHP.
For dynamic text that depends on data, translation should be performed in the controller and the already translated string passed through assign(). For example:
$t->assign('STATUS_TEXT', Cot::$L['status_' . $item['status']]);
8. Template Caching
XTemplate itself has no built‑in caching mechanism; however, Cotonti provides a static cache system at the page level. When using controllers, caching can be enabled with the #[Cacheable] attribute above the controller class (requires Cotonti 1.0+). This automatically saves the action’s result and serves it without re‑executing PHP until the cache expires. The lifetime is set in the configuration.
Caching does not affect templates that are parsed dynamically, but if your page is fully generated by a single action, you can significantly improve performance by enabling the controller cache.
9. Practical Example: Page List with Filter and Pagination
Let’s combine the acquired knowledge in a real‑world scenario. Suppose we have a «Book Catalog» module. We want to output a list of books with pagination and a genre filter. This will clearly demonstrate the interaction of XTemplate with a controller.
The BookController:
namespace cot\modules\libcat\controllers;
use cot\controllers\BaseController;
use cot\modules\libcat\models\BookRepository;
class BookController extends BaseController
{
public function actionList(): string
{
$genre = cot_import('genre', 'G', 'ALP');
$page = cot_import('d', 'G', 'INT', 1) - 1;
$perPage = 12;
$total = BookRepository::countByGenre($genre);
$books = BookRepository::findByGenre($genre, $page * $perPage, $perPage);
$pagenav = cot_pagenav('libcat', ['genre' => $genre], $page, $total, $perPage);
$paginationTags = cot_generatePaginationTags($pagenav);
$t = new XTemplate(cot_tplfile('book.list', 'module'));
$t->assign($paginationTags);
$t->assign('GENRE', $genre);
foreach ($books as $book) {
$t->assign([
'BOOK_ROW_ID' => $book['id'],
'BOOK_ROW_TITLE' => htmlspecialchars($book['title']),
'BOOK_ROW_AUTHOR'=> htmlspecialchars($book['author']),
'BOOK_ROW_PRICE' => number_format($book['price'], 2),
'BOOK_ROW_URL' => cot_url('libcat', ['a'=>'view','id'=>$book['id']])
]);
$t->parse('MAIN.BOOK_ROW');
}
$t->parse('MAIN');
return $t->text('MAIN');
}
}
The book.list.tpl template:
<!-- BEGIN: MAIN -->
<h1>Books in {GENRE}</h1>
<div class="row">
<!-- BEGIN: BOOK_ROW -->
<div class="col-md-3 mb-3">
<div class="card">
<div class="card-body">
<h5><a href="{BOOK_ROW_URL}">{BOOK_ROW_TITLE}</a></h5>
<p>Author: {BOOK_ROW_AUTHOR}</p>
<p>Price: {BOOK_ROW_PRICE}</p>
</div>
</div>
</div>
<!-- END: BOOK_ROW -->
</div>
<!-- PAGINATION BLOCK -->
<div class="pagination">
{PAGENAV_PREV}{PAGENAV_PAGES}{PAGENAV_NEXT}
</div>
<!-- END: MAIN -->
This example demonstrates how easily logic and presentation can be separated using XTemplate. The controller only populates the data, and the appearance is entirely determined by the template, which can be overridden in the theme without modifying the PHP code.
10. Template Inheritance and Theming
Cotonti supports a template hierarchy, allowing you to override only the necessary parts. If a file themes/your-theme/modules/libcat/book.list.tpl exists in the theme, it will be used instead of the default one from modules/libcat/tpl/. This gives designers complete freedom without affecting the module code.
Moreover, category‑specific templates can be created following the pattern book.list.{category_code}.tpl. When calling cot_tplfile('book.list', 'module'), the system first checks for a file with a suffix corresponding to the current category, and if not found, it uses the generic book.list.tpl. This capability is widely used in the Market module and others.
11. Debugging Templates
During development, it can be useful to output all assigned tags and their values. XTemplate includes a debug() method for this, which shows the object’s state. Calling $t->debug() returns an array of all set tags with their current values, helping to quickly identify mismatches.
Also, don’t forget about Cotonti’s logging: if you forget to close a block or make a typo, the system throws an exception indicating the file and line number, which greatly simplifies diagnostics.
12. Migrating Old Plugins to the New Approach
Many plugins written before controllers appeared use the global $plugin_body variable to output HTML. To modernize them, it is enough to wrap the logic in a controller class and, in the old hook, call a redirect to this controller. For example:
// in the hook file
Cot::$out['redirect'] = cot_url('myplugin', ['n' => 'main', 'a' => 'index']);
Inside the controller, you use XTemplate as usual. Gradually, all old plugins can be transitioned to the new standard, gaining the benefits of MVC and simplifying maintenance.
Conclusion
XTemplate in Cotonti is a classic templating engine — a time‑tested and deeply integrated subsystem. Knowledge of it is essential for every extension developer. With the advent of controllers, XTemplate has seamlessly integrated into the modern architecture, retaining its simplicity and flexibility. We have covered the basics of working with tags and blocks, advanced methods, integration with controllers, pagination, filters, localization, and caching. This knowledge is sufficient for building the vast majority of web applications on Cotonti.