Adding Functionality to the Concrete CMS Core REST API

In Concrete CMS 8.5.0 we introduced a fully functional REST API into the core, with full support for different authorization flows, scopes and more. The actual functionality of the API, however, remains mostly unbuilt as of June 2019. Let’s fix that! Here’s how to contribute to the Concrete REST API.

Prerequisites

You must be running Concrete 8.5.2 in order to have access to all classes described in this document. Additionally, you should have a thorough knowledge of Concrete's Routing and Controller system.

Purpose of This Document

This document's purpose is twofold: for developers who want to contribute to the built-in Concrete REST API (making it more useful, adding more methods for retrieving data about objects native to Concrete, etc...) this should serve as a good foundation before you start working on your own specific pull requests. Additionally, it will be helpful to any developers who want to use the built-in REST API framework for adding their own APIs to packages. If the latter is the case, make sure to read the documentation on Including Routes in Packages first.

Choose HTTP Verbs Carefully

Concrete’s REST API should take full advantage of HTTP verbs. In general, we use the following:

  • Read operations are performed with HTTP GET.
  • Creating new objects are performed with HTTP POST
  • Updating all of an existing object are performed with HTTP PUT
  • Updating part of an existing object is performed with HTTP PATCH
  • Deleting an object is performed with HTTP DELETE

Add a new Route Group

If the API method you’re adding belongs to a group of functionality that isn’t represented in the API yet, you’ll need to add a new group within the ApiRouteList class. Add that new group:

$api->routes('api/page.php’);

Then, create the new group at concrete/routes/api/page.php.

Register the API Route(s)

Let’s create a method for reading page data.

$router->get('/file/{fID}', '\Concrete\Core\Page\Api\PagesController::read')
->setRequirement(‘cID’ ,'[0-9]+')
->setScopes(‘pages:read');

Create a Database Migration that adds the relevant scope.

Since the pages:read scope is new, you’ll need to create a new database migration in Concrete\Core\Updater\Migrations\Migration that runs the following code:

protected function addScope($scope)
{
    $existingScope = $this->connection->fetchColumn('select identifier from OAuth2Scope where identifier = ?', [
        $scope
    ]);
    if (!$existingScope) {
        $this->connection->insert('OAuth2Scope', ['identifier' => $scope, 'description' => '']);
    }
}
public function upgradeDatabase()
{
    // add the new scopes.
    $this->addScope(‘pages:read');
}

You’ll need to make sure that this migration is run by setting it as the most recent db_version in concrete/config/concrete.php, the version_db key.

Create a Controller

The Concrete\Core\Page\Api\PagesController class doesn’t exist, so let’s make it.

<?php
namespace Concrete\Core\Page\Api;
use Concrete\Core\Api\ApiController;
use Concrete\Core\Application\Application;
use Concrete\Core\Page\Page;
use Concrete\Core\Page\PageTransformer;
use Concrete\Core\Http\Request;
use Concrete\Core\Permission\Checker;

class PagesController extends ApiController
{

    /**
     * @var Application
     */
    protected $app;

    /**
     * @var Request
     */
    protected $request;

    public function __construct(Application $app, Request $request)
    {
        $this->app = $app;
        $this->request = $request;
    }

    /**
     * Return detailed information about a page.
     * 
     * @param $cID
     * 
     * @return \League\Fractal\Resource\Item|\Symfony\Component\HttpFoundation\JsonResponse
     */
    public function read($cID)
    {
        $cID = (int) $cID;
        $page = Page::getByID($cID);
        if (!$page || $page->isError()) {
            return $this->error(t('Page not found.'), 404);
        } else {
            $permissions = new Checker($file);
            if (!$permissions->canViewPageVersions()) {
                return $this->error(t('You do not have access to read properties about this file.'), 401);
            }
        }

        return $this->transform($page, new PageTransformer());
    }
}

There’s a lot happening here, but it basically be followed like this:

  1. We run the read() method with the passed cID.
  2. We retrieve the page object and check that it’s valid. If it’s not, we return an error, using the error() method found in the core ApiController. The ApiController class is meant to be lightweight and provide a couple helpful methods for dealing with standard types in an API (like an error object, and a Fractal response.)
  3. If the page object is valid, we check its permissions.
  4. If we don’t have access to view the page’s versions (which is a greater permission level than simply viewing the page), we return an error. Otherwise, we transform the page using the PageTransformer class.

Supply the Transformer

Now you’ll need to create a Concrete\Core\Pages\Api\PageTransformer Fractal transformer. That’s the class actually responsible for returning useful JSON data in the API. Let’s build our transformer:

<?php
namespace Concrete\Core\Page;

use League\Fractal\TransformerAbstract;

class PageTransformer extends TransformerAbstract
{
    /**
     * Basic transforming of a page into an array
     *
     * @param Page $page
     * @return array
     */
    public function transform(Page $page)
    {
        $data = [
            'cID' => $page->getCollectionID(),
            'name' => $page->getCollectionName(),           
            // Add more here !!
        ];
        return $data;
    }
}

More to Do!

Those are the basics! In many ways that’s where the real work begins – the transformer. That’s where we have to determine the valuable information to return, and the way in which to structure it. Since it’s an API, we don’t want to change that once published. So let’s get to building!