Using WordPress with Lumen

Lumen, WordPress, Python, and Django. Oh My!

Categories: Development, WordPress

At work, I needed a fast and flexible way to present structured data that can be consumed by Python. This was done using WordPress leveraging Custom Post Types and Advanced Custom Fields.

Simply put. WordPress is the admin interface.

To present the data I chose to use Laravel 5.0 and It took about an hour to get things settled in.

I originally used the article from Junior Grossi

I have since learned of Lumen.  Laravels answer to Slim 3 or Silex, a micro-framework built with Laravels familiar components.

Install

To get things started lets Install Lumen download WordPress.

composer create-project laravel/lumen --prefer-dist

I placed the WordPress folder inside the application directory renaming it to admin. For security, I am not running the API and Admin in the same domain. I personally think its better to prevent the exposure of WordPress to the public as it tends to have its security flaws and may give a poor impression. But that’s just my opinion on the matter.

applicationadmin.com  /lumen/admin/

applicationapi.com /lumen/public/

I am expecting if you are reading this that you can negotiate the URL access.

Disable the WordPress front-end

Since we only need the Admin portion of WordPress we can cut off the front-end

Open /lumen/admin/index.php and redirect all requests to wp-admin

header("Location: ./wp-admin");
exit();

Fooling WordPress

Now that things are almost ready to fire up, we still need to make a theme.

Keeping this part minimal you will still need an index.php and style.css file.

Some extra goodness here comes in the form of functions.php and any of the other regular WordPress configurations you might want to do like configuring image sizes, cleaning up the sidebars and so on. Its also good to note that functions in your functions.php are also available to your Lumen app. Although I would recommend avoiding that.

Here is the theme that I used.

Making Lumen Aware of WordPress

This part to me is simply magic.

Opening /lumen/public/index.php add

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

It’s important to load this before Lumen ties its Boots.

Querying Posts

At a basic level, we can use the regular WP_Query from WordPress. Note the in front of WP_Query!

$query = new WP_Query($args);
$posts = $query->get_posts();

How I set up the API

The rest of this will be a high-level overview of how I went about configuring the API.

Use  Route Prefixing as a good practice app.com/v1/

I also set up and configured Memcached as standard WordPress database queries were DOG SLOW. This might possibly be a side effect of skipping all of WordPresses built-in optimizations. In my tests, I was running about 75 RPM as measured by New Relic my average request time was 1300ms. Not so Hot.

I wrapped each query with some simple logic.

if (Cache::has($cacheKey)):
    $object = Cache::get($cacheKey);
else:
    $query = new WP_Query($queryString);
    $posts = $query->get_posts();
    Cache::put($cacheKey, $posts, Carbon::now()->addMinutes(240));
endif;

This dropped my response time down to 85ms, now that was acceptable.

A pro-tip, Since working with an API in development means you will need to have access to updated date, you will need a way to clear the cache.

In my routes I added a purge end point, using the $cacheKey allowed me to clear the cache for say app.com/store/one while keeping app.com/store/two cached. The purge route will also return the update data making it effectively up to date on every call.

Cache::forget($cacheKey);

All of my queries and data points in my API were effectively the same. So I created a helper to pass WP_Query the $queryString

Setting up WordPress and the Data structures

Each API endpoint is a custom posy type accessed by a unique slug.

The slug would be the WordPress slug for the page/post/custom post

Recommended Plugins

I map Custom Post Types to main API endpoints app.com/v1/{custom-post-type} each page slug would be the end resource app.com/v1/{custom-post-type}/{slug}

Remember the purge call? app.com/v1/{custom-post-type}/{slug}/purge

Since I didn’t want to expose WordPress as the source, I used AWS to fire off uploaded media to my S3 bucket. Force Regenerate Thumbnails is also a good call for any afterthought image sizes. Your updated images will also be fired back to S3.

I set up my data structures using Advanced Custom Fields Pro, for the most part, this works flawlessly.

Getting ACF data from a Custom Post Type

$queryString = array(
    'post_type' => 'type-name',
    'name' => $slug,
    'posts_per_page' => 1,
    'orderby' => 'post_title',
);

$query = new WP_Query($queryString);
$posts = $query->get_posts();

$object['total'] = count($posts);
$object['status'] = 200;
$object['age'] = date("M d Y, H:i:s");

foreach ($posts as $post):
    $single_acf = get_fields($post->ID);

    if(empty($single_acf))
        $single_acf = null;

    if(empty($queryString['name']))
        $path = Request::url().'/'.$post->post_name;
    else
        $path = Request::url();

    $object['results'][] = [
        'created'=>$post->post_date,
        'modified'=>$post->post_modified,
        'path'=>$path,
//      'meta'=>$post,
        'data'=>$single_acf
    ];
endforeach;

I set my cache keys using a combination of the Post Type and the slug to keep each unique call catchable.

$cacheKey = 'post-type_'.$slug;

Sample Controller using a Helper to manage WordPress calls

This simple controller would be called using app.com/v1/one/

if( $slug != null ):
                        $cacheKey = 'one_'.$slug;
                        $queryString = array(
                'name' => $slug,
                'posts_per_page' => 1,
                'orderby' => 'post_title',
            );
        else:
            $cacheKey = 'one_all';
            $queryString = array(
                'post_type' => 'one',
                'posts_per_page' => 20,
                'orderby' => 'post_title',
            );
        endif;

        return AppHelpers::handleWordPressQuery($cacheKey, $queryString);
    }

    public function purgeOne($slug = null)
    {
        AppHelpers::handleCachePurge('one_'.$slug);
        return $this->slide($slug);
    }
}

handleWordPressQuery and handleCachePurge

get_posts();

            $object['total'] = count($posts);
            $object['status'] = 200;
            $object['age'] = date("M d Y, H:i:s");

            foreach ($posts as $post):
                if(strpos($cacheKey,'_all') !== false)
                    $single_acf = null;
                else
                    $single_acf = get_fields($post->ID);

                $category = get_the_category($post->ID);

                if(!empty($category))
                    $category = $category[0]->slug;
                else
                    $category = null;

                if(empty($single_acf))
                    $single_acf = null;

                if(empty($queryString['name']))
                    $path = Request::url().'/'.$post->post_name;
                else
                    $path = Request::url();

                $object['results'][] = [
                    'created'=>$post->post_date,
                    'modified'=>$post->post_modified,
                    'path'=>$path,
                    'template'=>$category,
                    'data'=>$single_acf
                ];
            endforeach;

            Cache::put($cacheKey, $object, Carbon::now()->addMinutes(240));
        endif;

        return $object;
    }

    static function handleCachePurge($cacheKey)
    {
        Cache::forget($cacheKey);
    }
}

Final Notes

One thing that I forgot to mention was that your Database settings all go through WordPress.  Any Lumen specific configuration was done using the .env files.

It will probably take you longer to configure your post types, and custom filed structures that it will set up the API. Working in Django has shown me some of the benefits of Python, and the Django framework it’s self. But even its Admin interface doesn’t even come close to what WordPress can do. Lumen with WordPress allows me to quickly update and add data that is consumed via CURL in Python making for a fast and efficient relationship.

How I monitor my VPS for free! I have to manage 35 sites so any leg up is an advantage. We have some simple template tags that we use to consume, parse, and place the date into the context of the template. Effectively allowing us to RESTfully create marketing content in a fraction of the time.


Adam Patterson

Adam Patterson

User Interface Designer & Developer with a background in UX. I have spent 5 years as a professionally certified bicycle mechanic and ride year-round.

I am a husband and father of two, I enjoy photography, music, movies, coffee, and good food.

You can find me on Twitter, Instagram, and YouTube!