Kirby REST API via HTTP in JSON format

I’ve built one recently, but unfortunately I can’t share it.

I’ve found that instead of a plugin-type solution, it’s best if you explicitly create your objects that the API gets/returns. Sure, you can just json_encode() a page object’s content, but you end up exposing things that you don’t necessarily want exposed.

(See below heading “Another Option” for a really simple approach.)

You could use template files to output JSON, but you’re much better off using Kirby’s router, much more flexible and powerful and much easier to change in the future. And a JSON response isn’t really a template or a view anyway; it’s data.

And with routes, you can apply route filters, like ensuring a user is logged in or has a certain role.

I’ll roughly describe my approach here, these code samples are untested but should get you going. Note that I’ve taken mine much farther, using Composer libraries and my own classes for handling the routes.

You’ll probably have to use [Field]->toString() and [Field]->value() and [Field]->int() when serializing your objects, since Kirby utilizes built-in __toString() for output from templates and snippets.

1. Create a plugin for your routes

// site/plugins/api.php

$prefix = "api/v1";

kirby()->routes([
    
    # GET events
    [
        'method' => 'GET',
        'pattern' => "{$prefix}/events",
        'action' => function() {
            // I like to use my own controllers instead of doing the work here
            // return (new MyApp\APIController)->getEvents()

            // get the Event pages
            $collection = pages()->find('events')->children()->visible();

            // You could just serialize the collection, but
            // you may not want to expose all that data
            // return response::json($collection->toJson());

            // Transform them for API delivery
            $data = $collection->map(function ($event) {
                // `serialize()` is not built in, see page models below
                return $event->serialize();
            });

            return response::json($data);
        }
    ]

]);

2. Create a page model

This is where you define serialize(), shown above.

You could create your data objects right in the api.php plugin, but I found it’s better to encapsulate that in the Page itself. Let each Page object manage its own serialization for API delivery.

I actually create a model for every page type in my site, each extending my own BasePage class, where a default serialize() function is included, which just spits out url, title, text, and some other basic fields. This way I’m not forced to define serialize() on every single Page type.

// site/models/event.php

class EventPage extends Page
{
    public function serialize()
    {
        return [
            'uid'     => $this->uid(),
            'url'     => $this->url(),
            'title'   => $this->title()->html()->toString(),
            'text'    => $this->text()->kirbytext(),
            'special' => $this->someSpecialFunction(),
        ];
    }
    public function someSpecialFunction()
    {
        return (int) ($this->someField()->int() + $this->someOtherField()->int());
    }
}

This is just a basic intro… Again, I’ve found it’s best to take the time to explicitly build up your own API. I considered writing a distributable plugin for getting a basic API, but the reality is that every API has different needs.

WordPress’s database structure is standardized, even across content types (post_types), so a generic, one-size fits all REST API makes sense. But in Kirby, we store all sorts of content in so-called “Pages”.

I do think there’s room for an API plugin, but it’d require so many ways of customizing, so many hooks into the system, that I think in the end it’s less work to just build it yourself.

Another option

If you’re just looking to expose all page’s public data via an api, it may make more sense to simply append json to the existing routes. For example, you could:

/pages/page-1/json
/pages/page-2/json
/events/2016/json

You’d still have to create a router and route each URI, so you’d essentially be recreating most of kirby’s default/built-in routing.

But if you did this (or something similar):

/pages/page-1/?format=json
/events/2016/?format=json

You could write a plugin that checks for get('format') and stops template execution, returning JSON instead.

But often when a plugin is called, kirby’s router isn’t done; so your plugin doesn’t know which page is being requested yet. You’re probably better off using page controllers.

// site/controllers/events.php

return function ($site, $pages, $page)
{
    if (get('format') == 'json')
    {
        $data = [
            'uid'   => $page->uid(),
            'title' => $page->title()->toString(),
            'text'  => (string) $page->kirbytext(),
        ];

        die(response::json($data, 200));
    }
}

Hope this helps!

8 Likes