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:
- We run the
read()
method with the passed cID. - 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 coreApiController
. TheApiController
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.) - If the page object is valid, we check its permissions.
- 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!