Setting up a Laravel API with a Wordpress backend

Recently I was tasked with building an API with a Wordpress backend, to which some would say: just use the WP-API plugin. Now, that’s perfectly reasonable, but what if you want to use custom post types, and not use the JSON structure WP-API has decided upon? And what if you want better performance? My answer: Laravel + Wordpress.

If you’ve already tried to make one of these solutions, you’ll probably know that it’s not as easy as it sounds. Heck, that might even be why you’re here!

My solution is based on an environment where you have no say as to where your virtual host is directed (Laravel’s public-folder being the usual suspect), and using Laravel 5.1 (LTS), as this is the lowest common denominator.

Folder structure

I place Wordpress in the base directory of my site, and Laravel in a subfolder called “api”, leaving me with a structure like this:

    /
        - api/
            # Laravel files
            - app/
            - bootstrap/
            - config/
            ...
        # Wordpress files
        - wp-admin/
        - wp-content/
        - wp-includes/
        ...

Now that the files have been unpacked and structured, we’re off to code country!

Code

Laravel

There are basically three things you need to change to make Laravel work:

  • The database config
  • Add a base .htaccess file
  • The primary route group
  • The public index.php file

Database config
The database config is pretty much a given. You need to set up your .env-file with the correct database connection information, but what probably forget is that Wordpress operates with a database table name prefix (usually “wp_”), so you either need to hard-code that in the database config file (api/config/database.php), or change the file to accept the prefix as a environment variable, like I did:

'connections' => [
        'sqlite' => [
            'driver'   => 'sqlite',
            'database' => storage_path('database.sqlite'),
            'prefix'   => '',
        ],

        'mysql' => [
            'driver'    => 'mysql',
            'host'      => env('DB_HOST', 'localhost'),
            'database'  => env('DB_DATABASE', 'forge'),
            'username'  => env('DB_USERNAME', 'forge'),
            'password'  => env('DB_PASSWORD', ''),
            'charset'   => 'utf8',
            'collation' => 'utf8_unicode_ci',
            'prefix'    => env('DB_PREFIX', ''),
            'strict'    => false,
        ],

        ...

Now I can set the DB_PREFIX variable in my .env-file

.htaccess
The second thing you need to do, is add a base .htaccess file to the Laravel installation. You need to do this because Laravel is in a subdirectory, and needs all requests sent to the public folder. In your “api”-folder, add a .htaccess file with the following content:

<IfModule mod_rewrite.c>
    RewriteEngine On

    RewriteRule ^(.*)$ public/$1 [L]
</IfModule>

Route group
The third thing you need to make Laravel work is add an encompassing route group, that informs Laravel that it’s operating in a subdirectory. To do that, just go to the route file (api/app/Http/routes.php) and wrap all of your routes in a route group with the prefix “api” - like so:

Route::group(['prefix' => 'api'], function() {
    Route::get('/', function () {
        return view('welcome');
    });
});

Include Wordpress
The last thing you need, is to include Wordpress in Laravel’s public index.php file (api/public/index.php). To do this, just add the following lines at the top of the file:

define('WP_USE_THEMES', false);
require __DIR__.'/../../wp-blog-header.php';

That’s basically what’s needed for Laravel to work in a Wordpress subdirectory.

Wordpress

Besides setting up wordpress to work with your database, you need to add some code. I know, it sucks. But it’s only one thing, well, one theme. In you theme directory (wp-content/themes) add a folder. I’ve called mine “api-theme”. Now you just to add three small files:

  • functions.php
  • index.php
  • style.css

These files are needed for Wordpress to understand that this is a theme.

style.css
This is just basic setup for a theme, nothing fancy here.

/*
Theme Name: API Theme
Author: CMAndersen
Author URI: http://cmandersen.com
Version: 1.0
*/

index.php
This code just redirects the user to the backend, since we don’t need a frontend for our API.

<?php
// Redirect the user to the admin panel
header('Location: ' . admin_url());
die;

functions.php
This file is more serious. I spent hours figuring this one out.
Since Wordpress is our default access to the site, it will add a 404 header when we access the API. Not only that, but Laravel will crash from the added headers.

<?php

// Remove the headers, since Laravel sets it's own headers, and 
// Laravel is more important for us.
add_filter('wp_headers', 'api_theme_header_removal');
function api_theme_header_removal($headers) {
    return [];
}

Now you can access both the Wordpress and the Laravel installation.

Of course you need to set up Laravel to actually be able to read the Wordpress data structure. I’ll add a post at a later time, explaining how to do that, but until then I’ll let you have a look at the model trait I’ve been working on to translate the Wordpress data structure into a more standard output:

<?php

namespace App\Traits;

trait Wordpress {
    /**
     * Translate the ID attribute to lowercase
     * 
     * @return mixed
     */
    public function getIdAttribute() {
        return $this->attributes['ID'];
    }

    /**
     * Transform the post_title attribute
     * 
     * @return mixed
     */
    public function getTitleAttribute() {
        return $this->attributes['post_title'];
    }

    /**
     * Tranformt eh post_content attribute
     * 
     * @return mixed
     */
    public function getContentAttribute() {
        return $this->attributes['post_content'];
    }

    /**
     * Transform the post_date_gmt attribute
     * 
     * @return mixed
     */
    public function getCreatedAtAttribute() {
        return $this->attributes['post_date_gmt'];
    }

    /**
     * Transform the post_modified_gmt attribute
     * 
     * @return mixed
     */
    public function getUpdatedAtAttribute() {
        return $this->attributes['post_modified_gmt'];
    }

    /**
     * Override the Model method to automatically append our transformed attributes
     * 
     * @return mixed
     */
    protected function getArrayableAppends()
    {
        $appends = array_merge([
            'id',
            'title',
            'content',
            'created_at',
            'updated_at'
        ], $this->appends);

        return $this->getArrayableItems(
            array_combine($appends, $appends)
        );
    }

    /**
     * Override the Model method to apply some default scopes
     * 
     * @param $builder
     * @return mixed
     */
    public function applyGlobalScopes($builder) {
        $builder = parent::applyGlobalScopes($builder); // TODO: Change the autogenerated stub

        $builder
            ->where('post_type', $this->post_type)
            ->where('post_status', 'publish');

        return $builder;
    }

    /**
     * Override the Model method to supply the Wordpress ID
     * 
     * @return string
     */
    public function getKeyName() {
        return $this->primaryKey ? $this->primaryKey : 'ID';
    }
    
    public function getContentAttribute() {
        return $this->attributes['post_content'];
    }

    public function getCreatedAtAttribute() {
        return $this->attributes['post_date_gmt'];
    }

    public function getUpdatedAtAttribute() {
        return $this->attributes['post_modified_gmt'];
    }

    protected function getArrayableAppends()
    {
        $appends = array_merge([
            'id',
            'title',
            'content',
            'created_at',
            'updated_at'
        ], $this->appends);

        return $this->getArrayableItems(
            array_combine($appends, $appends)
        );
    }

    public function applyGlobalScopes($builder) {
        $builder = parent::applyGlobalScopes($builder);

        $builder
            ->where('post_type', $this->post_type)
            ->where('post_status', 'publish');

        return $builder;
    }

    public function getKeyName() {
        return $this->primaryKey ? $this->primaryKey : 'ID';
    }
}