Doing some standard WordPress stuff.
Previously
I’ve set out to try and implement the Action-Domain-Responder pattern in a WordPress plugin.
While the usefulness of it might amount to nothing in the end the code involved, and the techniques, I find stimulating.
The first post in the series might prove a better starting point than this one to understand the reasoning and the flow of the code I’m showing.
This is a big experiment
While I try to reach the goal of implementing the ADR pattern in the context of a vanilla WordPress installation I’m also working on the new possibilities of the WordPress
module to put in place real functional testing in WordPress; the plugin is pulling wp-browser from this branch and the test code I’m showing is using it.
At the time of publishing this article the wp-browser branch is still experimental and this means some changes are in place in the plugin code; I’m requiring the dev-wploader-go-to
branch of wp-browser in the plugin Composer configuration:
composer require --dev lucatume/wp-browser:dev-wploader-go-to
The functional suite is configured to use the WPDb
, WPFilesystem
and WordPress
modules: the first two to manage the WordPress database and files, and the latter to provide the functional testing capabilities.
The WordPress
module is then configured to handle requests in the same scope as the tests setting the insulated
flag parameter to false
:
# Codeception Test Suite Configuration
#
# Suite for functional tests
# Emulate web requests and make WordPress process them
actor: FunctionalTester
modules:
enabled:
- WPDb
- Asserts
- WPFilesystem
- WordPress
- \Helper\Functional
config:
WPDb:
dsn: 'mysql:host=%DB_HOST%;dbname=%DB_NAME%'
user: %DB_USER%
password: %DB_PASSWORD%
dump: 'tests/_data/dump.sql'
populate: true #import the dump before the tests
cleanup: true #import the dump between tests
url: '%WP_URL%'
urlReplacement: true #replace the hardcoded dump URL with the one above
tablePrefix: 'wp_'
WordPress:
depends: WPDb
insulated: false
wpRootFolder: '%WP_ROOT_FOLDER%'
adminUsername: 'admin'
adminPassword: 'admin'
adminPath: '/wp-admin'
WPFilesystem:
wpRootFolder: '%WP_ROOT_FOLDER%'
Was the insulated
parameter of the WordPress
module configuration not set or set to true
it would behave as usual handling any request in a separate PHP process from the tests; to read more about the experimental branch have a look at this post.
Header and footer actions
Once the inebriating power of being able to route anything to anywhere wears out, a shortcoming of this solution kicks in: no template actions or filters are applied when rendering templates this way. WordPress power lies in its flexibility and ease of use and that is based largely on how its functionalities can be extended by themes and plugins; those themes and plugins, in turn, rely on the event based architecture of WordPress to run their business logic.
The definition “event based architecture” means, in code terms, actions and filters.
In its current incarnation the ADR plugin successfully handles the request for the /post
page but does so without allowing any functionality provided by plugins to kick in.
The first remedy that comes to mind is to, at a minimum, fire the wp_head
and wp_footer
actions.
Before adding any code I add a new test to the functional
suite, the first one actually, to make sure I know what I’m looking for:
codecept generate:cest functional "HooksSupport\HeadAndFooter"
Before answering the question “why a functional test?” it’s worth taking a look at the test code:
<?php
namespace HooksSupport;
use FunctionalTester;
class HeadAndFooterCest {
public function _before(FunctionalTester $I) {
$I->useTheme('twentyseventeen');
add_filter('adr.debug', '__return_true');
add_filter('klein_die_handler', function () {
return 'echo';
});
}
/**
* It should run the wp_head action while managing routes
*
* @test
*/
public function should_run_the_wp_head_action_while_managing_routes(FunctionalTester $I) {
add_action('wp_head', function () use (&$fired) {
$fired = true;
echo '<meta foo="bar">';
});
$I->amOnPage('/posts');
// we are on a page managed by the plugin
$I->canSeeElement('body.adr');
// the action fired
$I->assertTrue($fired);
// the action output is there
$I->seeInSource('<meta foo="bar">');
}
/**
* It should run the wp_footer action while managing routes
*
* @test
*/
public function should_run_the_wp_footer_action_while_managing_routes(FunctionalTester $I) {
add_action('wp_footer', function () use (&$fired) {
$fired = true;
echo '<p>Hello from the footer</p>';
});
$I->amOnPage('/posts');
// we are on a page managed by the plugin
$I->canSeeElement('body.adr');
// the action fired
$I->assertTrue($fired);
// the action output is there
$I->seeInSource('<p>Hello from the footer</p>');
}
}
There are some things going on here that is worth pointing out.
In the _before
method I’m filtering the default klein52 output handler to echo
the output in place of using die
to display it; since the handling is happening in the same process as the tests any code that is calling the die
or exit
instruction will kill the tests too; this is possible due to the fact the WordPress
module is not running in “insulated” mode.
Leveraging the fact the request is being handled in the same process as the tests I’m hooking in the wp_head
and wp_footer
actions directly expecting the code will be executed; this is possible due to the fact the WordPress
module is not running in “insulated” mode.
I’ve added a body
class check in the test to make sure that result comes from the plugin and not from the theme simply handling the request; the test installation has the Twentyseventeen theme installed and if the plugin was deactivated the tests would fail on the check for the body.adr
class (the theme has no notion of the adr
class after all) check. The others would, instead, pass: the theme will call the wp_head
and wp_footer
actions and will output the actions output by default.
As usual the first test run results in a failure:
But why did I use a functional test in place of an acceptance one? Here my personal discretion kicks in.
The reason I call this a “functional” test has to do with the testing methodology: I’m accessing WordPress internals, hooking actions and checking on a body class, to prepare and check my tests.
In the assumption that acceptance tests simulate a user interaction, a normal user, one that does not use the developer tools to inspect the code for example, user would and could not make all these checks and set up operations; just my opinion though.
Passing the first test
What should happen for this tests to pass is that the body_class
function, the wp_head
function and the wp_footer
function are called while rendering the output.
Since I’m relying on Twig to render the response output I modify the template to this:
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Posts</title>
{{ wp_head() }}
</head>
<body {{ body_class('adr') }}>
<main class="posts">
{% if posts %}
<ul>
{% for post in posts %}
<li class="post">
<h3>{{ post.post_title }}</h3>
{{ post.post_content }}
</li>
{% endfor %}
</ul>
{% else %}
<p>Nothing found...</p>
{% endif %}
</main>
{{ wp_footer() }}
</body>
</html>
In the template I’m using a Twig extension that will go from {{ function() }}
to the output of that function.
No such extension exists by default in Twig so I’ve created one in the src/adr/Twig/FunctionExtension.php
class:
<?php
class adr_Twig_FunctionExtension extends Twig_Extension {
protected $supportedFunctions = array();
public function setSupportedFunctions(array $supportedFunctions) {
$this->supportedFunctions = $supportedFunctions;
}
public function getFunctions() {
$functions = array();
foreach ($this->supportedFunctions as $function) {
$functions[] = new Twig_SimpleFunction($function, $function);
}
return $functions;
}
}
Provided some functions the extension will register each as a legit Twig function; by default no functions are supported so: where is that list of supported functions set?
That list is set by the adr_ServiceProviders_Templates
class; the service providers task is exactly to set up and provide one or more services ready to user and the “service” provided by this class is a ready to use Twig environment (an instance of the Twig_Environment
class in fact):
// file src/adr/ServiceProviders/Templates.php
class adr_ServiceProviders_Templates extends tad_DI52_ServiceProvider {
/**
* Binds and sets up implementations.
*/
public function register() {
$this->container->singleton('Twig_Environment',
array($this, 'buildTwigEnvironment'));
}
/**
* @return Twig_Environment
*/
public function buildTwigEnvironment() {
$root = dirname(dirname(dirname(dirname(__FILE__))));
$loader = new Twig_Loader_Filesystem($root . '/templates');
$options = array(
'cache' => $root . '/templates/cache',
);
// if debugging never use cached templates
if (apply_filters('adr.debug', false)) {
$options['debug'] = true;
$options['auto_reload'] = true;
}
$twig = new Twig_Environment($loader, $options);
$functionExtension = new adr_Twig_FunctionExtension();
// allow filtering the supported functions
$supportedFunctions = array('wp_head', 'wp_footer', 'body_class');
$supportedFunctions = apply_filters('adr.twig.supportedFunctions', $supportedFunctions);
$functionExtension->setSupportedFunctions($supportedFunctions);
$twig->addExtension($functionExtension);
return $twig;
}
}
I’m applying the adr.twig.supportedFunctions
filter to allow myself and third-party plugins to filter the list of supported functions; for the time being I’m doing the least amount of job and setting the ones I need as defaults.
Once the code is in place the test will pass:
Why filter in the service provider?
Why not filter the supported functions in the class?
The answer is a personal one:
- if a filter or action is about setup I want it to be where the setup happens; when using service providers I put all setup processes in the service providers
- having the filtering happen in the class would mean the class will now depend on the
apply_filters
function and real unit testing, no WordPress involved, just became more difficult for no benefit - I dislike the idea of the class “knowing” about its surroundings and use context if it can be avoided; while that’s a feeble defense for not filtering in the class, hooking on actions is a prime example of knowing and relying on the context
This applies to all filters and actions: I usually put them all in the service providers and that’s what I will do across the development of the plugin.
Next
I will try to “generalize” the code to separate the klein and do_parse_request
based handling logic from the ADR implementation to build something reusable without the ADR pattern in mind; furthermore I will try to make the plugin play nicer with the active theme.