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!

(I'm going to be traveling so I may not be quick on responses but I'll be back)

 

  • Like 5
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!

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...