Delayed output focuses on rendering the content/markup for all the various regions of our final document ahead of time, but delaying that output until everything is ready.
- How does delayed output work?
- How to use delayed output
- Populating placeholders (variables) with more
- Defining new placeholders to represent new regions
- Adding an _init.php file to the mix (a best practice)
- Automatic inclusions
- Alternatives to placeholder variables
- Drawbacks of delayed output
How does delayed output work?
Delayed output leaves a lot of flexibility for content or placement adjustments during the output generation process, because nothing is final till the end.
Rather than outputting content directly (like with direct output), we instead populate our content to placeholders (or variables) which are eventually output at the end of the request in a _main.php file. This _main.php file typically contains an entire HTML document from the opening <DOCTYPE html>
tag to the closing </html>
tag. Within that file are PHP tags to output those placeholders/variables, anywhere in the markup that you want them.
The placement of content matters little to our template files, which can focus purely on populating those placeholders or variables. Should the site later be re-designed and have everything get moved around, our template files don't need to change. Only our _main.php file and CSS files are likely to need adjustment to support the new design.
How to use delayed output
When using delayed output with variables, your template files focus on populating variables rather than outputting them. This leads to template files that are quite a bit simpler, and occasionally even blank! Here's an example of what our template file might look like:
/site/templates/basic-page.php
<?php $headline = $page->get("headline|title"); $bodycopy = $page->body; $sidebar = $page->sidebar; include("./_main.php");
As you can see above, our template file is simply populating 3 predefined regions by using placeholder variables: $headline
, $bodycopy
, and $sidebar
. These variables are populated with values that we want to be output in _main.php. Then we end by just including the _main.php file. What does this _main.php file look like?
/site/templates/_main.php
<DOCTYPE html> <html> <head> <title><?php echo $headline; ?></title> </head> <body> <div id='bodycopy'> <h1><?php echo $headline; ?></h1> <?php echo $bodycopy; ?> </div> <div id='sidebar'> <?php echo $sidebar; ?> </div> </body> </html>
What this example isn't showing is that your $bodycopy
and $sidebar
may consist of a lot more than just $page->body
and $page->sidebar
. Which leads us to…
Populating placeholders (variables) with more
Lets say that you want your $bodycopy
to include a list of comments at the end, and you want your $sidebar
to include sub-navigation to child pages. Here's how that might look:
/site/templates/basic-page.php
<?php $headline = $page->get("headline|title"); // bodycopy is body text plus comments $bodycopy = $page->body . $page->comments->render(); $sidebar = $page->sidebar; // check if this page has any children if(count($page->children)) { // render sub-navigation in sidebar $sidebar .= "<ul class='nav'>"; foreach($page->children as $child) { $sidebar .= "<li><a href='$child->url'>$child->title</a></li>"; } $sidebar .= "</ul>"; } include("./_main.php");
As you can see, we can put whatever we want into our predefined placeholders/variables very easily. We've got a lot of flexibility here.
Defining new placeholders to represent new regions
Lets say that we wanted to let our _main.php file decide where the sub-navigation should appear, rather than us appending it to the sidebar. Whether you need that or not depends, but lets look at how we might approach it. We will give our _main.php full control over where that <ul class='nav'>…</ul>
sub-navigation lives. We could do that by setting up another placeholder variable, which we will name $subnav
. Rather than populating $sidebar
, we would populate $subnav
. Then our _main.php file would determine where that $subnav
gets displayed.
Using the placeholder variable to hold pages rather than markup
In this particular case, we would have to repeat ourselves with writing the foreach()
code to generate the <ul class='nav'>…</ul>
in every template file where we wanted sub-navigation. It would make more sense to let our _main.php file handle that markup generation, and let our template file just pass it the list of pages that should appear in that sub-navigation. So we will use our $subnav
placeholder variable to hold a group of pages rather than a string of markup. Here's how that might look:
/site/templates/basic-page.php
<?php $headline = $page->get("headline|title"); $bodycopy = $page->body . $page->comments->render(); $sidebar = $page->sidebar; $subnav = $page->children; include("./_main.php");
And here's how the _main.php file might look:
/site/templates/_main.php
<DOCTYPE html> <html> <head> <title><?php echo $headline; ?></title> </head> <body> <div id='bodycopy'> <h1><?php echo $headline; ?></h1> <?php echo $bodycopy; ?> </div> <div id='sidebar'> <?php echo $sidebar if(count($subnav)) { echo "<ul class='nav'>"; foreach($subnav as $child) { echo "<li><a href='$child->url'>$child->title</a></li>"; } echo "</ul>"; } ?> </div> </body> </html>
Adding an _init.php file to the mix (a best practice)
One of the problems with the above examples is that each template file must populate every single placeholder variable output by the _main.php file. If you decide to add a new placeholder for a new region, you'd have to go back and edit all your template files to account for it. Otherwise you'd end up with "undefined variable" notices. In addition, maybe a lot of our template files simply don't need $bodycopy
to be anything more than $page->body
and don't need $sidebar
to be anything more than $page->sidebar
. We could think of those as our default values. Wouldn't it be nice if we didn't have to even mention them in our template files when we didn't need them to be different from the default?
This is an easy task to accomplish. Simply include an _init.php file at the top of each template file. Like the name implies, the _init.php file initializes all the placeholder variables that will get output in _main.php by establishing default values to them. Here's how our _init.php file might look:
/site/templates/_init.php
<?php $headline = $page->get("headline|title"); $bodycopy = $page->body; $sidebar = $page->sidebar; $subnav = $page->children;
Now all of our placeholders in _main.php are accounted for with default values. Our template files can focus only on modifying the placeholders that they want to be different from the defaults. Our basic-page.php only needs to append comments to the $bodycopy
, so it now looks like this:
/site/templates/basic-page.php
<?php
include("./_init.php");
$bodycopy .= $page->comments->render();
include("./_main.php");
Automatic inclusions
We now have a scalable and sustainable strategy for our template files, but it's a little annoying to have to repeat the include("./_init.php");
and include("./_main.php");
in every single one of our template files. Thankfully, there are two $config
options that can solve this for us:
/site/config.php
$config->prependTemplateFile = '_init.php'; $config->appendTemplateFile = '_main.php';
The $config->prependTemplateFile
specifies the file that you want ProcessWire to load before your template file, and the $config->appendTemplateFile
specifies the file you want ProcessWire to load after your template file. These files are assumed to live in /site/templates/. They act as if they had literally been prepended or appended to your template file, sharing the same variable space as your main template file. This is what enables you to share variables between them. Meaning, a variable set in your _init.php file (like $bodycopy
) will still be present ("in scope") in your template file, and will still be present in the _main.php file as well.
After editing our /site/config.php file to add those two options, our basic-page.php template file may be reduced to just this:
/site/templates/basic-page.php
<?php
$bodycopy .= $page->comments->render();
When to use automatic inclusions
Whether or not you should use automatic inclusions kind of depends on the situation. If you are working with a site that has various output needs, some direct and some delayed, then automatic inclusions may get in the way. Likewise, if you have more than one _main.php file (perhaps representing different layouts) then it might be preferable to include()
whatever output file you desire at the end of each template file. You may find in some cases that you'll use $config->prependTemplateFile
, but not $config->appendTemplateFile
. Again, it really just depends on the situation. However, now that you know about automatic inclusions, we think you'll find them very useful in many instances.
Alternatives to placeholder variables
When editing template files, if using a code editor like an IDE (such as PhpStorm), it might recognize and flag PHP variables as undefined, even if they really aren't. That's because it can't see your _init.php automatic file inclusion, where you might have defined all of your placeholder regions as PHP variables. While not a problem, it might be an annoyance. You can tell your editor about the variable by using a phpdoc comment, like this:
/** @var string $bodycopy */
The above simply tells your code editor that there is a $bodycopy
variable defined, even if it doesn't see it. (Note that this must go somewhere in <?php … ?>
tags). However, even having to do that might get annoying, especially if you've got a lot of region placeholder variables, and a lot of template files.
There is an alternative. In ProcessWire, you can use the region()
function to define, populate, read or modify any given region variable. It is designed specifically as a convenience for delayed output and enables you to work with function calls for getting/setting those placeholders, rather than using PHP variables.
While a primary benefit of the region() function is that it's more IDE friendly than variables, it is also beneficial in that it operates within it's own scope and thus it prevents the problems of variables getting accidentally overwritten. In addition, function calls are always in scope and this makes your regions more accessible from within other functions or classes (should you need it).
Below is a simple example of defining, populating and outputting a region named bodycopy:
// defining a region with default value in _init.php region('bodycopy', $page->body); // populating the region with custom value in basic-page.php region('bodycopy', "<h2>$page->headline</h2>" . $page->body); // outputting the region in _main.php echo region('bodycopy');
The above is just a brief introduction to the region() function. If this looks useful to you, see this blog post about region() for more details on how to use it, as there is a lot more than it can do.
If you find the region() function does not work, you can use wireRegion()
instead (which is an alias) or you can add the following to your /site/config.php file:
$config->useFunctionsAPI = true;
Drawbacks of delayed output
While delayed output is flexible and powerful, it depends upon placeholders (variables) which might be initially harder for some to understand (especially if new to coding in general). While delayed output is actually very simple, it's also a little different, so what's happening in delayed output might not always be immediately obvious to a developer new to an existing site, and sometimes needs more explanation than direct output might.
Another potential drawback of delayed output is that when using placeholder variables, your HTML/code editor may think those variables are undefined, even when they aren't. In addition, any time there are variables in use (especially across included files) there's an increased chance of accidentally overwriting variables. Using the region()
function (as described in the previous section) can resolve these drawbacks and more.