I’ve been working on replacing the front-end for the-spot.net and thus, the front-end of phpBB forums, using only the phpBB extension framework, trying to avoid having to put custom code into the core files, as was required pre-3.2 for some of my features.
It turns out that the existing topics on the phpBB forums are not entirely helpful for doing something as advanced as this project has turned out to be, and most topics are trivially solved with clearing cache or reading the documentation.
Occasionally, I need to do something that requires multiple help forum threads and documentation pages, so as I come across those solutions, I’ll combine them into a post here, because that’s the purpose of this blog, after all.
Rendering Template Partials
The first super complicated issue to solve was rendering what amounts to “part” of a template (hereafter referred to as “Partials”), without the html
shell. So, let’s get right to it then…you didn’t come here for my life’s story.
Goal: AJAX HTML Response
To start, I have an infinite scroll that delivers a set of Topics, instead of using pagination. To avoid duplicating twig templating engine in JS, I’ll just deliver HTML, and use JS to update any existing elements, or append new elements.
Step 1: Create template
class
First, create a template.php file to add a method for rendering Partials. I’ve placed mine for the extension “tsn/tsn” at tsn/tsn/framework/logic/template.php
. Here’s a look at the basics of the file.
<?php
namespace tsn\tsn\framework\logic;
use phpbb\template\twig\twig;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Error\SyntaxError;
/**
* Class template
* Adds functionality for rendering template partials
* @package tsn\tsn\framework\logic
*/
class template extends twig
{
// Directories
const TEMPLATE_DIR = '@tsn_tsn/';
const PARTIALS_DIR = self::TEMPLATE_DIR . 'partials/';
// Partials
const P_MYSPOT_FEED = self::PARTIALS_DIR . 'tsn_myspot_feed.html';
const P_TOPIC_CARD = self::PARTIALS_DIR . 'tsn_topic_card.html';
/**
* @param string $templateConstant A template path name or constant
*
* @return false|string|null
*/
public function renderPartial($templateConstant)
{
try {
$output = $this->twig->render($templateConstant, $this->get_template_vars());
// Usually this is via AJAX, so compress the whitespace
$output = preg_replace('/\s+/', ' ', $output);
} catch (LoaderError $e) {
$output = false;
error_log('Template Loader error: ' . $e->getMessage() . ' :: ' . $e->getCode());
} catch (RuntimeError $e) {
$output = false;
error_log('Template Runtime error: ' . $e->getMessage() . ' :: ' . $e->getCode());
} catch (SyntaxError $e) {
$output = false;
error_log('Template Syntax error: ' . $e->getMessage() . ' :: ' . $e->getCode());
}
return $output;
}
}
It inherits phpbb\template\twig\twig
, and thus its constructor. It has constants to help combine the various directories and file names should any of them need to be changed along the way. And it adds a single method to allow the render of a partial template.
This new method is a wrapper for the call to the inherited twig
property and its render()
method. This new method receives a single parameter for the Partial template name to render. The template name (or constant) and template vars are then passed into the twig’s render method, and the HTML is returned. If the render method fails, it throws exceptions, so catch them and handle them as appropriate for your needs.
Optional: In my case, I am using this for AJAX right now, so I am reducing all sequential whitespace to a single space character.
Step 2: Service Decoration
Now that we have the class, let’s put it in play. This is done with Service Decoration in phpBB 3.2.x, which tells the phpBB code to use this class instead of the original class. In this case, anywhere we would have used $this->template
, it will invoke our class instead of the original class.
This block goes into your services.yml
file
tsn.tsn.framework.logic.template:
class: tsn\tsn\framework\logic\template
decorates: 'template'
arguments:
- '@path_helper'
- '@config'
- '@template_context'
- '@template.twig.environment'
- '%core.template.cache_path%'
- '@user'
- '@template.twig.extensions.collection'
- '@ext.manager'
The key line is the decorates: 'template'
to tell the extension manager which service to replace.
Step 3: Method Usage
As mentioned above, I am using this implementation in an AJAX controller to return HTML strings for further processing. When standing up my AJAX controller, I hint that this template variable is of my new template class.
For context, my AJAX controller route looks like this in services.yml
:
tsn.tsn.controller.ajax:
class: tsn\tsn\controller\ajax_controller
arguments:
- '@auth'
- '@auth.provider_collection'
- '@captcha.factory'
- '@config'
- '@content.visibility'
- '@dbal.conn'
- '@controller.helper'
- '@language'
- '@path_helper'
- '@request'
- '@template'
- '@user'
And for now, it looks like this in routing.yml
:
tsn_tsn_ajax:
pattern: /tsn/ajax/{route}
defaults: { _controller: tsn.tsn.controller.ajax:doIndex, route: "index" }
So the actual controller looks like this (with the extra stuff commented away)…
<?php // ... use statements /** * Class ajax_controller * @package tsn\tsn\controller */ class ajax_controller extends AbstractBase { /** @var stdClass the JSON response */ private $response = null; /** * ajax_controller constructor. * * // Other Params * @param \tsn\tsn\framework\logic\template $template * // Other params */ public function __construct(/* other params... , */ template $template /* , ... other params */) { parent::__construct(/* other params... , */ $template /* , ... other params */); $this->response = new stdClass(); $this->response->status = 0; // 0: error; 1: success, 2: info/warning $this->response->data = []; // whatever is necessary $this->response->message = null; // message for 0/2 status } /** * @param $route * * @return \Symfony\Component\HttpFoundation\Response * @uses url::AJAX_MYSPOT_FEED_PAGE */ public function doIndex($route) { // ... Setup User Access $statusCode = Response::HTTP_OK; switch ($route) { case url::AJAX_MYSPOT_FEED_PAGE: // ...do work to set template variables $this->response->status = 1; $this->response->data['html'] = $this->template->renderPartial(template::P_MYSPOT_FEED); break; default: $statusCode = Response::HTTP_NOT_FOUND; break; } $headers = ['Content-Type' => 'application/json; charset=UTF-8']; if (!empty($this->user->data['is_bot'])) { $headers['X-PHPBB-IS-BOT'] = 'yes'; } return new Response(json_encode($this->response), $statusCode, $headers); } }
The key line for usage is $this->template->renderPartial(template::P_MYSPOT_FEED);
with the constant for the template to be rendered as the only parameter.
Step 4: The Partial
And finally, the partial for this example looks like any other Twig partial:
{# This template spools up a page of Topic Cards for the MySpot Feed This template requires a `topics` blockvar to iterate over #} {% if topics %} {% for topic in topics %} {% include '@tsn_tsn/partials/tsn_topic_card.html' %} {% endfor %} {% endif %}
Key Takeaway
Avoid duplicating code where necessary.
- Use the existing template engine, and avoid recreating it in either PHP or JS.
- Use Partials to render the same Template everywhere, in as generic a way as possible. Using wrapping elements with modifier classes to affect minor visual changes to the inner elements.
- Add your own PHP classes & traits to handle shared logic between Controllers.
- Use Service Decoration to replace/extend existing phpBB Services with your own classes.