-
Notifications
You must be signed in to change notification settings - Fork 10
Component View
View component is defined with Web/View.php
class.
After reading through this guide, you will:
- Understand the Layouts and recognize how it extends and complements the WordPress template hierarchy.
- Know what is meant by the DRY Principle, why being DRY beats being WET, and see how most WordPress themes are WET.
WordPress is pretty smart. Every time you load up a request it will search for the most relevant template available in your theme and load it. This is the Template Hierarchy in action and it enables us to easily customize the appearance of our sites.
Want to customize a specific page named "About"? Just copy page.php
, to page-about.php
and edit
away to your heart's content. You only need to look at the success of WordPress to realize that
this system works because of its simplicity and accessibility. But it doesn't work perfectly.
To prevent each new template from having a duplicate header, footer and sidebar, WordPress
encourages you to separate that code into other templates and include them with the get_header()
,
get_footer()
and get_sidebar()
functions (all of which are based on get_template_part). Whilst
this makes your code more manageable, with only one edit needed to implement a change across all
templates, it still duplicates code that simply doesn't need to be duplicated; the code which calls
each of those templates.
In your typical layoutless theme, every page template will look something like the following:
<?php get_header(); ?>
<div class="main">
<div class="content">
<?php // Our page specific markup and loop goes here ?>
</div>
<?php get_sidebar(); ?>
</div>
<?php get_footer(); ?>
Even though we know that every template will take this base format and render the header, footer, sidebar calls each time, we still need to continuously repeat the code to keep WordPress happy; it's laborious and unnecessary.
DRY simply means Don't Repeat Yourself and conforming to the DRY Principle means:
Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.
So whilst we have a base format for our pages, this "knowledge" is written countless times, spread across numerous files and has no authoritative representation. This is the opposite of DRY code and it's usually described as being WET, meaning that you Write Everything Twice.
As you can see from the standard template, WordPress goes beyond Continously Repeating Yourself. Whatever you want to call it, it wastes your time when creating the code, when reading the code and when editing the code; it's a lose-lose-lose situation (plus repetition is only fun in rhetoric) but it's easy enough to avoid.
The goal of a layout is to remove any repeated markup from individual templates and put it
into a single file. This file, {theme}/views/layouts/main.php
becomes the single, unambiguous, authoritative representation
of knowledge (i.e. the main format code). By doing this we can put the focus entirely on the page
specific markup and loop, simplifying our templates to look like this:
<?php
$this->extends('layouts/main');
// Our page specific markup and loop goes here
?>
It's neat. It's tidy. You never need to make calls to get_header()
, get_footer()
or get_sidebar()
again. You can also refactor the main format of your site by editing {theme}/views/layouts/main.php
.
But best of all, it takes less than 50 lines of code to do so.
This pattern is used in all modern PHP Frameworks: Laravel, Yii 2 and Symfony (Symfony uses Twig, and Twig also has layouts).
First realization of similar feature for WordPress was described in 2011 and was called a Theme Wrapper (read article).
Let's take a closer look into layouts.
This is done using the standard WordPress Template Hierarchy, which, as mentioned before,
selects the most relevant template as our starting point. Once this template has been chosen,
but before it's loaded, WordPress runs the template_include($template)
filter.
We use this filter to run our wrap function that saves the real $template
path and always return
root {theme}/index.php
file as a template. This file will be an entry point of our templates system.
<?php
namespace JustCoded\WP\Framework\Web;
use JustCoded\WP\Framework\Objects\Singleton;
/**
* Views base class.
* Used for layouts and render partials
*/
class View {
use Singleton;
/**
* Layouts call chain.
*
* @var array
*/
private $extends = array();
/**
* Theme template path to be loaded.
*
* @var string
*/
public $template;
/**
* View constructor.
*
* Executed immediately before WordPress includes the predetermined template file
* Override WordPress's default template behavior.
*/
protected function __construct() {
add_filter( 'template_include', array( $this, 'init_template' ), 999999 );
}
/**
* Implement template wrappers. For this we need to remember real template to be loaded.
* Then we return the object itself to be able to manipulate the loading process.
*
* @param string $template Template to be included inside theme.
*
* @return $this
*/
public function init_template( $template ) {
$this->template = $template;
return $this;
}
/**
* Convert object to string magic method.
* We replaced string with object inside `template_include` hook, so to support next include statement we need
* to add magic method, which make object to string conversion.
*
* Here we will just return theme index.php file, which will be the entry point of our views engine.
*
* @return string
*/
public function __toString() {
return locate_template( array( 'index.php' ) );
}
// ...
}
This code makes WordPress template loader to load our theme root {theme}/index.php
file.
Furthermore, after including {theme}/index.php
we have a global variable $template
inside, which is actually
our View instance. So we can find out the real template and run a method to include templates and
process template inheritance.
{theme}/index.php
<?php
/**
* Main template in WordPress theme.
* Used to load views engine
*
* @see /views/ folder instead
* @var $template \JustCoded\WP\Framework\Web\View
*/
$template->run();
Inside each specific template you should specify the layout to extend at the beginning of the file:
{theme}/views/page/page.php
<?php
/* @var \JustCoded\WP\Framework\Web\View $this */
$this->extends( 'layouts/main' );
// Our page specific markup and loop goes here
We include all templates inside a View class instance, that's why $this
variable is available
inside view files and is pointing to a View class instance.
extends()
method simply memorize the layout to inherit from and starts the output buffer.
View component
<?php
class View {
// ...
/**
* Registers parent template.
* Parent template will be rendered just after current template execution.
*
* To use current template generated html use `$content` variable inside the parent view
*
* @param string $layout View name to register.
*
* @return bool
* @throws \Exception If no parent view template found.
*/
public function extends( $layout = 'layouts/main' ) {
if ( false === $layout ) {
return false;
}
// WordPress compatibility to still process usual headers.
if ( empty( $this->extends ) ) {
do_action( 'get_header', null );
}
// check that we have required template.
$template = $this->locate( $layout, true );
// memorize the template.
array_push( $this->extends, $template );
// start buffer.
ob_start();
return true;
}
// ...
After processing the main template View component will get the output buffer as $content
variable
and include the layout template (actually it will include any template, which was specified inside
$this->extend()
directive).
Layout file will looks like this:
{theme}/views/layouts/main.php
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<!-- Your head part -->
</head>
<body <?php body_class(); ?>>
<div id="page">
<?php $this->include( 'partials/header' ); ?>
<div id="content" class="site-content">
<div id="primary" class="content-area">
<main id="main" class="site-main" role="main">
<?php echo $content; ?>
<?php $this->include( 'partials/sidebar' ); ?>
</main><!-- #main -->
</div><!-- #primary -->
</div><!-- #content -->
<?php $this->include( 'partials/footer' ); ?>
</div><!-- #page -->
<?php wp_footer(); ?>
</body>
</html>
Furthermore, you're not limited to use only one extends() directive. You can inherit several templates inside each other. This is the main advantage of Layouts over Theme Wrapper.
Layout files are loaded after main content is generated. Template inheritance loading process is activated
inside View::run()
method, which is called inside {theme}/index.php
file:
View.php
<?php
class View {
// ...
public function run() {
// add alias.
$template = $this->template;
include $this->template;
$this->wrap();
}
}
wrap()
method checks for existed output buffers and registered templates through extends()
method and then
includes them:
View.php
<?php
class View {
// ...
public function wrap() {
if ( empty( $this->extends ) ) {
return false;
}
while( ob_get_contents() && $template = array_pop( $this->extends ) ) {
$content = ob_get_contents();
// clean view file buffer.
ob_clean();
// reset query to protect header from unclosed query in the content.
wp_reset_postdata();
// render under the existing context.
include $template;
}
}
}
Even when we use layouts as base wrapper html, it can be pretty big. Or different layouts can share
similar parts. To stay DRY we create small templates, which can be included in other templates and
call them partials. Generic partials are placed in {theme}/views/partials/
folder.
To include another template you can use View component method include()
:
<?php $this->include( 'folder/template-name', $params ); ?>
This method replace WordPress core get_template_part()
function. It support child themes as well,
so if you created a child theme you can re-write specific view files inside child theme as usual
WordPress templates.
$params
variable allows you to pass some already defined variables in partial template (for example $model
).
If you need to share some data from main template to layout you can use $this->params
class property inside views.
Let's check an example:
{theme}/views/page/page.php
<?php
$this->extends('layouts/main');
$this->params['subheading'] = get_post_meta('subheading');
{theme}/views/post/category.php
<?php
$this->extends('layouts/main');
$this->params['subheading'] = get_term_meta('subheading', get_queried_object_id());
{theme}/views/layouts/main.php (or any other view)
<!-- ... -->
<h3><?php echo esc_html($this->params['subheading']); ?></h3>
<!-- ... -->
During daily usage of layouts we found several issues, which can happen with some 3d-party plugins.
Some plugins try to use javascript directly inside the_content()
and they think the script is already
registered by wp_head()
. With layouts wp_head()
is called AFTER the content part is generated!
Standard request cycle
HTTP Request → WP index.php → wp-config → WP core → Theme functions.php
→ WP Hooks (setup_theme, init …) → WP Query
→ {template} → get_header → CONTENT → get_footer
Layout-based request cycle
HTTP Request → WP index.php → wp-config → WP core
→ Theme functions.php → /app/Theme.php → Theme component objects
→ WP Hooks (setup_theme, init …) → WP Query
→ {view} → {Model} → CONTENT
→ {layout file} → get_header / get_footer