Many developers today are buzzing about how they use the WordPress REST API in conjunction with a JavaScript framework. While that process does work well, I’ve been exploring how to correlate the REST API to a PHP MVC framework, specifically Laravel.
In this article, I will highlight a few different ways to integrate the WordPress REST API into Laravel.
I recently built a web application to serve helpful tips and links into my WordPress plugin Caldera Forms. Most of the content it sends exists in WordPress so I thought about creating a plugin that would run on the Caldera Forms site. While it was totally feasible, I didn’t like the scalability challenges associated with giving our WordPress site this additional role.
Instead of adding all of those extra requests to the workload of our WordPress site, I built a small Laravel app on another server that queries the WordPress REST API on the main site to get content. This is by far the best option as it gives my content one authoritative location.
If you’re unfamiliar with Laravel basics, check out the Laravel docs and consider checking out Laracasts. Also, before you read on, consider checking out my article on dependency injection containers as the second half of this article involves registering a service for Laravel’s dependency injection container.
Let’s get started.
A Few Simple Solutions
A Very Basic API Client
Eric Barnes of Laravel News published a tutorial on how to use the WordPress REST API to sync posts to the Laravel database, which is then used to display the content. You could even simplify this approach further. Instead of writing the posts to the database, you could use the cache and refresh once that gets stale.
Here is a very simple WordPress REST API client adapted from that article:
<?php namespace App; class WpApi { protected static $url = 'https://calderaforms.com/wp-json/wp/v2/'; public static function getPost(int $id ) { $url = self::$url . 'posts/' . $id; return self::getJson( $url ); } public static function getPosts( int $page ) { $url = self::$url . '/posts?per_page=' . $page; return collect( self::getJson( $url ) ); } protected static function getJson( $url) { $response = file_get_contents($url, false); return json_decode( $response ); } }
You could then use this in a client in a controller to show a post or posts, like this:
namespace App\Http\Controllers; use App\WpApi; class Posts extends Controller { public function post( $id ) { $post = WpApi::getPost( $id ); return view( 'wp.post', [ 'post' => $post ]); } public function posts( $page ) { $posts = WpApi::getPosts( $page ); return view( 'wp.posts', [ 'posts' => $posts ]); } }
This is not a great system since it turns each HTTP request into two requests. It’s useful if you have an application where one part is content managed by WordPress and the rest is not. In that specific scenario, Barnes discusses how to use an automated database sync.
While the aforementioned solutions are all viable options, there are a few others worth exploring.
Query the WordPress Database Directly
If you don’t want duplicated data in your database, and both servers could access the WordPress database, then Corcel is a good option. It is a PHP package — it doesn’t have to be used with Laravel — that creates Laravel Eloquent models from WordPress posts.
Corcel is pretty simple to setup. Just put some thought into whether you want to use the same database for both apps — Laravel or WordPress. If not, you can have two databases on the same server or on their own server. Using one server, or one database is easier than two database servers. But, two servers may be more scalable. Also, Laravel is not tightly coupled to MySQL and may be using some other database technology, making sharing a database impossible.
Once you have Corcel setup, you can make a simple Posts controller in your app and then we can refactor it to use Corcel:
<?php namespace App\Http\Controllers; class Posts extends Controller { public function post( $id ) { $post = Posts::find( $id ); return view( 'wp.post', [ 'post' => $post ]); } public function posts() { $posts = Posts::paginate(15); return view( 'wp.posts', [ 'posts' => $posts ]); } }
Caching REST API Queries
I don’t love the idea of two apps connecting to the same database. I’d rather stick to using the WordPress REST API. It keeps WordPress as an isolated provider of content. So, let’s adapt the first example to use a persistent object cache — Redis, Memcached — in Laravel.
This populates short-lived copies of the data in Laravel and the load on WordPress is minimal. Refactor the WordPress REST API client shown above to use caching. Laravel provides a simple abstraction over various caching systems via the Cache facade.
Here is the refactored API client:
<?php namespace App; use Illuminate\Support\Facades\Cache; class WpApi { protected static $url = 'https://calderaforms.com/wp-json/wp/v2/'; public static function getPost(int $id ) { $url = self::$url . 'posts/' . $id; return self::getJson( $url ); } public static function getPosts( int $page ) { $url = self::$url . '/posts?per_page=' . $page; return collect( self::getJson( $url ) ); } protected static function getJson( $url) { $cached = Cache::get( md5( $url ) ); if( empty( $cached ) ){ $response = file_get_contents($url, false); $json = json_decode( $response ); //store for 12 hours (12*60 minutes) Cache::put( md5( $url ), $json, 720 ); return $json; }else{ return $cached; } } }
Creating a Service Provider
The last solution can be used to add a WordPress-managed blog to a Laravel app. In my example, that wasn’t what I was doing: instead, I needed to serve content stored in the local database, but I wanted a way to import some of it from WordPress via the REST API.
The end of what my application displays is just a little bit of the content and a link back to the source. For that reason, while I did end up duplicating content to the WordPress database, I only stored post title, excerpt, an array of categories, an array of tags, featured image, URL, and permalink.
To make this work in a way that would allow me to use multiple routes and easily add support for other WordPress sites, I ended up creating a Model, a REST API client, and a Service Provider.
I can’t share a complete, abstract system here. But, if you’re familiar with Laravel, you can use what I have to share to put together your own system.
Here is the model, which as I explained only has the parts of the post I needed:
<?php namespace App\WPAPI; use App\Contracts\Content; class CalderaWP extends \App\Abstracts\WPAPI implements Content { protected $root = 'https://calderawp.com/wp-json'; protected $routeConfig = [ 'posts' => 'wp/v2/posts', 'cf-addons' => 'calderawp_api/v2/products/cf-addons' ]; public function getAddOns( int $page = 1 ){ return $this->get( 'posts', [ 'cf-addons' => $page ] ); } public function getAddon( int $id ) { return $this->get( 'cf-addon', [], $id ); } public function getPosts( int $page = 1, int $per_page = 50 ) { return $this->get( 'posts', [ 'page' => $page, 'per_page' => $per_page ] ); } public function getPost( int $id ) { return $this->get( 'posts', [], $id ); } }
This is a good start, but I would like to see it work with a non-persistent database — like Redis — so that it acted more like a cache in front of the WordPress REST API. That said, the model isn’t really required here. Once the REST API client is a service provider, it could be injected directly into a Laravel controller, and there would be no need for persistent database storage inside of Laravel.
With that in place, I needed a system to get post data into the Laravel database. I used two interfaces. The first was for WordPress API clients and the second for importing content. My use case would implement both, but a separate implementation of this system I’m using handles content and products differently.
Here is the first interface, for API clients:
<?php namespace App\Contracts; use App\WPAPI\RouteCollection; interface WPAPI { /** * Get the routes for this API * * @return RouteCollection */ public function getRoutes() : RouteCollection; /** * Get root URL for API * * @return string */ public function getRoot() : string ; }
And the second interface for content:
<?php namespace App\Contracts; use App\Post; interface Content { /** * Get post by WordPress ID * * @param int $wp_id WordPress post ID * * @return Post */ public function getPost( int $wp_id ) : Post; }
I implemented the first interface in an abstract class to build a generic REST API client:
<?php namespace App\Abstracts; use App\WPAPI\Route; use App\WPAPI\RouteCollection; /** * Class WPAPI * * Base WordPress REST API client * @package App\Abstracts */ abstract class WPAPI implements \App\Contracts\WPAPI { /** * Array of routes to be used * * 'name' => 'url * * @var array */ protected $routeConfig; /** * API root URL * * @var string */ protected $root; /** * Routes for this API * * @var RouteCollection */ protected $routes; public function __construct( ) { $this->routes = new RouteCollection(); $this->constructRouteCollection(); } public function getRoot() : string { $this->root; } public function getRoutes() : RouteCollection { return $this->routes; } public function get( string $route, array $params = [], int $id = 0 ){ try { $route = $this->routes->getRoute( $route ); }catch ( \Exception $e ){ return $e; } if ( 0 == $id ) { $url = $this->trailingslashit( $this->root ) . $route->url . '?' . http_build_query( $params ); } else { $url = $this->trailingslashit( $this->root ) . $this->trailingslashit( $route->url ) . "$id" . '?' . http_build_query( $params ); } return json_decode( file_get_contents( $url ) ); } protected function constructRouteCollection(){ if( is_array( $this->routeConfig ) && ! empty( $this->routeConfig ) ){ foreach ( $this->routeConfig as $name => $url ){ $route = Route::factory( $name, $url ); $this->routes->addRoute( $route ); } } } protected function trailingslashit( $string ) { return $this->untrailingslashit( $string ) . '/'; } protected function untrailingslashit( $string ) { return rtrim( $string, '/\\' ); } }
You will notice in that class, routes, are represented by a route object that is used to build a route collection. This object makes it easier to represent WordPress REST API route data. These classes are dependent on my Object library, which is on Github.
Here is the route class, which is a container for route name and URL:
<?php namespace App\WPAPI; use calderawp\object\almostStdImmutable; class Route extends almostStdImmutable { protected $name; protected $url; public static function factory( string $name, string $url ){ $obj = new static(); $obj->name = $name; $obj->url = $url; return $obj; } }
And here is the routes collection which collects route objects:
<?php namespace App\WPAPI; use calderawp\object\almostStdImmutable; class RouteCollection extends almostStdImmutable { protected $routes; /** * @param $name * * @return Route * @throws \Exception */ public function getRoute( $name ) : Route { if( array_key_exists( $name, $this->routes) ){ return $this->routes[ $name ]; } throw new \Exception( 'No matching route' ); } public function addRoute( Route $route ) :bool { $this->routes[ $route->name ] = $route; return true; } }
With all of that, I was able to extend my REST API client for an implementation specific to my site:
<?php namespace App\WPAPI; use App\Contracts\Content; class CalderaWP extends \App\Abstracts\WPAPI implements Content { protected $root = 'https://calderawp.com/wp-json'; protected $routeConfig = [ 'posts' => 'wp/v2/posts', 'cf-addons' => 'calderawp_api/v2/products/cf-addons' ]; public function getAddOns( int $page = 1 ){ return $this->get( 'posts', [ 'cf-addons' => $page ] ); } public function getAddon( int $id ) { return $this->get( 'cf-addon', [], $id ); } public function getPosts( int $page = 1, int $per_page = 50 ) { return $this->get( 'posts', [ 'page' => $page, 'per_page' => $per_page ] ); } public function getPost( int $id ) { return $this->get( 'posts', [], $id ); } }
Finally, I created and registered a service provider that can be injected into my importer controller, but could also be injected into a controller for showing content:
<?php namespace App\Providers; use App\Content\CalderaWP; use Illuminate\Support\ServiceProvider; class CalderaWPContentServiceProvider extends ServiceProvider { /** * Bootstrap the application services. * * @return void */ public function boot() { // } /** * Register the application services. * * @return void */ public function register() { $this->app->bind( \App\Contracts\Content::class, function( $app ){ return new CalderaWP( ); }); } /** * Get the services provided by the provider. * * @return array */ public function provides() { return ['App\Content\CalderaWP']; } }
Great Options In PHP
The WordPress REST API opens up a ton of opportunities for us to use WordPress in different ways. It’s also motivated me to learn many different skills. Laravel is a wonderful tool that I am really happy I’ve learned. I hope that this article will help you see options for using Laravel or other Symfony-based PHP frameworks with the WordPress REST API.
3 Comments