Advanced: Supporting the Fractal Middleware

Fractal is a PHP library for working with API output. From the website:

What is Fractal? Fractal provides a presentation and transformation layer for complex data output, the like found in RESTful APIs, and works really well with JSON. Think of this as a view layer for your JSON/YAML/etc. When building an API it is common for people to just grab stuff from the database and pass it to json_encode(). This might be passable for “trivial” APIs but if they are in use by the public, or used by mobile applications then this will quickly lead to inconsistent output.

What this means practically is that Fractal sits between your data objects and the actual library that transforms those objects into JSON. In some cases this seems like overkill, but once you have objects that relate to other objects, it’s handy to have a library that ensures the resulting output doesn’t change between versions.

Example

Here’s a practical example of how the concrete5 core uses Fractal. It’s somewhat contrived, but was meant to be, since it’s (hopefully) a fairly simple example.

When enabling the concrete5 API and going to /ccm/api/v1/system/info, you’ll get a JSON object containing properties like version, code_version, db_version and more. This is the same data about your site available to view at Dashboard > System and Settings > Environment > Environment Information. This route definition is defined in the ApiRouteList class, within a system route group:

$api->buildGroup()
    ->scope('system')
    ->routes('api/system.php');

We define the actual logic in concrete/routes/api/system.php file. It could look something like this:

use Concrete\Core\System\Info;
use Symfony\HttpFoundation\JsonResponse;
$router->get('/system/info', function() {
    $info = new Info();
    $data = [
        'version' => $info->getVersionInstalled(),
        'code_version' => $info->getCodeVersion(),
        'db_version' => $info->getDbVersion(),
        'packages' => $info->getPackages(),
        'overrides' => $info->getOverrides(),
        'cache' => $info->getCache(),
        'server' => $info->getServerSoftware(),
        'server_api' => $info->getServerAPI(),
        'php_version' => $info->getPhpVersion(),
        'php_extensions' => $info->getPhpExtensions(),
        'php_settings' => $info->getPhpSettings(),
    ];
    return new JsonResponse($data);
});

This would work, but it’s brittle; if we ever reuse this info object and want to include it in an API call, we have to manually copy this logic into that spot. Even worse, we have to make sure we keep that instance in sync with this. What we need is something that passes the Concrete\Core\System\Info object to a data transformer. Fractal does that. This is how our /system/info route definition actually looks in api/system.php.

use Concrete\Core\System\Info;
$router->get('/system/info', function() {
    return new \League\Fractal\Resource\Item(new Info(), new \Concrete\Core\System\InfoTransformer());
});

The InfoTransformer class that houses the actual JSON data object looks like this:

<?php
namespace Concrete\Core\System;

use League\Fractal\TransformerAbstract;

class InfoTransformer extends TransformerAbstract
{

    public function transform(Info $info)
    {
        return [
            'version' => $info->getVersionInstalled(),
            'code_version' => $info->getCodeVersion(),
            'db_version' => $info->getDbVersion(),
            'packages' => $info->getPackages(),
            'overrides' => $info->getOverrides(),
            'cache' => $info->getCache(),
            'server' => $info->getServerSoftware(),
            'server_api' => $info->getServerAPI(),
            'php_version' => $info->getPhpVersion(),
            'php_extensions' => $info->getPhpExtensions(),
            'php_settings' => $info->getPhpSettings(),
        ];
    }
}

But wait! I can hear you already. “How does returning an object of League\Fractal\Resource\Item (as we do from the /system/info route) translate into a valid Symfony\HttpFoundation\Response object? Through the magic of middleware. In our ApiRouteList class we add a middleware to all calls. This middleware, Concrete\Core\Http\Middleware\FractalNegotiatorMiddleware is responsible for looking at all responses from a route, and checking to see if they are a valid Fractal object. If so, they are transformed into a JSON response.

public function process(Request $request, DelegateInterface $frame)
{
    $response = $frame->next($request);

    $this->fractal->setSerializer($this->getSerializer());

    // Handle a Resource
    if ($response instanceof ResourceInterface) {
        $response = $this->fractal->createData($response);
    }

    // Handle outputting a scope
    if ($response instanceof Scope) {
        $json = $response->toJson();

        // Build a new Json response
        return JsonResponse::fromJsonString($json);
    }

    return $response;
}

Use Fractal in Your APIs

If you want to use Fractal in any of your own API custom code, simply make sure you apply this middleware to your route group or route. When you start working with complex interconnected API objects, you’ll be happy you did.

Loading Conversation