REST Services in PHP

I’m putting together a REST framework in PHP. Hopefully this suffices as an example of a handler.

I’ve not yet tested this one, but hope it works. I’m pleased with how the code is turning out. The simple GET handler is

/**
 * REST access to get a list of events for the user.
 */
$REST_HANDLERS['GET']['/event/'] = function($matches)
{
        RestTools::returnGetResult(Event::getForUser(User::$CURRENT->getId()));
};

/**
 * REST access to get a single event for this user.
 */
$REST_HANDLERS['GET']['/event/(\d+)'] = function($matches)
{
        RestTools::returnGetResult(Event::getForUserAndId(User::$CURRENT->getId(), $matches[1]));
};

Updates are also relatively easy, but have added business logic. This POST handler for invitees can be called with a list of invitees rather than having to call it multiple times. This seems unusual for REST based on what I’ve seen so far. The single call can be a single transaction and will perform far better than multiple calls.

/**
 * REST access to add an invitee or invitees to an event or modify the existing ones.
 * Accepts multiple entries in an array.
 * Event ID does not have to be set.
 */
$REST_HANDLERS['POST']['/event/(\d+)/invitees/'] = function($matches, $data)
{
        $eventId = $matches[1];

        $myInvite = EventInvitee::get($eventId, User::$CURRENT->getId());
        if($myInvite)
        {
                try
                {
                        DataModel::$dbh->beginTransaction();

                        $adminRights = $myInvite->role == 'admin';

                        if(is_array($data))
                        {
                                foreach($data as $newInviteeData)
                                {
                                        $newInviteeData->event_id = $eventId;
                                        EventInvitee::addOrUpdateInvitee($newInviteeData, $adminRights);
                                }
                                RestTools::returnPostResult("/event/$eventId/invitees/");
                        }
                        else
                        {
                                $data->event_id = $eventId;
                                EventInvitee::addOrUpdateInvitee($data, $adminRights);
                                RestTools::returnPostResult("/event/{$eventId}/invitees/{$data->user_id}");
                        }
                        DataModel::$dbh->commit();
                }
                catch (Exception $e)
                {
                        RestTools::sendError($e->getMessage);
                        DataModel::$dbh->rollback();
                }
        }
       else
        {
                // I have no invite so can't see the event even if it exists.
                RestTools::notFound($matches[0], "Event not found for that Id");
        }
};

The Json.php that handles it is quite simple. I’ve tried to make it similar in some ways to JAX-RS in the Java world. It’s not going to be as powerful, but I like the way that JAX-RS annotates methods to connect them to endpoints. Using an array of endpoint patterns seems an approximation of this.

<?php
/**
 * RESTFUL JSON in PHP
 */

include_once './lib/rest_handlers.php';
include_once './lib/user.php';

// Items with REST handlers.
include_once './lib/event.php';

if(!User::$CURRENT)
{
        header('HTTP/1.1 403 Forbidden (not logged on)');
        die();
}

$pathinfo = $_SERVER['PATH_INFO'];
if(! $pathinfo)
{
    $pathinfo = $_SERVER['ORIG_PATH_INFO'];
}

$method = $_SERVER['REQUEST_METHOD'];
$patterns = $REST_HANDLERS[$method];
$done = false;
foreach($patterns as $pattern => $handler)
{
        if(preg_match('@^' . $pattern . '$@', $pathinfo, $matches))
        {
                if($method == 'PUT' || $method == 'POST')
                {
                        $data = RestTools::readRequestObject();
                        if(!$data)
                        {
                                RestTools::sendError('Not able to read JSON data from request.');
                                die();
                        }
                        $handler($matches, $data);
                }
                else
                {
                        $handler($matches);
                }
                $done = true;
        }
}

if(!$done)
{
        header('HTTP/1.1 404 Not Found');
        ?>
        Not Found:
        <pre>
        <?= htmlspecialchars($pathinfo); ?>
        </pre>
        Possibilities for Method: <code><?= htmlspecialchars($method) ?>
        <ul>
        <?php
        foreach($patterns as $pattern => $handler)
        {
                ?><li><code><?= htmlspecialchars($pattern) ?></li><?php
        }
        ?>
        </li>
        <?php
}
?>

I’ve tested this with GET requests. I need to test with PUT and POST and DELETE, for which I now have CURL.

This technique, with a Dojo client in my case, does provide some separation of concerns. I keep my business logic in these handlers or the lower level data access classes. The disadvantage here is that a page may require multiple AJAX requests to render. I could solve this by making a REST handler to GET the data for a whole page as a big composite object. Not hard. I’ve already created such an object and tried feeding it to the Javascript code inline in a PHP page which did work quite well. This makes the page ready to go as soon as it is ¬†loaded, but slows down loading on a slow server.

The possible problem with the composite object is making Dojo save just the parts that need saving. I couild explore “rel” entries for the parts. Alternatively mutations will have to be done as specific AJAX service calls. This may end up being the way forward.

The list of invitees is a tab in a tabbed view on the page, along with other views which are lists. It makes sense to load the Event header information synchronously as part of the initial page load so the user sees it immediately, but load the tab contents asynchronously when the user first displays each tab. Dojo’s REST store can then be used to update the items in these lists.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.