Jump to content

Plates for ProcessWire - An adapter for the Plates templating system by The League of Extraordinary Packages


FireWire
 Share

Recommended Posts

Plates for ProcessWire is a module to make using Plates with your ProcessWire templates plug-and-play. Plates is an extremely lightweight pure PHP templating system that provides features that developers have come to expect when building applications and sites. From the Plates website:

Quote

Plates is a native PHP template system that’s fast, easy to use and easy to extend. It’s inspired by the excellent Twig template engine and strives to bring modern template language functionality to native PHP templates. Plates is designed for developers who prefer to use native PHP templates over compiled template languages, such as Twig or Smarty.

Highlights from the documentation:

  • Native PHP templates, no new syntax to learn
  • Plates is a template system, not a template language
  • Plates encourages the use of existing PHP functions
  • Increased code reuse with template layouts and inheritance
  • Template folders for grouping templates into namespaces
  • Data sharing across templates
  • Pe-assign data to specific templates
  • Built-in escaping helpers
  • Easy to extend using functions and extensions

Plates is an extremely stable application that has been in development and use in production since 2014. This module is also a simple adapter so I am confident in it's stability as I've already used it myself. However, the custom extensions included should be considered early releases and bug reports are welcome, pull requests are very welcome as well!

If you're familiar with Plates or just want to get started now, you can download the module from the Github repository. Batteries are included, documentation provided.

So, FireWire, why another templating engine?

There are many stellar templating engines available. I've used several of them and they have truly great features. I also appreciate the simplicity of working with PHP. While templating engines do sometimes offer more terse syntax, it's not a killer feature for everyone like code reuse, nesting, and layouts may be. Code editors will always offer first-class support for PHP and syntax highlighting makes some of the arguments about readability less of a feature benefit for alternatives. Plates takes care of the limitations that come with writing pure PHP templates.

Plates feels very at home with the ProcessWire API. Like ProcessWire, it also scales infinitely and its focus on core features over the large library approach makes for a great developer experience. If you've worked with templating engines in the past, the features are familiar. If you haven't, you'll be up to speed remarkably fast. I wrote this module with the intention of making it a "drop-in and code" experience, and I've worked on using the extensibility of Plates to add some extra superpowers to boot.

Plates is another great option that may be useful to you whether because it's more your style, it fits your use case, or maybe your first time adding a little extra oomph to your ProcessWire templates. The first 10 minutes you spend with the Plates documentation might be the last 10 minutes.

A Simple Example

Start with files and folders. Things to know off the bat:

  • Plates for ProcessWire comes pre-configured to look for Plates templates in your /site/templates folder
  • By default it will look for files with the extension '.plates.php' to help differentiate from ProcessWire files, however this may be customized to any extension you prefer on the module config page
  • The folder structure here is entirely up to you, this example can be used but is not required.
/site
  /templates
    /components
      image_gallery.plates.php
    /layouts
      main.plates.php
    /views
      home.plates.php
    home.php
  ready.php

Your ProcessWire templates will contain one line that hands off rendering to Plates

<!-- /site/templates/home.php -->
<?=$plates->templates->render('views/home')?>

Start by creating your layout. We'll keep it simple.

<?php namespace ProcessWire;
// /site/templates/layouts/main.plates.php
/**
 * @property string|null $title       Page title
 * @property string|null $description Meta description
 */
$navBase = $pages->get('/');
?>
<!DOCTYPE html>
<html>
  <head>
    <title><?= $title ?? $page->title; ?></title>
    <?php if ($description ?? null): ?>
      <meta name="description" content="<?=$description?>">
    <?php endif ?>
    <link rel="stylesheet" href="<?=$config->paths->templates?>styles/app.css">
  </head>
  <body>
    <header class>
      <img src="/path/to/logo.jpg">
      <nav>
        <ul>
          <?php foreach ($navBase->children->prepend($navBase) as $navPage): ?>
            <li>
              <a href="<?=$navPage->url?>"><?=$navPage->title?></a>
            </li>
          <?php endforeach ?>
        </ul>
      </nav>
    </header>

    <section>
      <?= $this->section('hero'); ?>
    </section>

    <?= $this->section('content') ?>

    <footer>
      <?= $this->section('footer'); ?>
    </footer>
    <script src="/path/to/your/file.js"></script>
  </body>
</html>

I like to add docblocks at the top of my Plates templates because we can pass data to any template or layout wherever needed. This is optional and just a style preference. Some notes:

  • The full ProcessWire API is available
  • Your Plates templates are rendered inside a Plates Template object. To use any Plates function, custom function, or custom extension you use $this

Jumping over to home.plates.php

<?php namespace ProcessWire;
// /site/templates/views/home.plates.php

$this->layout('layouts/main', ['description' => $page->description]);
?>

<?php $this->start('hero') ?>
  <h1><?=$page->headline?></h1>
  <img src="<?=$page->hero_image->url?>" alt="$page->hero_image->description">
<?php $this->end() ?>

<section>
  some stuff here
</section>

<section>
  <?php if ($page->gallery->count()): ?>
    <?php $this->insert('components/image_gallery', [
      'images' => $page->gallery,
      'title' => __('Image Gallery'),
    ]) ?>
  <?php endif ?>
</section>

<section>
  Some stuff there
</section>

<?php $this->start('footer') ?>
  <p>Thanks for visiting</p>
<?php $this->end() ?>

Things to note:

  • The full ProcessWire API is available including language functions
  • Even though this file is located in the 'views' subdirectory, Plates is configured out of the box to use '/site/templates/' as the base directory, so you can write paths without '../' directory traversal
  • Plates has a feature called Folders that allow you to create namespaced directory locations at any depth with nice and clean syntax. The example packaged with the module shows a demo.
  • We chose the main layout and passed the 'description' variable which is available in main.plates.php as $description
  • $this->start('hero') and $this->stop() capture what is output to those sections in main.plates.php, there is no limit on sections and they can have any name aside from 'content' which is reserved.
  • Any content that exists outside of a defined start/stop section is automatically output to the 'content' section in your layout

And the image gallery:

<?php namespace ProcessWire;
// /site/templates/components/image_gallery.plates.php
/**
 * @property string|null $title  Optional gallery title
 * @property Pageimages  $images Images field
 */
?>
<div>
  <?php if ($title ?? null): ?>
  	<?=$this->batch($title, 'strtolower|ucfirst')?>
  <?php endif ?>
  <ul>
    <?php foreach ($images as $image): ?>
      <li>
        <img src="<?=$image->url?>" alt="<?=$image->description?>">
      </li>
    <?php endforeach ?>
  </ul>
</div>

Some more notes:

  • You can use $this->insert() in any Plates file, including layouts. You can also nest using $this->insert() recursively and nest in other component-style files
  • Any template can have a layout and you can nest layouts. So home.plates.php could use main.plates.php as its layout, and main.plates.php could use something like base.plates.php so layouts themselves can share code through inheritance.
  • You can use batch() to execute multiple functions on a value. Any PHP function that accepts one argument (or one argument and the rest optional) can be chained in a batch. This also works with custom functions and Extension functions where you can do some really neat stuff. This is similar to functions and filters in other templating engines.

The Syntax

The syntax, like ProcessWire, is just PHP and has complementary style and a simple API. Plates only makes style recommendations. One of the key elements to making templates in any engine work is knowing where to put logic and where control structures should do the heavy lifting. With PageClasses and organized code, templates can be clean and concise with just PHP. At it's core, Plates primarily keeps focus on templates which makes it different than other engines that tend to include new syntax and tools because they already have to build a parser or interpreter.

The batch() function covers a most use cases and is a welcome tool to use as is or as a complement to more custom functions and extensions. Speaking of...

Plates for ProcessWire Extensions

This module comes with several optional extensions that add useful tools for building templates. Many also provide some parity with other templating solutions. All custom extensions are optional and can be enabled/disabled on the module config page. Plates for ProcessWire extensions provide over 100 custom functions to use in your templates. Many of them are batchable, many of them are written to use with ProcessWire objects such as Page and WireArray/PageArray. Others are intended to make template code shorter and cleaner, others are just nice to have.

The Conditionals Extension brings some nice to have utilities.

<!-- From our example above -->
<?php if ($page->gallery->count()): ?>
  <?php $this->insert('components/image_gallery', [
    'images' => $page->gallery,
    'title' => __('Image Gallery'),
  ]) ?>
<?php endif ?>

<!-- Consider this instead -->
<?php $this->insertIf('components/image_gallery', $page->gallery->count(), [
  'images' => $page->gallery,
  'title' => __('Image Gallery'),
]) ?>

Tidy up single line outputs

<!-- Instead of this -->
<?php if ($page->text_field): ?>
  <h2><?=$page->text_field?></h2>
<?php endif ?>

<!-- Consider this -->
<?=$this->if($page->text_field, "<h2>{$page->text_field}</h2>")?>

Use match instead of long if/elseif/else chains, or matchTrue for more complex conditions

<h2>
  <?=$this->match($weather, [
    'sunny' => __('Grab your sunglasses'),
    'cold' => __('Wear a coat'),
    'rainy' => __('Bring an umbrella'),
  ])?>
</h2>

<h2>
  <?=$this->matchTrue([
    __('Tickets are available') => $ticketCount > 10,
    __('Hurry, tickets are almost sold out') => $ticketCount > 1,
    __('Sold Out') => true,
  ])?>
</h2>

The Functions Extension provides a wide array of flexible and batchable functions

<!-- Get the sum of all items in a WireArray/PageArray, associative array, or object by field/key/property, Also works on indexed arrays -->
<p>Total: <?=$this->sum($page->cart_items, 'price')?></p>

<!-- Group items in an associative array, array of objects, or WireArray/PageArray by property or field -->
<?php foreach ($this->group($pages->get('template=players'), 'team_name')) as $teamName => $players): ?>
  <h2><?=$teamName?></h2>
  <ul>
    <?php foreach ($players as $player): ?>
      <li>
        <?=$player->title?><br>
        <?=$player->position?>
      </li>
    <?php endforeach ?>
  </ul>
<?php endforeach ?>

<!--
  Get PageArrays inclusive of their parent using withChildren()
  Assign attributes/values if a page matches the current page using attrIfPage()
  withChildren() accepts a Page, selector, page ID. A second selector can be passed to filter child pages
-->
<nav>
  <ul>
    <?php foreach ($this->withChildren('/') as $navItem): ?>
      <li<?=$this->attrIfPage($navItem, 'class', 'active')?>>
        <a href="<?=$navItem->url?>">
          <?=$navItem->title?>
        </a>
      </li>
    <?php endforeach ?>
  </ul>
</nav>

<!-- Generate an unordered list of breadcrumbs -->
<?=$this->breadcrumbs(['startPage' => '/', 'separator' => ' | ', 'ulClass' => 'breadcrumb-nav'])?>

<!-- Create an indexed array with iterator from index 1 on any iterable object -->
<?php foreach ($this->batch($page->images, 'toList|from1') as $i => $image): ?>
  <img src="<?=$image->url?>" alt="<?=$image->description?>" data-slide-index="<?=$i?>">
<?php endforeach ?>

The configurable Asset Loader extension lets you link, inline, and preload assets with automatic cache busting version parameters. Directories and namespaces are yours to choose.

<?=$this->preloadAssets([
  'fonts::ProximaNova.woff2',
  'fonts::ProximaNovaLight.woff2',
])?>
<?=$this->preloadAsset('js::app.js')?>
<?=$this->linkAsset('styles::app.css')?>
<?=$this->inlineAsset('styles::critical.css')?>

<?=$this->linkAsset('js::app.js')?>

There are more extensions and a lot more functions, with documentation. Many functions that work with arrays also work with WireArray and WireArray derived objects making them batchable.

If you're a RockPageBuilder rockstar, check out the README file for details on how to use an included utility function to make Plates and RPB work together 👍

Try It Out!

If you want to give it a try, download the module from the Github repository and take it for a spin. When it gets a little more testing I may submit it to the modules directory.

I'm a consistent user of plain PHP, Latte, and Blade for templating and I think Plates is a great addition to the developer toolbox. Interested to hear your thoughts.

  • Like 6
  • Thanks 1
Link to comment
Share on other sites

Hey @FireWire, thanks for the post! I hope you can shed some light on this for me.

Personally, I’ve never used template engines. My lazy, Occam's Razor-inspired mindset has always questioned the need to add another layer of complexity—especially since it’s something else to learn. I feel similarly about CSS preprocessors.

That said, regarding PHP template systems, could you elaborate a bit more on your experience with them? Also, forgive my ignorance, but do they “compete” with the markup regions in PW?

Thanks!

  • Like 1
Link to comment
Share on other sites

@zilli Some great questions! I spent many years writing vanilla PHP template markup. For Occam's Razor, have the leeway to apply as many or few features a tool offers. Your mentioning Markup Regions is a good example, I use many features of ProcessWire but have never used Markup Regions (I have no opinion, it just coincidentally never became part of my workflow). Both MR and templating engines aim to overcome challenges introduced when using PHP (like any language) alone for output. So it's up to the preference of the developer and needs of a project, a la "there are no wrong answers". My limited experience with MR make me a less than ideal candidate to draw comparisons or speak to compatibility.

My first thought it to flip the question of complexity and see how it applies to tools you/me/we may already use. Page Classes are a layer of complexity to abstract logic out of presentation, ProcessWire itself is a layer of complexity to abstract database transactions/data management. Templating strategies are a layer of complexity that make your workflow less complex. Tools like MR and templating packages do that as well while sometimes affording some extra tools.

My experience with templating solutions was born out of bumping my head on limitations and moments of thinking "there has to be a better way to do this". I ended up using creative tricks to make 'require' and 'require_once' carry the load but ended up making my code harder to manage, more files to make it work, and felt like I was breaking good practice rules. Consider this simple example:

<!-- /site/templates/inc/header.inc.php -->
<!DOCTYPE html>
<html lang="en">
<head>
  <title><?=$page->title?></title>
  <?php if ($includeGoogleAnalytics ?? false): ?>
    <?php require_once '/path/to/google_analytics.inc.php' ?>
    <!-- GA code here -->
  <?php endif ?>
</head>
<body id="$pageId" class="<?=$pageClass?>">
  <header>
    <img src="/path/to/logo.png">
    <h1><?=$headline?></h1>
  </header>

Then I have my home.php and to make this work, I have to start setting big scope variables to be made available to the 'require_once' code. This got worse as the site needed more features and the header and footer became more complex. Later I had to create a new headers for other parts of the site, so multiply the code above a few more times, and add some more 'require_once' lines to each of them.

<?php namespace ProcessWire;
// /site/templates/home.php

$pageId = 'stuff-here';
$pageClass = 'stuff-there';
$includeGoogleAnalytics = true;

require_once __DIR__ . 'partials/header.inc.php';
?>

<!-- We haven't even added one line of markup/code for the home page -->

<?php
$includeCallToAction = true;
$includeTestimonials = false;
$anotherOne = true;
$iHearYouLikeOutOfScopeVariables = false;

require_once __DIR__ . 'partials/footer.inc.php';
?>

Here's an actual snippet from a site I build years ago. If you look at a page and it has a lot of if statements, sometimes but not always, it says "I'm a file that does too much" and it becomes increasingly difficult to manage.

<!-- Real snippet of production code from /site/templates/partials/footer.inc.php -->
<?php if ($mainNav): ?>
  <?php require_once __DIR__ . '/../inc/site_nav_overlay.inc.php' ?>
<?php endif ?>
<?php if ($salesCtaModal): ?>
  <?php require_once __DIR__ . '/../inc/cta_sales_quote_modal.inc.php' ?>
<?php endif ?>
<?php if ($solarCleaningCtaModal): ?>
  <?php require_once __DIR__ . '/../inc/cta_solar_cleaning_modal.inc.php' ?>
<?php endif ?>
<?php if ($solarRepairCtaModal): ?>
  <?php require_once __DIR__ . '/../inc/cta_solar_repair_modal.inc.php' ?>
<?php endif ?>

With a templating solution approach this would have been solved with 4 files. You can significantly reduce the number of if statements because each of these have a job to do, they know what it is, and when you're maintaining the site, or adding new features, there is zero confusion about where to go. Working on the blog parts? Go edit the blog templates and blog layout. The service/maintenance pages know they'll need the "cleaning" and "solar repair" modals so that whole if block doesn't exist anymore. Cake.

<?php namespace ProcessWire;

// This is the base layout. The layouts below declare this as *their* layout and pass data if needed
$this->layout('layouts/base');

// For the majority of pages on the site
$this->layout('layouts/main');

// For the pages dedicated to customer service and maintenance rather than sales
$this->layout('layouts/service_mainenance');

// For all blog and blog related pages
$this->layout('layouts/blog');
?>

Is this how everyone does it? Maybe not, hopefully not, but every project runs the risk of becoming Frankenstein's monster.

Long story short, the complexity would have been immediately reduced by introducing a templating tool with a predictable set of features and functions to use.

To contrast, as a longtime user of preprocessors myself, I can say that those have the potential to introduce a lot of things that fundamentally change how you work with the language itself. Sass/Less bring nesting, loops, variables (before custom properties were available), mixins, includes, file imports, modifier functions, if/else control flows, custom functions, (the list goes on) and an entire JavaScript toolset to handle it. You can solve a lot of problems you don't have! This isn't an argument against preprocessors, but there is a higher level of discipline you must have to keep this from going off the rails and being so clever you outsmart yourself.

On the other hand, templating is so incredibly fundamental to producing apps and websites that the constrains start to tighten much sooner and it becomes more clear how many workarounds and "bending the rules" of good practice are present. The benefits of introducing templating tools bring a higher number of benefits that are more impactful faster. You're also adopting a common development practice, so it is leveling up your skillset.

I have to mention that these benefits are not unique to Plates. Layouts, and enhanced insert/include features, are common to pretty much all templating tools. Pick the one that feels right be it Latte, Twig, Smarty, or Plates.

My only argument in favor of Plates is that if templating is something that you're introducing into your projects for the first time you may be up to speed faster because: a) the core features set of Plates is very limited (intentionally), and b) it's the same PHP syntax you already use. The concepts and strategy you use with Plates will translate to other templating tools.

In the case of this module, the potential amount of complexity is almost entirely due to the custom extensions I built which is why they are optional and disabled by default. You can safely ignore everything in my post above from "Plates for ProcessWIre Extensions" on down and still get simple yet powerful tools. If you have any Q's, post them here.

  • Like 3
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

×
×
  • Create New...