WordPress provides a solid foundation from which we can explore seemingly endless opportunities. Even when I was totally new to PHP development, I was still able to conduct experiments using WordPress. This got me started as a developer and in turn helped me learn PHP and JavaScript.
Before last summer, I had no experience working with RESTful APIs. But then I got introduced to the WordPress REST API, and suddenly had a good way (and reason) to learn how to use RESTful APIs. That’s what I love so much about WordPress.
Recently I’ve been teaching myself to use the Symfony router system for front-end routing and templating on a WordPress site. WordPress is, again, giving me an easy way to step into a new concept.
It’s not just a good learning opportunity, it’s a very useful skill to have when doing WordPress site development or making a WordPress-powered app. So I’d like to show you how to do this, and explain why you might want to.
Why Should You Learn This?
I’m totally aware that this article shows a more complicated way of doing something that WordPress already does automatically. However, there are plenty of good reasons to learn how to do this sort of thing — i.e. for educational reasons and because sometimes reinventing the wheel may result in a more performant wheel.
To begin with, this could potentially be a lot more performant than WordPress’s router templating system because the default is highly dynamic, designed to work with any site. By using a more customized and declarative system, you could potentially serve front-end requests much faster.
In addition, while the examples in this article show how to serve blog posts and blog archives — something WordPress does quite well — they can be applied to any post type or data from a custom table, or retrieved via a remote API. The WordPress routing and templating system was designed with blogs in mind, and that is not the best paradigm for all sites.
Beyond the practical example in your work, if you do custom site development, this is also a great learning experience. There are a lot of CMSs and PHP frameworks that use the Symfony router and other Symfony components. If you’re looking to mature your PHP development skills, this type of learning experience is a good way to get started with customs that are non-specific to WordPress.
WordPress is incredibly powerful, but it’s not always the best option. As WordPress-centric PHP and JavaScript developers, we shouldn’t choose WordPress because it’s the only thing we know, we should choose it because it’s the best tool for the job. Similarly, we should choose a different PHP or JavaScript framework when it’s a better choice.
The same argument can be made for learning a front-end JavaScript framework like Angular or React. I love reading what Roy Sivan has been writing on JavaScript MVC frameworks — it makes me realize I have a lot to learn there. I’m more advanced as a PHP developer than I am as a JavaScript developer, so what I have to teach is PHP frameworks.
Creating Your Router
In this article, I’m going to demonstrate the basics of using the Symfony router by showing how to do basic blog site functionality. I will create routes for posts, post archives, a front-page, and 404 errors. You can apply what you learn from this process to create custom routes for the unique needs of your own projects.
I will discuss where to load your router, how to install the Symfony components we need, how to build the router, and briefly explain templating with Symfony’s Twig templating system. The last part, PHP templating is a huge topic in itself, but it’s worth discussing because you need to be able to show your content.
This tutorial assumes you’re already familiar with Composer and object oriented PHP. This definitely isn’t a beginner tutorial, but if you’re up for the challenge, it should be fun.
Where To Load Your Router
I’m starting with where to load the file structure and at the action before installation. This is because you need to know where to put your Composer file. Since this will most likely be customized for a specific site, it’s really up to you where you want to store your files.
Theoretically, you could create a theme for this, but I think that defeats the purpose. In my opinion, the absolute latest you want to load your router is at template_redirect. But, the earlier you do it, the faster the response will be, and the less unnecessary processing WordPress will have to do.
Of course, the earlier you load your router, the fewer components of WordPress will be loaded. That’s good and bad. Loading early means that you will have less overhead, but you also risk calling functions or class that are not yet fully ready to be used.
Personally, I think it makes the most sense to use a mu-plugin for your router, though including a file directly in WordPress’s main index.php is also an option. Using a mu-plugin means that the plugin can’t be disabled, which would be catastrophic for your site. It also means you can load at mu_plugin_loaded, which is fairly early in terms of WordPress’ loading, but get_posts() is available to you.
You will need to experiment with different hooks to load at depending on your own needs. For this example, we will use a mu-plugin. I created a file called router.php in my mu-plugins folder, and a directory called router that holds the Composer file and the classes and templates for the router. The actual mu-plugin just includes the autoloader and instantiates the main router class.
Because my way of using this router doesn’t require a theme, I defined WP_USE_THEME to false in my index.php.
Installing
For this demo, I’m actually going to use the micro-framework Silex. Silex gives me everything I needed in a pretty small package. There are a few other micro-frameworks that are similar, including Slim and Lumen (a lightweight version of Larvel), which are both worth checking out.
These micro-frameworks are small, highly-performant, and built using Symfony components. Once you have more experience using micro-PHP frameworks, you can make an informed decision on which one makes most sense for you, or if you need a full framework, like Larvel.
In your composer.json file, you need to make sure to include Silex. You probably want to, but don’t have to add Twig, the default template engine for Symfony. Here is what the require section of my composer file looks like:
[js]
"require": {
"silex/silex": "~1.3",
"twig/twig": "^2.0@dev"
},
[/js]
As a side note, I was unable to get Composer to build this package when I ran the composer update in OS X, since my version of OS X used an outdated version of PHP. I SSHed into my virtual machine — created with VVV — and ran the composer update from there.
I also used the autoload section to create a PSR-4 namespace in a sub-directory of the router called “src.” You can see the full Composer file here. My namespace is shelob9\router, however, you should use your own username or company name as the root namespace.
With that in place, I laid out my plugin like this:
[php]
if ( is_admin() || ( defined ( ‘REST_REQUEST’ ) && REST_REQUEST ) || ( defined( ‘DOING_AJAX’ ) && DOING_AJAX ) && ( isset( $PHP_SELF ) && in_array( rtrim( $PHP_SELF, ‘/’ ), array( ‘/wp-login.php’, ‘/wp-register.php’, ‘/wp-cron.php’ ) ) ) ) {
return;
}
include_once( dirname( __FILE__ ) . ‘/router/vendor/autoload.php’ );
new \shelob9\router\routes();
[/php]
This will ensure that the mu-plugin doesn’t break the admin, the WP REST API plugin, admin-ajax, or login. It will also ensure that I’m not loading up the main class of my router “routes,” which I will cover in the next section.
Building The Router
The router will actually turn a URL into a page. Let’s start with something very simple. Here’s the beginning of a router, which is very basic, but will serve responses:
[php]
<?php namespace shelob9\router; class routes { function __construct() { $app = new \Silex\Application(); $app->get(‘/blog/{id}’, function ($id) use ($app) {
if( 0 == absint( $id ) ) {
$app->abort( 500, ‘Invalid!’ );
}
$post = get_post( $id );
if ( empty( $post ) ) {
$app->abort(404, "Post $id does not exist.");
}
return ‘Post Title: ‘. $app->escape( $post->post_title );
});
$app->run();
}
}
[/php]
This class is very simple. In its constructor, we create a new instance of Silex and add one route to it. It’s basic, but with this in place, a request to blog/24 will either display the title of post 24 or a 404 error and message if post 24 doesn’t exist.
How does it work? We use the get method of the Silex application to add a route for get requests starting with /blog/, but to make it dynamic we use a variable for the second part of the URL. The second argument of the get method is the callback to create the response. For simplicity, I used a closure, but you could call another function or class here.
In the callback, we first test if $id is greater than zero. If it’s not, we can use the abort method to create a response immediately. It’s first argument is the HTTP status code, and the second argument is the message to return. This is a useful way to create 404 or 500 errors.
If that check doesn’t fail, the next step is to pass $id to get_post() to attempt to get a post. If get_post returns false, we can use abort to create a 404 error. If not, for now, we just return its title so we can show that this works.
Not that complicated? Let’s add a second route. This time, we will make our front page to show a list of posts. This route will just be “/”. If we wanted to, we could use /blog.
Here is the refactored router that loops through the first ten posts on the front page:
[php]
<?php namespace shelob9\router; class routes { function __construct() { $app = new \Silex\Application(); $app->get(‘/{id}’, function ($id) use ($app) {
if( 0 == absint( $id ) ) {
$app->abort(404, ”);
}
$post = get_post( $id );
if ( empty( $post ) ) {
$app->abort(404, "Post $id does not exist.");
}
return ‘Post Title: ‘. $app->escape( $post->post_title );
});
$app->get(‘/’, function () use ($app) {
$posts = get_posts(
array(
‘posts_per_page’ => 10
)
);
if ( empty ($posts ) ) {
$app->abort(404, "No posts found." );
}else{
foreach( $posts as $post ) {
$out[] = $post->post_title;
}
return implode( "/n", $out );
}
});
$app->run();
}
}
[/php]
At this point, I hope you’re getting the idea. You probably also want to use the post slug instead of the ID and you may want to have pagination. If you see how variables are passed from the URL to the router, then it shouldn’t be hard for you to see how to build that out.
We can provide pagination with a route, like this:
[php]
$app->get(‘/page/{page}’, function ($page) use ($app) {
if( 0 == absint( $page ) ) {
return;
}
$posts = get_posts(
array(
‘posts_per_page’ => $this->posts_per_page,
‘paged’ => $page
)
);
}
[/php]
We can also refactor our /id to use a name instead of ID number if we want. We will just have to use get_posts instead of get_post to query by post_name.
Before we pull this together, let’s back up and add some Twig templating, so we’re not just echoing properties of the post object directly.
Templating With Twig
This system is very modular. You don’t have to use Twig as your templating system, or use any templating system at all, for that matter. There are other PHP templating systems out there, such as Blade and Handlebars.php.
I’m not going to get too far into how Twig works, as it is highly intuitive and well documented. I will show you how to load Twig templates in your router, as well as some basic templating with it.
First, create a directory called templates in your router directory. In it, create three files:
- posts.twig: This will be used for archive views.
- post.twig: This will be used for single post views.
- single.twig: This is a partial that both of the other files will use for showing a single post’s data.
Let’s start with posts.twig. You will want to start by creating full HTML5 markup for this file, as if you were creating a static HTML file. In the element where you wish for your posts to be displayed, use this Twig markup:
[html]
{% for post in posts %}
{% include ‘single.twig’ %}
{% endfor %}
[/html]
This is a simple loop that will loop through each post and display it with the single.twig file. If you want, you can see my complete posts.twig file here.
The post.twig file will be almost identical in that file we will only ever be showing one post, so the loop is unneeded. We can just use:
[html]
{% include ‘single.twig’ %}
[/html]
Again, you can see my full post.twig if you want. I hope you are seeing how to include partials and thinking about navigation, footers, sidebars, and the like.
Now, we just need the single.twig, which will be used to render an individual post.
The syntax for using the data passed to the template engine is {{ array_name.field }}. Since our array is called “post” we will use {{ post.post_content }} to show the post content. Twig has pretty cool escaping, using a pipe, which you will see in my example single.twig, which just looks like this:
[html]
<article id="{{ post.ID|e(‘html_attr’) }}">
<h1>{{ post.post_title }}</h1>
<div class="entry-content">
{{ post.post_content|raw }}
</div>
</article>
[/html]
Do be careful of using raw as your escaping scheme. While it does allow for HTML tags to be rendered, which is good, it also allows for executable JavaScript to be rendered. In the actual project I am considering using a router like this, I have a special method for stripping <script> tags in from the post title, excerpt, and content.
Now that our templates are setup, we need to refactor our router to use Twig. The first thing we have to do is add Twig as a service in the $app variable. So after we instantiate the application class, and before we create routes, we will the register method to do so. Here is what that looks like:
[php]
$app->register(new \Silex\Provider\TwigServiceProvider(), array(
‘twig.path’ => dirname( __FILE__ ).’/templates,
‘debug’ => true,
));
[/php]
Please notice two things here in the array that are used as the second argument for this method. The first key of the array defines the path for templates. You can customize that to fit your needs. The second key is debug. It defaults to false, which you want in production. But leaving it as true is very useful in testing, and will output a stack trace when error happen.
Now, that Twig is available in our app, let’s use it in some routes. Here is a route for a single post:
[php]
$app->get(‘/blog/{name}’, function ($name) use ($app) {
$posts = get_posts(
array(
‘post_name’ => $name,
‘post_type’ => ‘post’,
‘post_status’ => ‘publish’
)
);
if( empty( $posts ) ) {
$app->abort(404, "Post $name Not Found!!");
}
$data = array( ‘post’ => (array) $posts[0] );
return $app[‘twig’]->render( ‘post.twig’, $data );
});
[/php]
Most of this is very basic usage of get_post(). The important thing is the last line, here we return using $app[ ‘twig’ ]->render(); That method takes two arguments. The first is the template to use. The other is an array of data to render. Notice that I casted the WP_Post object to an array and put it in an array key called “post”. This is why the “{{ post.post_content }}” syntax is used. If I had put in a key called “foo” then I would use “{{ foo.post_content }}” in my template.
The reason I used “post” was so that my post template and my posts templates could use the same partial. In the route for my main page, and paginated archives, I uses “posts” as the key, so I can loop through posts as post. It just makes for easy reading.
Here are my routes for the main page, and pagination, using the posts.twig template for both:
[php]
$app->get(‘/’, function () use ($app) {
$posts = get_posts(
array(
‘posts_per_page’ => 10,
‘post_type’ => ‘post’,
‘post_status’ => ‘ publish’
)
);
if( empty( $posts ) ) {
$app->abort(404, "No posts found");
}
foreach( $posts as $post ) {
$data[] = (array) $post;
}
$data = array( ‘posts’ => $data );
return $app[‘twig’]->render( ‘posts.twig’, $data );
});
$app->get(‘/page/{page}’, function ($page) use ($app) {
if( 0 == absint( $page ) ) {
return;
}
if( empty( $posts ) ) {
$app->abort(404, "No posts found");
}
foreach( $posts as $post ) {
$data[] = (array) $post;
}
$data = array( ‘posts’ => $data );
return $app[‘twig’]->render( ‘posts.twig’, $data );
})->convert( ‘page’, function ( $page ) {
return (int) $page;
});
[/php]
I’m showing the pagination route here, not only because it is useful, but also to show the convert method of app. That method is used to convert values passed into get() for sanitization or validation purposes. I am using it on the paginated routes to ensure the page is an integer. In my final router, which you can see here, I am using convert to pass the post slug in my “/blog/{name}” route through sanatize_title().
Now You’re Ready To Get Started
Now that you have the basics, you have a new skill in your bag of tricks that you can build on when creating WordPress sites, WordPress-powered apps. Also, you are ready to start exploring other PHP frameworks.
In fact, I think the next step for learning to apply these skills would be to build an app using Silex, or some other micro-framework, on a different server than WordPress, and make requests to a WordPress site on another server, via the WordPress REST API to get post content. That would allow you to decouple content editing from the content display. With a good caching system on the front-end server, your use for the WordPress site itself would be quite light.
Whatever you do, I hope you enjoyed learning this, and it has broadened your horizons as a PHP developer. Personally, I’m excited about pushing my skills further and exploring more of what I can do, with and without WordPress using Symfony components and other PHP frameworks.
7 Comments