Skip to content

Api bundle

Introduction

Installation

Endpoints

The Api Bundle introduce the concept of endpoints. An endpoint is a class with a handleRequest function to process the request and create data. Similar to an action of a controller, but it will not expect a response for returning. Instead we apply data and let decide later how a response will look like.

Here we have a simple endpoint, that uses the Route attribute for routing and find a book by the request and add the title to the data.

php
namespace App\Endpoint;

use Enhavo\Bundle\ApiBundle\Data\Data;
use Enhavo\Bundle\ApiBundle\Endpoint\AbstractEndpointType;
use Enhavo\Bundle\ApiBundle\Endpoint\Context;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;

#[Route(path: '/book/{slug}', name: 'app_book', defaults: ['_format' => 'html'])]
class BookEndpointType extends AbstractEndpointType
{
    public function handleRequest($options, Request $request, Data $data, Context $context)
    {
        $book = $this->findBook($request);
        $data->set('title', $book->getTitle());
    }
    
    private function findBook(Request $request)
    {
        // ...
    }
}

Note

The data you add, must be a scalar type or an array. If you have an object, you have to normalize it first.

The Api Bundle only supports a json response by default, but could be extended in your application or use the endpoints in the app bundle to use templates for example.

Change status code

You can use the context object to change the response behaviour, e.g. change the status code.

php
public function handleRequest($options, Request $request, Data $data, Context $context)
{
    $book = $this->findBook($request);
    if ($book === null) { 
         $context->setStatusCode(400); 
         return; 
    } 
    $data->set('title', $book->getTitle());
}

Redirect

It is also possible to pass a hole response to the context.

php
public function handleRequest($options, Request $request, Data $data, Context $context)
{
    $book = $this->findBook($request);
    if ($book->titleChanged()) { 
         $response = new RedirectResponse($this->generateUrl('app_book', [ 
            'id' => $book->getId(); 
         ])) 
         $context->setResponse($response); 
         return; 
    } 
    $data->set('title', $book->getTitle());
}

Warning

The response is just a suggestion for the endpoint. In some cases this might be ignored.

Load endpoints

If you use route attributes for endpoints. Make sure you load the Endpoint folder in your routing. It will also check all subfolder for further endpoints.

yaml
endpoint:
    resource: ../src/Endpoint
    type: endpoint

It is also possible to use prefixes if you load a folder.

yaml
endpoint:
    resource: ../src/Endpoint/User
    prefix: /user
    type: endpoint

Warning

If you use prefixes for different folders, you should take care, that no folder will be loaded twice. The last loaded route will always count, but routes can be only overwritten by name. So if you don't use names for the routes it will be added twice.

Routing

Instead of php attributes, you can also define routes over the configuration. That make sense if you wan't to use different options for endpoints.

yaml
app_book:
    path: /book/{slug}
    defaults:
        _endpoint:
            type: App\Endpoint\BookEndpointType

Options

If your endpoint will be reused for different routes, you can make it configurable with options. For the options we use the OptionsResolver component from Symfony

Use the function configureOptions to define the possible options for the endpoint. The resolved options will be passed to several functions of the endpoint e.g. handleRequest

php
use Symfony\Component\OptionsResolver\OptionsResolver; 

class BookEndpointType extends AbstractEndpointType
{
    public function handleRequest($options, Request $request, Data $data, Context $context)
    {
        $book = $this->findBook($request);
        if ($options['description']) { 
            $data->set('description', $book->getDescription()); 
        } 
        $data->set('title', $book->getTitle());
    }
    
    public function configureOptions(OptionsResolver $resolver): void
    { 
        $resolver->setDefaults([ 
            'description' => true, 
        ]); 
    } 
    
    private function findBook(Request $request)
    {
        // ...
    }
}

So if you use the endpoint with the routing configuration, you are now able to change its behaviour over options.

yaml
app_book:
    path: /book/{slug}
    defaults:
        _endpoint:
            type: App\Endpoint\BookEndpointType
            description: true

app_other_book:
    path: /other/book/{slug}
    defaults:
        _endpoint:
            type: App\Endpoint\BookEndpointType
            description: false

Endpoint extensions

The endpoint api uses the type pattern, so it's possible that multiple endpoints create data for a single request.

From the type pattern we know two possible extensions. One is the parent (vertical) and the other one is the extension type (horizontal).

Extend with parent

Extension by parent will only extend one single endpoint. You define the parent by return the FQCN in the getParentType function.

php
namespace App\Endpoint;

use Enhavo\Bundle\ApiBundle\Data\Data;
use Enhavo\Bundle\ApiBundle\Endpoint\AbstractEndpointType;
use Enhavo\Bundle\ApiBundle\Endpoint\Context;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;

class SectionEndpointType extends AbstractEndpointType
{
    public function handleRequest($options, Request $request, Data $data, Context $context)
    {
        $sections = $this->findSections($request);
        $data->set('sections', $sections);
    }
    
    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'sections' => true,
        ]);
    }
    
    public static function getParentType(): ?string
    {
        return BookEndpointType::class;
    }
}

Extend with extension type

The extension type can extend multiple extension at the same time with getExtendedTypes

php
namespace App\Endpoint\Extension;

use Enhavo\Bundle\ApiBundle\Data\Data;
use Enhavo\Bundle\ApiBundle\Endpoint\AbstractEndpointTypeExtension;
use Enhavo\Bundle\ApiBundle\Endpoint\Context;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\OptionsResolver\OptionsResolver;

class SectionExtensionType extends AbstractEndpointTypeExtension
{
    public function handleRequest($options, Request $request, Data $data, Context $context)
    {
        $sections = $this->findSections($request);
        $data->set('sections', $sections);
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'sections' => true,
        ]);
    }

    public static function getExtendedTypes(): array
    {
        return [BookEndpointType::class];
    }
}

Context Data

To communicate between extended endpoints, you can use the Context class. Just set some data and later you can get the data in the extended type.

php
public function handleRequest($options, Request $request, Data $data, Context $context)
{
    $book = $this->findBook($request);
    $context->set('book', $book);
    $data->set('title', $book->getTitle());
}
php
public function handleRequest($options, Request $request, Data $data, Context $context)
{
    $book = $context->get('book');
    $data->set('sections', $book->getSections());
}

Tip

This is often useful if you retrieve an object from e.g. a repository and then normalize it. If you pass it also to the context, then the next extensions don't have to retrieve it again. This may increase performance as well.

Normalization

Api documentation