Basic ProcessWire website workflow - Part Two

Reading time ~25 minutes

Introduction

Following the Part One.

In the previous article we have set the foundations, now it’s time to actually code the pages our basic website.

Template Inheritance

Let’s start by explaining the concept of template inheritance. The Smarty documentation provides a nice explanation of this approach, which allows the developer/designer to compose each page by writing only the necessary code and inheriting or including the common parts that are shared.

“Template inheritance is an approach to managing templates that resembles object-oriented programming techniques. Instead of the traditional use of {include …} tags to manage parts of templates, you can inherit the contents of one template to another (like extending a class) and change blocks of content therein (like overriding methods of a class.)”

As a general rule, the shared parts of the website should live in different files. If you think about it, coding the header and the footer on every page is a waste of time and it is extremely inefficient because if you eventually need to change something, you will have to edit all the pages. Make sure to understand this concept before moving on.

Now we can create our main template file. This will be the master page or, if you prefer, the skeleton of our website. This will be extended each time by our actual pages.

As I said before I’m not going into the details of the look of your site, therefore you can use whatever front-end framework (Bootstrap, Foundation, etc.) and technology (SASS, SCSS, plain CSS or even PostCSS) you’re comfortable with. I don’t make assumptions and neither PW does.

The most basic template file could be the following:

<!DOCTYPE html>
<head>
    <!-- meta tags -->
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <link rel="shortcut icon" href="{$config->urls->root}favicon.ico" />

    <!-- common stylesheets shared on all the pages -->

    {block "styles"}{/block}
</head>
<body>
    <!-- Header -->
    {include 'partials/header.tpl'}
    <!-- /Header -->

    <!-- Content area-->
    {block "content"}{/block}
    <!-- /Content area -->

    <!-- Footer -->
    {include 'partials/header.tpl'}
    <!-- /Footer -->

    <!-- common scripts shared by all the pages: jquery and so on -->

    {block "scripts"}{/block}
</body>
</html>

Call this file template.tpl and place it into the view directory.

You can notice a couple of things here:

  • the include instruction loads an external file into the template. The header and the footer, which are shared in the entire website, will have their own file (header.tpl and footer.tpl).
  • the {block...} tags define the changeable blocks of content. The "content" block will contain the injected content from each page, while the "styles" and "scripts" blocks can be used to insert additional CSSs and JavaScript files required by individual pages.

For now you can hard-code your styles (using <link> tags) and scripts (using <script> tags), later we will use the AIOM module to manage our assets. You can use the following instruction to get the URL of your templates folder:

{$config->urls->templates}

Now we can add the header and the footer. Create the partials folder in templates and add your files header.tpl and footer.tpl. We will keep these files as simple as possible. As I said, I’m not using any particular CSS framework here, the class names are just semantic helpers for helping you to understand the meaning of the code:

<nav class="navigation">    
    <!-- navigation menu -->
</nav>

Again, in this case you can hard-code your navigation menu or leave it empty, because later we will use the MarkupSimpleNavigation module to build it.

For the footer you should first decide which elements you want to insert. In this example I’m going to use a 3-columns footer:

  • 1st column: the contact info of the School, hard-coded in the file because it’s an information that is not likely to change very often
  • 2nd column: random images from a random gallery
  • 3rd column: the Facebook widget

We can code this as follows:

<footer>
    <div class="col">
        <address>
            <!-- The Address of the Ski School -->
        </address>
    </div>
    <div class="col">
    	<!-- gallery images -->
    </div>
    <div class="col">
    	<div class="fb-like-box" data-href="YourFacebookPageUrl" data-width="255" data-height="280" data-colorscheme="dark" data-show-faces="true" data-header="true" data-stream="false" data-show-border="true"></div>
    </div>
</footer>

Ok, we have our column placeholders! We can add the rest of the content later.

Now we can start coding our template files.

Basic Page

This template represents the most simple pages of our website, holding simply a body and images fields.

This is the basic-page.tpl view file

{extends "template.tpl"}

{block "content"}

    <div class="image-header" style="background-image: url('{$header_image}');">
    	<h1 class="title">{$page->title}</h1>
    </div>

    <div class="content">
    	{$page->body}
    </div>

{/block}

Let’s take the time to examine a few things here!

The first Smarty instruction on this page allows us to extend the template.tpl file, then we inject the content block into our main template. The $page variable is provided by PW to every template and it contains all the fields specific to the page being viewed. It is available by default on the view files thanks to the TemplateEngineFactory module.

And what is $header_image? You may ask why I’m not using $page->header like the other fields. The reason is really simple: we have to deal with possible empty fields. In PW the title is mandatory for every page so we are covered. As regarding the body we don’t really care (at least in this tutorial) if is an empty string, but we would like to use a default image in case the $page->header field is empty. We can use our controller file basic-page.php for this scenario. Here’s the code:

<?php

if($page->header) {
    $view->set('header_image', $page->header->width(1920)->url);
} else {
    $view->set('header_image', $config->urls->templates . 'images/header.jpg');
}

If the header field contains an image, we will set the header_image variable with the url of the 1920 pixel-wide version, generated and cached on the fly by PW. Otherwise we will just use a default image on the filesystem.

As you might already guessed by now, you can use the syntax $view->set, provided by the TemplateEngineFactory module, to pass variables to the view:

<?php

$hello = 'Hello World!';
$view->set('hello', $hello);

Finally you can use Smarty’s syntax to output the variable in this way: {$hello}.

Now you can get your hands dirty and create a couple of pages using this template in the admin section to check if everything works correctly.

Contact Page

A simple contact page contains at least a body, a map for the Ski School location and a contact form for sending emails to the site owner. Building a Contact Form is really simple in PW: you can use one of the available modules in the PW directory or you can roll your own personal solution, just look at the PW forum for inspiration. Nevertheless I added a basic class, just to get you started.

This is the view file contact-page.tpl:

{extends "template.tpl"}

{block "styles"}
    <script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?sensor=false"></script>
{/block}

{block "content"}

    <div class="image-header">
    	<!-- here we will render the Google Map using the MapMarker Module -->
    </div>

    <div class="content">
    	{$page->body}

    	<div id="contact-form">
            {if isset($errors)}
                <div class="alert alert-danger">
                    <ul>
                        {foreach $errors as $error}
                            <li>{$error}</li>
                        {/foreach}
                    </ul>
                </div>
            {/if}
            {if isset($success)}
                <div class="alert alert-success">
                    {$success}
                </div>
            {/if}
            <!-- Contact Form -->
            {if isset($contactform)}
                {$contactform->render()}
            {/if}
            <!-- /Contact Form -->
        </div>
    </div>

{/block}

First we include the Google APIs script in the head using, ONLY for this case1, the styles section. Then we inject the body of the page and finally we render the contact form. The errors and success variables are used to display Bootstrap alerts to inform the user of validation errors or about the outcome of the email sending process.

The controller file contact-form.php is the following:

<?php

include_once(__DIR__ . '/include/contact_form/ContactForm.php');

$contactForm = new ContactForm();
$form = $contactForm->buildForm();

// if the form was submitted we process the form
if($input->post->submit) {

    // process the form and check for errors
    $form->processInput($input->post);

    // check honeytrap
    if($form->get("hp_email")->value != "") {
        die('Spam!');
    }

    // if there are errors
    if($form->getErrors()) {
        $view->set('errors', $form->getErrors());

        $view->set('contactform', $form);
    } else {
        if($contactForm->sendMail($input->post)) {
            $view->set("success", __("Your message has been sent."));
        } else {
            $view->set("errors", array(__("There has been an error during the sending process.")));
        }
    }
} else {
    // pass the empty form to the view
    $view->set("contactform", $form);
}

The code is straightforward, just follow the comments.

Notice: the double-underscore function is used by PW to make a string translatable. We will cover translations later in another post. For now you can check out the PW documentation on code internationalization for more informations.

The class included at the top of the file is a simple PHP Class that builds a Bootstrap-compatible contact form using PW APIs, you can find it here. It also includes a honeypot field for spam detection.

As we said before, this page will show all the photogalleries available in our site. The idea is to show one random image for each gallery as a preview, along with a title and a link to the gallery page. First let’s take a look to our controller gallery-index.php:

<?php

// Code for the image header omitted [...]

$galleries = wire("pages")->find("template=gallery");

$view->set('galleries', $galleries);

In this case we are using a PW selector to get all the pages with the gallery template. ProcessWire selectors allow us to intuitively make complex database queries on our pages, without the need to use directly SQL. Think them as an abstraction layer between your business code and your data.

Notice: if you start to have a lot of galleries you should consider to paginate them, to speed up the rendering time and to provide a better user experience. This suggestion is valid every time you have to deal with too much content for one page.

Finally we pass the $galleries variable, which is an instance of PageArray, to the view where it will be used in a foreach loop, just like a regular array.

{extends "template.tpl"}

{block "content"}

    <div class="image-header" style="background-image: url('{$header_image}');">
	   <h1 class="title">{$page->title}</h1>
    </div>

    <div class="content">
    	{$page->body}

        {if $galleries|@count == 0}
            <div class="alert alert-warning">
                <span>{_('There are no photogalleries.')}</span>
            </div>
        {else}
            <ul class="gallery-index">
                {foreach $galleries as $gallery}
                    <li class="gallery-item">
                        <a href="{$gallery->url}" title="{$gallery->title}">
                            <img src="{$gallery->images->getRandom()->size(255,160)->url}" alt="{$gallery->title}" class="img-responsive" />
                        </a>
                       <span>{$gallery->title}</span>
                    </li>
                {/foreach}
            </ul>
        {/if}
    </div>

{/block}

First we check that the galleries array is not empty, using the @count instruction, then we use a foreach loop to iterate through all the galleries, extracting the title field and a random image to build the proper markup. This snippet takes for granted that the images field of each gallery is not empty, however you can easily edit the code to check for this case and display a static image instead.

The code to build a simple gallery is even simpler. The controller file is empty because we just need the images field, which is directly available in the view through the $page variable:

{extends "template.tpl"}

{block "content"}

    <div class="content">
        <h1 class="gallery-title">{$page->title}</h1>
        <div class="images-index">
            {foreach $page->images as $image}
                <div class="image-item">
                    <a href="{$image->url}" rel="gallery" title="{$image->description}" class="lightbox" title="{$image->description}">
                        <img src="{$image->size(255,160)->url}" class="thumbnail img-responsive" />
                    </a>
                </div>
            {/foreach}
        </div>
    </div>

{/block}

As in the previous case, we use a foreach loop to iterate through all the images. We can use a lightbox plugin to display the original image {$image->url} in a modal window and a smaller thumbnail {$image->size(255,160)->url} as preview. Notice how clean and intuitive the PW APIs are.

News Index

This is the page that collects all the news on the site. They should appear on the page in inverse chronological order, from the most recent to the oldest, and they should also be paginated for a better navigation. Probably you might also want to display only an excerpt of the news content, with a link to the full page. Let’s take a look to the controller:

<?php

$paginated_news= $pages->find("template=news, sort=-date, limit=10");

$view->set('paginated_news', $paginated_news);

$view->set('paginator', $paginated_news->renderPager(array(
    'nextItemLabel' => "<i class='fa fa-angle-double-right'></i>",
    'nextItemClass' => "next",
    'previousItemLabel' => "<i class='fa fa-angle-double-left'></i>",
    'previousItemClass' => "prev",
    'currentItemClass' => "active",
    'listMarkup' => "<ul class='pagination'>{out}</ul>",
    'itemMarkup' => "<li class='{class}'>{out}</li>",
    'linkMarkup' => "<a href='{url}'>{out}</a>"
)));

Using a selector we are looking for all the pages that have a news template, sorting them in a reverse chronological order (notice the minus character before the date field) and finally we limit the result to 10 items. The paginated_news variable is then passed to the view, as well with an instance of the PW pager which gives us a nice out-of-the-box pagination element, styled according to our needs. Remember that the option “allow for page numbers” should be enabled in the settings for this template.

{extends "template.tpl"}

    <div class="content-column clearfix">
        {if $paginated_news|@count == 0}
    	    <div class="alert alert-danger">
                <span>{__('There are no news')}</span>
            </div>
        {/if}

        {foreach $paginated_news as $news}
            <article class="news">		    
                <h3>
                    <a href="{$news->url}" title="{$news->title}" rel="bookmark">{$news->title}</a>
                </h3>			
                <div class="meta">                                    
                    <ul>
                        <li>{$news->date}</li>
                    </ul>
                </div>
                <div class="news-content">{$item->body|truncate:440:"..."}</div>
                <a class="read-more" href="{$news->url}" title="{$news->title}">{__('Read More')}</a>
            </article>
        {/foreach}

        {if $paginator != ""}
            <hr/>
            <div>
                {$paginator}
            </div>
        {/if}

    </div>

    {include 'partials/sidebar.tpl'}

First we check for the empty news case. Then we iterate through all the news to display the basic information such as the title, the date and an excerpt of the body, properly truncated using Smarty’s truncate modifier.

The output format of the date is defined in the settings of the field, however you can always bypass the default behavior of PW by applying your own formatting to the unformatted field:

{strftime("%d %B %Y", $news->getUnformatted('date'))}

Single News

The code for displaying a single news is obviously very similar to the above:

{extends "template.tpl"}

{block "content"}

    <div class="content">
        <article class="news">    
            <h2>{$page->title}</h2>
            <div class="meta">                           
                <ul>
                    <li>{$page->date}</li>
                    <li>{$page->createdUser->name}</li>
                </ul>
            </div>
            <div class="news-content">{$page->body}</div>
        </article>

        <ul class="pager">
            {if $page->prev->id != 0}
                <li><a href="{$page->prev->url}"><i class="fa fa-chevron-left"></i>{__('Previous')}</a></li>
            {/if}
            {if $page->next->id != 0}
                <li><a href="{$page->next->url}">{__('Next')}<i class="fa fa-chevron-right"></i></a></li>
            {/if}
        </ul>
    </div>

    {include "partials/sidebar.tpl"}

{/block}

We include a simple pager element to navigate to the previous and next siblings in our page tree. However we first have to check that the previous and next pages are not instances of NullPage by checking their ID property.

Finally we include the sidebar.tpl partial in our view. For example you can use this section to display a list of recent pages or a navigation menu. Just use your creativity.

Home

Now, after all the things we just learned, it’s time to create our Home Page. The controller is somewhat familiar, isn’t it?

<?php

$recent_news = $pages->find("template=news, sort=-date, limit=4");

$view->set('recent_news', $recent_news);

And the following is the view file:

{extends "template.tpl"}

{block "content"}
    <div id="slider">
    	<!-- Here we will insert a simple image slider -->
    </div>

    <div>
    	<div class="content">
            {$page->body}
    	</div>

    	{if $recent_news|@count > 0}
            <hr/>

            <h3>{__('Latest News')}</h3>

            <ul class="carousel">
                {foreach from=$recents_news item=news}
                    <li class="carousel-item">
                        <h3>
                            <a href="{$news->url}" title="{$news->title}" rel="bookmark">{$news->title}</a>
                        </h3>
                        <div class="meta">
                            <ul>
                                <li>{$news->date}</li>
                                <li>{$news->createdUser->name}</li>
                            </ul>
                        </div>
                        <div class="news-content">{$news->body|truncate:240:"..."}</div>
                        <a class="read-more" href="{$news->url}" title="{$news->title}">{__('Read More')}</a>
                    </li>
                {/foreach}    
            </ul>
    	{/if}
    </div>

{/block}

We reserved an empty place on the top for a simple image slider. You can hard-code your own or use a Repeater to create a repetition of title and image fields. However for this tutorial I’m going to use a module that I have personally developed for this purpose.

At this point you should have enough confidence to build the Instructor Index and Instructor Page by yourself, just apply the concepts that you have learned so far.

_init.php

The last file that I want to examine for this post, is _init.php2. As I said in Part One, this file contains the logic for all the elements that are shared between all the pages. Let’s take a look:

<?php

include_once("./include/functions.php");

// gets the current language code
$view->set('current_language', $user->language->isDefault() ? 'it' : $user->language->name);

// render a simple language switch for the current page
$view->set('langswitch', renderLanguageSwitch($page));

// get six random photos from a random gallery
$view->set('gallery_photos', getRandomGalleryPhotos(6));

// builds a menu for the footer
$view->set('footer_menu', getFooterMenu());

First we include a functions.php file which contains a couple of helpers functions, then we set a current_language variable to the view to hold the current 2-char language code of the user visiting the site.

Language Switch

Next we use a simple function to build a language switch based on the current page. This parameter is necessary because we may have pages that don’t currently have translations for all the languages, therefore we need to exclude those languages from the switch. The function, which takes inspiration from the PW forum, is the following:

<?php

function renderLanguageSwitch($page)
{
    $user = wire('user');
    $config = wire('config');
    $languages = wire('languages');

    $current_language = $user->language;

    $name = $current_language->name == 'default' ? 'it' : $current_language->name;

    $out =
        '<ul>
            <li><a onclick="javascript:return false;"><span class="flag_' . $name . '"></span>' . $current_language->title . '</a></li>';

    foreach($languages as $language)
    {
        if(! $page->viewable($language)) continue;

        if($language->id == $current_language->id) continue;

        $user->set('language',$language);

        $name = $language->name == 'default' ? 'it' : $language->name;

        $out .=
            '<li><a rel="alternate" hreflang="' .$name. '" href="' .$page->url. '"><span class="flag_' . $name . '"></span>' . $language->title . '</a></li>';
    }

    $out .= '</ul>';

    $user->set('language', $current_language);

    return $out;
}

The approach is really simple, but requires a bit of attention:

  • get the current language from the user viewing the site, with $user->language
  • build the disabled link of the current language
  • iterate through all the languages and check if the page is viewable with each one and if that language doesn’t correspond to the current language
  • if it passes our checks we have to change temporarily the language of the user, in order to access the translated URL of the page with $page->url
  • finally we set back the original language to the user

Now we have to get a few photos from a random gallery that should be displayed in the footer. By now you should be able to build the code on your own, it is really simple:

<?php

function getRandomGalleryPhotos($count = 3)
{
    $pages = wire('pages');

    $gallery = $pages->find("template=gallery, limit=1, sort=-random")->first();

    return $gallery->images->getRandom($count);
}

Now in the footer.tpl file you can access the variable gallery_photos to build the markup with Smarty. Don’t forget to also include a link to the galleries page.

Finally you can use the function getFooterMenu() to create a menu for the footer that takes only first-level pages. You can adapt this approach to build any kind of iteration on your pages, such as a full navigation menu or a XML sitemap of your site. Don’t be afraid to experiment with PW because it’s nearly impossible to break things.

<?php

function getFooterMenu()
{
    $page = wire('page');
    $pages = wire('pages');

    $out = '<ul>';

    $homepage = $pages->get("/");
    $children = $homepage->children;
    $children->prepend($homepage);

    foreach($children as $child) {
        $class = $child === $page->rootParent ? "menu-item current-menu-item" : "menu-item";

        $out .= "<li class='$class'><a href='{$child->url}'>{$child->title}</a></li>\n";
    }

    $out .= '</ul>';

    return $out;
}

Again, this code is really straightforward. We use the home page and its sub-pages to build an unordered list for the menu. Notice that we are building the markup directly in the function, just for simplicity, but you might as well use Smarty for the purpose.

Notice: I suggest you to always keep a browser tab opened on the wonderful PW Cheatsheet, which will help you to understand each PW function.

That’s it for this episode! In the next post we will look into the details on how to implement the PW modules introduced in Part One.

Stay tuned!

  1. The MapMarker module requires the Google Maps APIs to be present in the head section. Otherwise you can hard-code your solution, using your favorite approach.

  2. The underscore before the name of the file allows PW to skip the file when searching for new templates to add.

Test Laravel filesystem storage with Virtual File System

How to test Laravel filesystem storage with a Virtual File System provided by PHP-VFS and Flysystem. Continue reading