Skip to content

Adding custom media type

In this example case, you pass a new media type in the Accept header of a GET request to /content/locations/{locationPath} route and its controller action (Controller/Location::loadLocation).

By default, this resource takes an application/vnd.ibexa.api.Location+xml (or +json) Accept header. The following example adds the handling of a new media type application/app.api.Location+xml (or +json) Accept header to obtain a different response using the same controller.

You need the following elements: * A ValueObjectVisitor to create the new response corresponding to the new media type; * A ValueObjectVisitorDispatcher to have this ValueObjectVisitor used to visit the default controller result; * An Output\Visitor service associating this new ValueObjectVisitorDispatcher with the new media type.

Note

You can change the vendor name (from default vnd.ibexa.api to new app.api like in this example), or you can create a new media type in the default vendor (like vnd.ibexa.api.Greeting in the Creating a new REST resource example). To do so, tag your new ValueObjectVisitor with ibexa.rest.output.value_object.visitor to add it to the existing ValueObjectVisitorDispatcher, and a new one will not be needed. This way, the media-type attribute is also easier to create, because the default Output\Generator uses this default vendor. This example presents creating a new vendor as a good practice, to highlight that this is custom extensions that isn't available in a regular Ibexa DXP installation.

New RestLocation ValueObjectVisitor

The controller action returns a Values\RestLocation object wrapped in Values\CachedValue. The new ValueObjectVisitor has to visit Values\RestLocation to prepare the new Response.

To be accepted by the ValueObjectVisitorDispatcher, all new ValueObjectVisitor need to extend the abstract class Output\ValueObjectVisitor. In this example, this new ValueObjectVisitor extends the built-in RestLocation visitor to reuse it. This way, the abstract class is implicitly inherited.

 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
33
34
35
36
37
38
39
40
41
42
43
44
45
<?php declare(strict_types=1);

namespace App\Rest\ValueObjectVisitor;

use Ibexa\Contracts\Core\Repository\URLAliasService;
use Ibexa\Contracts\Rest\Output\Generator;
use Ibexa\Contracts\Rest\Output\Visitor;
use Ibexa\Rest\Server\Output\ValueObjectVisitor\RestLocation as BaseRestLocation;
use Ibexa\Rest\Server\Values\URLAliasRefList;

class RestLocation extends BaseRestLocation
{
    private URLAliasService $urlAliasService;

    public function __construct(URLAliasService $urlAliasService)
    {
        $this->urlAliasService = $urlAliasService;
    }

    public function visit(Visitor $visitor, Generator $generator, $data)
    {
        // Not using $generator->startObjectElement to not have the XML Generator adding its own media-type attribute with the default vendor
        $generator->startHashElement('Location');
        $generator->attribute(
            'media-type',
            'application/app.api.Location+' . strtolower((new \ReflectionClass($generator))->getShortName())
        );
        $generator->attribute(
            'href',
            $this->router->generate(
                'ibexa.rest.load_location',
                ['locationPath' => trim($data->location->pathString, '/')]
            )
        );
        parent::visit($visitor, $generator, $data);
        $visitor->visitValueObject(new URLAliasRefList(array_merge(
            $this->urlAliasService->listLocationAliases($data->location, false),
            $this->urlAliasService->listLocationAliases($data->location, true)
        ), $this->router->generate(
            'ibexa.rest.list_location_url_aliases',
            ['locationPath' => trim($data->location->pathString, '/')]
        )));
        $generator->endHashElement('Location');
    }
}

This new ValueObjectVisitor receives a new tag app.rest.output.value_object.visitor to be associated to the new ValueObjectVisitorDispatcher in the next step. This tag has a type property to associate the new ValueObjectVisitor with the type of value is made for.

1
2
3
4
5
6
7
8
9
services:
    #…
    App\Rest\ValueObjectVisitor\RestLocation:
        class: App\Rest\ValueObjectVisitor\RestLocation
        parent: Ibexa\Contracts\Rest\Output\ValueObjectVisitor
        arguments:
            $urlAliasService: '@ibexa.api.service.url_alias'
        tags:
            - { name: app.rest.output.value_object.visitor, type: Ibexa\Rest\Server\Values\RestLocation }

New ValueObjectVisitorDispatcher

The new ValueObjectVisitorDispatcher receives the ValueObjectVisitors tagged app.rest.output.value_object.visitor. As not all value FQCNs are handled, the new ValueObjectVisitorDispatcher also receives the default one as a fallback.

1
2
3
4
5
6
7
services:
    #…
    App\Rest\Output\ValueObjectVisitorDispatcher:
        class: App\Rest\Output\ValueObjectVisitorDispatcher
        arguments:
            - !tagged_iterator { tag: 'app.rest.output.value_object.visitor', index_by: 'type' }
            - '@Ibexa\Contracts\Rest\Output\ValueObjectVisitorDispatcher'
 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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<?php declare(strict_types=1);

namespace App\Rest\Output;

use Ibexa\Contracts\Rest\Output\Generator;
use Ibexa\Contracts\Rest\Output\ValueObjectVisitorDispatcher as BaseValueObjectVisitorDispatcher;
use Ibexa\Contracts\Rest\Output\Visitor;

class ValueObjectVisitorDispatcher extends BaseValueObjectVisitorDispatcher
{
    private array $visitors;

    private BaseValueObjectVisitorDispatcher $valueObjectVisitorDispatcher;

    private Visitor $outputVisitor;

    private Generator $outputGenerator;

    public function __construct(iterable $visitors, BaseValueObjectVisitorDispatcher $valueObjectVisitorDispatcher)
    {
        $this->visitors = [];
        foreach ($visitors as $type => $visitor) {
            $this->visitors[$type] = $visitor;
        }
        $this->valueObjectVisitorDispatcher = $valueObjectVisitorDispatcher;
    }

    public function setOutputVisitor(Visitor $outputVisitor): void
    {
        $this->outputVisitor = $outputVisitor;
        $this->valueObjectVisitorDispatcher->setOutputVisitor($outputVisitor);
    }

    public function setOutputGenerator(Generator $outputGenerator): void
    {
        $this->outputGenerator = $outputGenerator;
        $this->valueObjectVisitorDispatcher->setOutputGenerator($outputGenerator);
    }

    public function visit($data)
    {
        $className = get_class($data);
        if (isset($this->visitors[$className])) {
            return $this->visitors[$className]->visit($this->outputVisitor, $this->outputGenerator, $data);
        }

        return $this->valueObjectVisitorDispatcher->visit($data);
    }
}

New Output\Visitor service

The following new pair of Ouput\Visitor entries associates Accept headers starting with application/app.api. to the new ValueObjectVisitorDispatcher for both XML and JSON. A priority is set higher than other ibexa.rest.output.visitor tagged built-in services.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
parameters:
    #…
    app.rest.output.visitor.xml.regexps: ['(^application/app\.api\.[A-Za-z]+\+xml$)']
    app.rest.output.visitor.json.regexps: ['(^application/app\.api\.[A-Za-z]+\+json$)']

services:
    #…
    app.rest.output.visitor.xml:
        class: Ibexa\Contracts\Rest\Output\Visitor
        arguments:
            - '@Ibexa\Rest\Output\Generator\Xml'
            - '@App\Rest\Output\ValueObjectVisitorDispatcher'
        tags:
            - { name: ibexa.rest.output.visitor, regexps: app.rest.output.visitor.xml.regexps, priority: 20 }

    app.rest.output.visitor.json:
        class: Ibexa\Contracts\Rest\Output\Visitor
        arguments:
            - '@Ibexa\Rest\Output\Generator\Json'
            - '@App\Rest\Output\ValueObjectVisitorDispatcher'
        tags:
            - { name: ibexa.rest.output.visitor, regexps: app.rest.output.visitor.json.regexps, priority: 20 }

Testing the new media-type

In the following example, curl and diff commands are used to compare the default media type (application/vnd.ibexa.api.Location+xml) with the new application/app.api.Location+xml.

1
2
3
diff --ignore-space-change \
  <(curl --silent https://api.example.com/api/ibexa/v2/content/locations/1/2) \
  <(curl --silent https://api.example.com/api/ibexa/v2/content/locations/1/2 --header 'Accept: application/app.api.Location+xml');
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
2c2,3
< <Location media-type="application/vnd.ibexa.api.Location+xml" href="/api/ibexa/v2/content/locations/1/2">
---
> <Location media-type="application/app.api.Location+xml" href="/api/ibexa/v2/content/locations/1/2">
>  <Location media-type="application/vnd.ibexa.api.Location+xml" href="/api/ibexa/v2/content/locations/1/2">
37a39,42
>  </Location>
>  <UrlAliasRefList media-type="application/vnd.ibexa.api.UrlAliasRefList+xml" href="/api/ibexa/v2/content/locations/1/2/urlaliases">
>   <UrlAlias media-type="application/vnd.ibexa.api.UrlAlias+xml" href="/api/ibexa/v2/content/urlaliases/0-d41d8cd98f00b204e9800998ecf8427e"/>
>  </UrlAliasRefList>