Skip to content

Creating new REST resource

To create a new REST resource, you need to prepare:

  • The REST route leading to a controller action.
  • The controller and its action.
  • Optionally, one or several InputParser objects if the controller needs to receive a payload to treat, one or several value classes to represent this payload and potentially one or several new media types to type this payload in the Content-Type header.
  • Optionally, one or several new value classes to represent the controller action result, their ValueObjectVisitor to help the generator to turn this into XML or JSON and potentially one or several new media types to claim in the Accept header the desired value.
  • Optionally, the addition of this resource route to the REST root.

In the following example, you add a greeting resource to the REST API. It is available through GET and POST methods. GET sets default values while POST allows inputting custom values.

Route

New REST routes should use the REST URI prefix for consistency. To ensure that they do, in the config/routes.yaml file, while importing a REST routing file, use ezpublish_rest.path_prefix parameter as a prefix.

1
2
3
app.rest:
    resource: routes_rest.yaml
    prefix: '%ezpublish_rest.path_prefix%'

The config/routes_rest.yaml file imported above is created with the following configuration:

1
2
3
4
app.rest.greeting:
    path: '/greet'
    controller: App\Rest\Controller\DefaultController::helloWorld
    methods: [GET]

CSRF protection

If a REST route is designed to be used with unsafe methods, the CSRF protection is enabled by default like for built-in routes. You can disable it by using the route parameter csrf_protection.

1
2
3
4
5
6
app.rest.greeting:
    path: '/greet'
    controller: App\Rest\Controller\DefaultController::helloWorld
    methods: [GET,POST]
    defaults:
        csrf_protection: false

Controller

Controller service

You can use the following configuration to have all controllers from the App\Rest\Controller\ namespace (files in the src/Rest/Controller/ folder) to be set as REST controller services.

1
2
3
4
5
6
7
8
services:
    #…
    App\Rest\Controller\:
        resource: '../src/Rest/Controller/'
        parent: ezpublish_rest.controller.base
        autowire: true
        autoconfigure: true
        tags: [ 'controller.service_arguments' ]

Having the REST controllers set as services enables using features such as the InputDispatcher service in the Controller action.

Controller action

A REST controller should:

  • return a value object and have a Generator and ValueObjectVisitors producing the XML or JSON output;
  • extend Ibexa\Rest\Server\Controller to inherit utils methods and properties like InputDispatcher or RequestParser.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php declare(strict_types=1);

namespace App\Rest\Controller;

use App\Rest\Values\Greeting;
use EzSystems\EzPlatformRest\Message;
use EzSystems\EzPlatformRest\Server\Controller;
use Symfony\Component\HttpFoundation\Request;

class DefaultController extends Controller
{
    public function greet(Request $request): Greeting
    {
        if ('POST' === $request->getMethod()) {
            return $this->inputDispatcher->parse(
                new Message(
                    ['Content-Type' => $request->headers->get('Content-Type')],
                    $request->getContent()
                )
            );
        }

        return new Greeting();
    }
}

If the returned value was depending on a Location, it could have been wrapped in a CachedValue to be cached by the reverse proxy (like Varnish) for future calls.

CachedValue is used in the following way:

1
2
3
4
return new CachedValue(
    new MyValue($args…),
    ['locationId'=> $locationId]
);

Value and ValueObjectVisitor

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<?php declare(strict_types=1);

namespace App\Rest\Values;

class Greeting
{
    public string $salutation;

    public string $recipient;

    public function __construct($salutation = 'Hello', $recipient = 'World')
    {
        $this->salutation = $salutation;
        $this->recipient = $recipient;
    }
}

A ValueObjectVisitor must implement the visit method.

Argument Description
$visitor The output visitor.
Can be used to set custom response headers (setHeader), HTTP status code ( setStatus)
$generator The actual response generator. It provides you with a DOM-like API.
$data The visited data. The exact object that you returned from the controller.
It can't have a type declaration because the method signature is shared.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<?php declare(strict_types=1);

namespace App\Rest\ValueObjectVisitor;

use EzSystems\EzPlatformRest\Output\Generator;
use EzSystems\EzPlatformRest\Output\ValueObjectVisitor;
use EzSystems\EzPlatformRest\Output\Visitor;

class Greeting extends ValueObjectVisitor
{
    public function visit(Visitor $visitor, Generator $generator, $data)
    {
        $visitor->setHeader('Content-Type', $generator->getMediaType('Greeting'));
        $generator->startObjectElement('Greeting');
        $generator->attribute('href', $this->router->generate('app.rest.greeting'));
        $generator->valueElement('Salutation', $data->salutation);
        $generator->valueElement('Recipient', $data->recipient);
        $generator->valueElement('Sentence', "{$data->salutation} {$data->recipient}");
        $generator->endObjectElement('Greeting');
    }
}

The Values/Greeting class is linked to its ValueObjectVisitor through the service tag.

1
2
3
4
5
6
services:
    #…
    App\Rest\ValueObjectVisitor\Greeting:
        parent: ezpublish_rest.output.value_object_visitor.base
        tags:
            - { name: ezpublish_rest.output.value_object_visitor, type: App\Rest\Values\Greeting }

Here, the media type is application/vnd.ez.api.Greeting plus a format. To have a different vendor than the default, you could create a new Output\Generator or hard-code it in the ValueObjectVisitor like in the RestLocation example.

InputParser

A REST resource could use route parameters to handle input, but this example illustrates the usage of an input parser.

For this example, the structure is a GreetingInput root node with two leaf nodes, Salutation and Recipient.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<?php declare(strict_types=1);

namespace App\Rest\InputParser;

use App\Rest\Values\Greeting;
use EzSystems\EzPlatformRest\Exceptions;
use EzSystems\EzPlatformRest\Input\BaseParser;
use EzSystems\EzPlatformRest\Input\ParsingDispatcher;

class GreetingInput extends BaseParser
{
    public function parse(array $data, ParsingDispatcher $parsingDispatcher)
    {
        if (!isset($data['Salutation'])) {
            throw new Exceptions\Parser("Missing or invalid 'Salutation' element for Greeting.");
        }

        return new Greeting($data['Salutation'], $data['Recipient'] ?? 'World');
    }
}

Here, this InputParser directly returns the right value object. In other cases, it could return whatever object is needed to represent the input for the controller to perform its action, like arguments to use with a Repository service.

1
2
3
4
5
6
services:
    #…
    App\Rest\InputParser\GreetingInput:
        parent: ezpublish_rest.input.parser
        tags:
            - { name: ezpublish_rest.input.parser, mediaType: application/vnd.ez.api.GreetingInput }

Testing the new resource

Now you can test both GET and POST methods, and both XML and JSON format for inputs and outputs.

1
2
3
4
5
6
7
8
curl https://api.example.com/api/ezp/v2/greet --include;
curl https://api.example.com/api/ezp/v2/greet --include --request POST \
    --header 'Content-Type: application/vnd.ez.api.GreetingInput+xml' \
    --data '<GreetingInput><Salutation>Good morning</Salutation></GreetingInput>';
curl https://api.example.com/api/ezp/v2/greet --include --request POST \
    --header 'Content-Type: application/vnd.ez.api.GreetingInput+json' \
    --data '{"GreetingInput": {"Salutation": "Good day", "Recipient": "Earth"}}' \
    --header 'Accept: application/vnd.ez.api.Greeting+json';
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
HTTP/1.1 200 OK
Content-Type: application/vnd.ez.api.greeting+xml

<?xml version="1.0" encoding="UTF-8"?>
<Greeting media-type="application/vnd.ez.api.Greeting+xml" href="/api/ezp/v2/greet">
 <Salutation>Hello</Salutation>
 <Recipient>World</Recipient>
 <Sentence>Hello World</Sentence>
</Greeting>

HTTP/1.1 200 OK
Content-Type: application/vnd.ez.api.greeting+xml

<?xml version="1.0" encoding="UTF-8"?>
<Greeting media-type="application/vnd.ez.api.Greeting+xml" href="/api/ezp/v2/greet">
 <Salutation>Good morning</Salutation>
 <Recipient>World</Recipient>
 <Sentence>Good morning World</Sentence>
</Greeting>

HTTP/1.1 200 OK
Content-Type: application/vnd.ez.api.greeting+json

{
    "Greeting": {
        "_media-type": "application\/vnd.ez.api.Greeting+json",
        "_href": "\/api\/ezp\/v2\/greet",
        "Salutation": "Good day",
        "Recipient": "Earth",
        "Sentence": "Good day Earth"
    }
}

Registering resources in REST root

You can add the new resource to the root resource through a configuration with the following pattern:

1
2
3
4
5
6
7
ez_publish_rest:
    system:
        <scope>:
            rest_root_resources:
                <resourceName>:
                    mediaType: <MediaType>
                    href: 'router.generate("<resource_route_name>", {routeParameter: value})'

The router.generate renders a URI based on the name of the route and its parameters. The parameter values can be a real value or a placeholder. For example, 'router.generate("ibexa.rest.load_location", {locationPath: "1/2"})' results in /api/ezp/v2/content/locations/1/2 while 'router.generate("ibexa.rest.load_location", {locationPath: "{locationPath}"})' gives /api/ezp/v2/content/locations/{locationPath}. This syntax is based on Symfony's expression language, an extensible component that allows limited/readable scripting to be used outside the code context.

In this example, app.rest.greeting is available in every SiteAccess (default):

1
2
3
4
5
6
7
ibexa_rest:
    system:
        default:
            rest_root_resources:
                greeting:
                    mediaType: Greeting
                    href: 'router.generate("app.rest.greeting")'

You can place this configuration in any regular config file, like the existing config/packages/ibexa.yaml, or a new config/packages/ibexa_rest.yaml file.

The above example adds the following entry to the root XML output:

1
<greeting media-type="application/vnd.ez.api.Greeting+xml" href="/api/ezp/v2/greet"/>