This post is the last part in a series on how to use WordPress as API for Laravel. In previous posts, we setup WordPress such that it can act as an API and we setup the authentication. In this last post, we actually retrieve data from the WordPress API for use in a Laravel application.
We use this setup for a project that was originally built in WordPress. The project involves our personal cycling blog. After writing many articles for several years, it was time for a technical upgrade, a redesign and some new features. Since Laravel is nowadays our framework of choice for application development, we chose to build the new version of the project in Laravel, but we keep the content management in WordPress. Especially since the release of WordPress 5.0, writing and maintaining blog content this way is very easy.
Retrieving posts
With the WordPress API ready and authentication in place, we now like to retrieve posts, pages and our custom post type ‘mountains’ via the WordPress API. We only describe the procedure for posts, because it’s the same for pages and custom post types.
Writing a request wrapper
We use the GuzzleHttp library to easily perform requests from the Laravel app to the WordPress API.
First, we write a wrapper for all our calls to WordPress. The wrapper method accepts a url and an object type (e.g. page, post or a custom post type). This object type is used to format the result we get from the WordPress API. When we retrieve a post from the API we get a deeply nested array with lots of details related to this post. We only need a small fraction of this data and we don’t want too much dependence on the format of the response throughout the Laravel codebase, so we reformat this response in a dedicated library. For example, if we use the meta description of a post in a blade view, we don’t want to use $blog[‘meta’][‘_yoast_wpseo_metadesc’][0], but $blog->meta_description.
Our request wrapper is using the following code (explanation below the code).
<?php namespace App\Libraries;
use GuzzleHttp\Client; use DB; use Cache; class RequestLibrary { public function __construct() { $this->token = DB::table('WpAuth')->first()->access_token; $this->client = new Client(); $this->params = [ 'headers' => [ 'Authorization' => 'Bearer '.$this->token, 'Accept' => 'application/json' ] ]; } public function getData($url, $type = null) { $url = config('services.wp_api.url').'/wp-json/wp/v2/'.$url; $cacheKey = md5($url); return Cache::remember($cacheKey, 86400, function() use ($url, $type) { try { $response = $this->client->request('GET', $url, $this->params); $body = $response->getBody()->getContents(); $data = [ 'body' => json_decode($body, true), 'headers' => $response->getHeaders() ]; if ($type == 'post') { return (new BlogLibrary)->reformatBlogList($data); } elseif ($type == 'page') { return (new PageLibrary)->reformatPage($data); } return $data; } catch (\Exception $e) { abort(500, $e->getMessage()); } }); } }
- In the constructor, we retrieve the access token. We store it in the database connected to our Laravel app. Alternatively, the .env can be used for this. Next, we create the params array that we need for our request.
- The getData method accepts the API endpoint, and optionally the type of object we retrieve. The API base url is added to the endpoint in this method, so you only have to pass the variable part to the method. Based on the object type, we format the response for use in the Laravel app (see below). If no type is specified, just return the full response.
- The response body contains most of the relevant content of a post, but the headers may contain some information as well. For some calls, there is a X-WP-Total header that contains the total number of posts when you retrieve a filtered set. This can be useful for pagination.
- In our case, the data in WordPress is not changing too often. Therefore, to improve the performance, we cache the response of the API using Laravel’s built-in cache functionality. We define a cache key based on the url we’re calling and we keep the response in the cache for 24 hours (86400 seconds). Laravel’s Cache::remember method checks whether there’s already a cached response for this cache key. If so, the cached data is returned. If not, the data is retrieved from the WordPress API and also stored in cache. More information on setting up caching for Laravel can be found in the documentation.
Formatting the response
The response we get from the WordPress API is formatted for further use in our Laravel app. Therefore, we send the blog post response to the reformatBlogList method of our BlogLibrary. This library has the following code:
<?php namespace App\Libraries;
use Jenssegers\Date\Date; use stdClass; class BlogLibrary { /** * Reformat the content of an array of blog posts * @param array $rawBlogs * @return array */ public function reformatBlogList($rawBlogs) { $blogs = []; foreach ($rawBlogs['body'] as $rawBlog) { $blogs[] = $this->reformatSingleBlog($rawBlog); } return [ 'data' => $blogs, 'meta' => ['number_of_posts' => $rawBlogs['headers']['X-WP-Total'][0]] ]; } /** * Reformat the contents of a single blog post * @param array $rawBlog * @return obj */ public function reformatSingleBlog($rawBlog) { $blog = new stdClass; $blog->id = $rawBlog['id']; $blog->title = $rawBlog['title']['rendered']; $blog->slug = $rawBlog['slug']; $blog->date = Date::createFromFormat('Y-m-d\TH:i:s', $rawBlog['date']); $blog->excerpt = $rawBlog['excerpt']['rendered']; $blog->content = (new UtilLibrary)->reformatContent($rawBlog['content']['rendered']); //seo_title if (!empty($rawBlog['meta']['_yoast_wpseo_title'])) { $blog->seo_title = str_replace([' %%sep%%', ' %%sitename%%'], '', $rawBlog['meta']['_yoast_wpseo_title'][0]); } else { $blog->seo_title = $rawBlog['title']['rendered']; } $blog->meta_description = $rawBlog['meta']['_yoast_wpseo_metadesc'][0] ?? null; $blog->author = $rawBlog['_embedded']['author'][0]; $blog->featured_media = (new ImageLibrary)->reformatFeaturedImage($rawBlog['_embedded']['wp:featuredmedia'][0]); //tags en categorieen nog ophalen $categories = []; $tags = []; foreach ($rawBlog['_embedded']['wp:term'] as $items) { foreach ($items as $item) { if ($item['taxonomy'] =='category') { $categories[] = $item; } elseif ($item['taxonomy'] == 'post_tag') { $tags[] = $item; } } } $blog->categories = $categories; $blog->tags = $tags; //Comments $comments = []; if (isset($rawBlog['_embedded']['replies'][0])) { $comments = (new CommentLibrary;)->getComments($rawBlog['_embedded']['replies'][0]); } $blog->comments = $comments; return $blog; } }
- We get a list of blog posts from the API and loop through them one by one. The meta data (originating from the headers) of the response contains the total number of posts.
- Foreach of the posts, we format the response. We only add the properties that we need in our Laravel app, but there is a lot more, so adjust this method as you like.
- We make some references to other libraries that also contain formatting methods. For example, the featured image of a post is again a deeply nested array from which we only need some attributes. The same is the case for the comments of a post.
- Tags and categories are both taxonomies and they are grouped together in the [‘_embedded][‘wp:term’] part of the response. We need them separately, so we split them.
Different calls for blog posts
Now we have everything in place to format our posts once we retrieved them from the WordPress API. Next, we need to actually call the API endpoints. The WP REST API handbook contains a full list of endpoints, but we discuss some useful endpoints here.
GET /posts
Get all posts
GET /posts?categories=3
Get all posts from the category with id 3. You can find the category id’s in the WordPress admin.
GET /posts?categories_exclude=58
Get all posts except the ones from the category with id 58.
GET posts?tags=24
Get all posts that have a tag with id 24
GET posts/10
This endpoint retrieves a single blog post by the id of the post. However, in many cases the id of the post is unknown and you only have the slug. Luckily, you can use the following endpoint to retrieve the post based on its slug:
GET posts?slug=post-slug
The downside of getting a single post using this call is that the response has an additional nesting layer. Even though slugs should be unique, you have to explicitly take the first element of the response.
For other entities (e.g. pages and custom post types) you can use similar endpoints. Just replace ‘posts’ by ‘pages’ or the api slug of your custom post type, which can be found in the settings of the plugin.
For all endpoints you can add _embed (e.g. posts?_embed). By adding this parameter, you get a lot of related entities, like categories, tags, authors and featured images, so you don’t have to retrieve them by separate api calls.
Pagination
By default, the API response contains the first 10 results (blog posts). This can be customized by adding the per_page and page attributes to the endpoint. For example, to get posts 20 to 40, the following endpoint can be used:
GET /posts?per_page=20&page=2
The total number of posts can be found in the header of the response as described above. Next, you can build your own pagination, or use Laravel’s built-in pagination features.
Change the maximum number of posts
For some reason, the maximum value for the per_page parameter is 100. In most cases this is fine, but in specific cases you may need more. For our cycling blog, we want to display a list of all mountains of a certain region, even when there are more than 100 mountains. This can be achieved by introducing an alternative parameter, which we call ‘posts_per_page’. In the functions.php file of the blank theme of the WordPress app we add the following code:
add_action( 'rest_mountain_query', 'mountain_override_per_page' );
function mountain_override_per_page( $params ) { if ( isset( $params ) && isset( $params[ 'posts_per_page' ] ) && isset($_GET['posts_per_page']) ) { $params[ 'posts_per_page' ] = $_GET['posts_per_page']; } return $params; }
Now we are able to call our mountain-endpoints and retrieve more than 100 mountains at once.
Retrieving post meta
When you retrieve a post, page or custom post type via the WordPress API, the response only contains fields that are considered safe. All other attributes contain potentially sensitive information, so they are excluded from the API response by default. Post meta data, for example the SEO title and meta description generated by the popular Yoast plugin, are therefore not returned by the API.
To retrieve those fields via the WordPress API, you have to add them to the response. This is achieved by adding some code to the functions.php file of the blank theme. For example, for Yoast’s title and meta description you have to add the following lines:
register_meta(‘post’, ‘_yoast_wpseo_metadesc’, [‘show_in_rest’ => true]);
register_meta(‘post’, ‘_yoast_wpseo_title’, [‘show_in_rest’ => true]);
For other fields, just look up the key in the wp_postmeta table of your WorpPress database. Now, when you retrieve an entity via the API, the response has an additional attribute ‘meta’ that contains the relevant data.
Internal links
We have our WordPress application at api.domainname.com, while our main Laravel website is at domainname.com.
When we add a new post to this WordPress app and link to other posts, the href’s of these links start with https://api.domainname.com (full url’s are stored in the content blocks). To display the post at domainname.com without broken links, we modify the API response in Laravel.
In the ‘formatting the response’ section we had the following line of code for the content of a post:
$blog->content = (new UtilLibrary)->reformatContent($rawBlog[‘content’][‘rendered’]);
This reformat content method has the following code:
public function reformatContent($content) { return str_replace(config('services.wp_api.url'), '', $content); }
We defined the WordPress API as an external service in Laravel and replaced this part of the full url by an empty string. The resulting post content now contains relative urls starting with a /
Images
The images uploaded via WordPress are also available at api.domainname.com. For a new website you can simply reference them by this url, but this may not be the desired approach.
In our case, the WordPress website was originally build at domainname.com and the featured images and content blocks of the posts contained references to images like domainname.com/image.jpg. Meanwhile, we moved the WordPress website to api.domainname.com, so these references were incorrect. Additionally, Google indexed the images without ‘admin’ in the urls, so we didn’t want to change the image urls in existing posts.
We chose the following approach:
First, we converted the full image urls to relative urls. Next, we added the following route to the routes/web.php file, where the wordpress/… part corresponds to the format of the image urls in WordPress:
Route::get(‘wordpress/wp-content/uploads/{year}/{month}/{filename}’, ‘UploadsController@showWpImage’);
Last, we created the UploadsController with the following method:
public function showWpImage($year, $month, $filename) { $slug = '/wordpress/wp-content/uploads/'.$year.'/'.$month.'/'.$filename; $imageUrl = config('services.wp_api.url').$slug; $file = file_get_contents($imageUrl); $mimeType = $this->getMimeType($file); // this method is beyond the scope of this post return response($file, 200) ->header('Content-Type', $mimeType) ->header('Cache-Control', 'public') ->header('expires', \Carbon\Carbon::now()->addDays(10)); }
Now images are displayed correctly in the Laravel website without modifying the content of the posts in WordPress.
Submitting comments
An important part of a blog is the possibility to add comments to a post. Comments can be added and retrieved using the WordPress REST API.
Getting comments for a certain post is easy: when you get a post via the API, the comments are automatically included in the response.
To add a comment to a post, create a form in your Laravel app for each post where users can leave their name, email address and comment.
Next, perform a POST request to the /comments endpoint of the WordPress API. Send at least the following data with the request:
- post: the id of the post the comment refers to.
- author_name: the name of the author (of the comment)
- author_email: the email address of the author (of the comment)
- content: the actual comment
- date: the date at which the comment is submitted.
Filtering using advanced custom fields
In WordPress ‘custom post types’ and ‘advanced custom fields’ are commonly used plugins that add a lot of flexibility to WordPress.
In our WordPress website, we have a custom post type ‘mountains’ that has some custom fields like ‘region’, ‘country’ and ‘(mountain) length’. In our new Laravel website, we like to retrieve all mountains of certain region.
To be able to make this call, we first have to make it available via functions.php of the blank theme in WordPress by adding two filters. Add the following code:
add_filter( 'rest_query_vars', function ( $valid_vars ) { return array_merge( $valid_vars, array( 'region', 'meta_query' ) ); } ); add_filter( 'rest_mountain_query', function( $args, $request ) { $region = $request->get_param( 'region' ); if ( ! empty( $region ) ) { $args['meta_query'] = array( array( 'key' => 'region', 'value' => esc_sql($region), 'compare' => '=', ) ); } return $args; }, 10, 2 );
The first filter makes ‘region’ available as an attribute for filtering. The next filter describes what to do with this filter. The key ‘rest_mountain_query’ is important, because it determines to which post types the filter should be applied. In this case, the post type is ‘mountain’. Now, the WordPress API is ready.
In Laravel, we can add ®ion=regionname to our calls to filter on region.
Final remarks
It has been an interesting experience to rebuild our personal cycling blog in Laravel using the original WordPress website as an API. In the end I’m happy to have taken this approach. Although there were quite some challenges as written in this series, I believe WordPress and Laravel are a great combination for projects where content management is important (like a blog), or when (a large amount of) content is already available in WordPress.
WordPress has excellent opportunities to write blog posts, handle revisions and comments and makes it easy to organize content using tags and categories. And in our case, the blog was originally built in WordPress, so we didn’t have to migrate our old posts, which was also a major advantage.
Laravel on the other hand has great flexibility to develop custom features, to easily deploy new features and to organize everything exactly the way you like. So now I am finally ready to start implementing the new features that are on my wishlist for quite some time…
To be continued!
If you want to have a look at the result, visit train-mee.nl (it’s in dutch only).
Hi guys. Excellent series. I am learning a lot!!
I am considering the possibility to use wordpress to handle a multi tenant network of websites (but I don’t know yet if I will use NUXT or Laravel).
What do I mean with multi tenant?
We want to make several blogs, each living in their domain. Each blog should have categories, post, custom landing page… But the endpoint would be the same.
I have thought about creating a custom post type called website (like you have for mountains).
What do you think? Is it possible? Thank you very much!
Hi Manu,
I don’t think a custom post type would be necessary, because WordPress already has ‘posts’.
Maybe using the ‘Advanced Custom Fields’ plugin would be useful. With this plugin, you can add an additional field ‘website’ to the posts.
When retrieving posts from the WordPress API for one of the Laravel/NUXT websites, you can use the website-field as a filter.
Thank you for the reply, Leonie.
Will you do a tutorial for user wordpress login through laravel’s guards?
Hi Brian,
We didn’t plan for this, but it’s an interesting idea, so I’ll keep it in mind!
Thanks for sharing this information.
Hello
Thank you for the tutorial, I followed the steps, but in the end you didn’t show how to render the fetched data.
If i go and try this {!!html_entity_decode($data->content)!!} there is no style and images url’s aren’t linked to the wp site.
Please how to render a post ?
Best Regards.
Thanks for sharing information. keep sharing