Article

Modernizing WordPress Development with Sage 9: Part 3

Sage-9-Controller

In Part 1 and Part 2 of this series, we’ve covered Controller in Sage 9. According to MVC pattern (Model-View-Controller), in the ideal circumstance, Views and Controller will be completely separated which enables us to reuse Views more conveniently.

Imagine you are tailoring a website. There are parts that share the same HTML markup but use data from 3 different sources (let’s say, one from a Post, one from an API, and one from a custom database).

The most common way to achieve this is to create a partial template, then have condition checking implemented, where the partial template is displayed and data is pulled from the source accordingly. Another approach would be to create 3 partial templates using the same markup but change the data source. Either way, both approaches come with their own headaches.

Controller: The Knight in the Shining Armor

As we mentioned above, in MVC, Views, and Controllers are completely separated. Views don’t need to “know” where the data comes from. They simply wait for the Controller to feed them data to display.

So, to solve the aforementioned problem, consider creating a template echoing some variable. Then create a Controller to expose the data to the template.

Creating Our Controller

While we were writing this entry of the series, Sage 9.0.2 has released. It included a major change upgrading the package soberwp/controller from version 9.0.0-beta4 in previous release to current 2.1.0. Thus, this entry got updated to cover soberwp/controller 2.1.0

First, navigate to the directory ./app/Controllers where there will be two Controllers existing in the directory already, which are App.php and FrontPage.php

App.php is the global Controller. Every template in our theme will be able to access data/method in this Controller. Other controllers will be named after the template file name but converted to CamelCase. For example:

  • FrontPage.php is corresponding to front-page.blade.php
  • Single.php is corresponding to single.blade.php
  • SinglePostType.php is corresponding to single-post-type.blade.php

You can learn more about template file names via WPHierarchy.

Inside the controller there are some requirement, listed below:

  1. namespace must be AppControllers
  2. import (use) SoberControllerController
  3. The class name must be the same as the file name (CamelCase of template file name)
  4. The class must be extended from Controller

For example:

<?php // app/Controller/Single.php
namespace AppControllers;

use SoberControllerController;

class Single extends Controller {
  // ... methods go here
}

Inside the class, There are 3 types of methods we are going to use:

  1. public function MethodName() Public method will be used to expose data back to View, where MethodName() will be converted to a snake_case variable as $method_name. From here, we can use it inside the template.
  2. public static function MethodName() Static method won’t be converted to a variable. You can access them directly by runningClassName::MethodName(). Notice that this is a static method call. That means you can also receive/pass arguments to the method if needed.
  3. protected function MethodName() Protected method won’t be exposed to the template. That means you can’t access it from outside the class. This is generally used for data processing inside an object. (Dependencies injection is also available in __construct())

Here is the example of exposing data from Controller to view

<?php // app/Contollers/Single.php
namespace AppControllers;

use SoberControllerController;

class Single extends Controller {
  public function PostTitle() {
    return $something; // get_the_title();
  }
}
{{-- resources/views/single.blade.php --}}
<h3 class="entry__title">
  {{ $post_title }}
</h3>

Create Components with Trait

In a more complex theme, we will usually run into some circumstances of using Partial Templates for reused elements, like headers and footers. And if these partial templates are using data from the Controller, we might end up repeating every method in each Controller. We can avoid this situation by using Trait.

Trait is a PHP built-in feature. It will let you write a set of method and reuse them in classes (it’s metaphorically same as file inclusion).

For example, we have a page header as a partial template like this:

Let’s say the title and description on the header can be changed conditionally. So, we use Controller to expose data back to the template file,  avoiding the template being spaghetti from using a lot of if ... else.

In this case, instead we have to write the same method in every Controller, we create our trait and use it in controllers (we suggest keeping the file name and directory structure the same between templates and controllers/traits to make them easily recognizable. For example, trait file for views/partials/page-header.blade.php will be app/Controllers/Partials/PageHeader.php)

To create a trait, just declare the trait name and write methods inside the trait block as usual:

<?php // app/Controllers/Partials/PageHeader.php
namespace AppControllersPartials;

trait PageHeader {
  public function PageTitle() {
    return $something;
  }
  
  public function PageDescription() {
    return $something;
  }
}

And in the Controller file, we call Trait with the use keyword inside a class block:

<?php // app/Controllers/Single.php
namespace AppControllers;

use SoberControllerController;

class Single extends Controller {
  use AppControllersPartialsPageHeader; // call Trait PageHeader
}

Inherit Methods between controller using Tree interface

Let’s take a minute and look back at WordPress template hierarchy (here), You will notice that WordPress will look for less and less specific template files if it can’t find the more specific template. For example, if WordPress can’t find single-post-type.blade.php, then it will look for single.blade.php (and it will end up at index.blade.php, eventually).

Following the hierarchy, Controller has a Tree interface to let it inherit methods from a less specific template. For example, we have method PageTitle() in Controller Index.php. If we create the Controller Single.php without implementing Tree, we won’t be able to use PageTitle() from Index.php. Then, we’ll have to write the same method in Single.php

The Tree interface will solve this issue. All you need to do is use the module SoberControllerModuleTree and implement it to our Controller:

<?php // app/Controllers/Index.php
namespace AppControllers;

use SoberControllerController;

class Index extends Controller {
  public function PageTitle() {
    return get_the_title();
  }
  
  public function PageDate() {
    return get_time();
  }
}
<?php // app/Controllers/Single.php
namespace AppControllers;

use SoberControllerController;
use SoberControllerModuleTree; // call Tree Module

class Single extends Controller implements Tree {
  // implementing Tree interface
}

As shown in the example above, you can use $page_title and $page_date in the template single.blade.php, which actually was declared in Controller Index.php

Using the Tree interface is a better solution than using Trait in some circumstances, because you can safely redeclare the same method name in the Controller. If you want to change something in PageTitle() you can just redeclare it like this:

<?php // app/Controllers/Single.php
namespace AppControllers;

use SoberControllerController;
use SoberControllerModuleTree;

class Single extends Controller implements Tree {
  public function PageTitle() {
    return "Page: " . get_the_title();
  }
}

And in single.blade.php you’ll be able to call $page_date which came from Index.php, while $page_title will now using data from Single.php

Note: only public method can be inherited via the interface.

A Final Note: Template Override

There is ONE edge case where you can’t just declare a class name for the template. That case is 404.php since you can’t name the class using the number. To solve it, you can still specify which template this Controller is for by declare property $template

<?php
namespace AppControllers;

use SoberControllerController;

class PageNotFound extends Controller {
  protected $template = "404";
}