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.
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.
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.
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.
endpoint:
resource: ../src/Endpoint
type: endpoint
It is also possible to use prefixes if you load a folder.
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.
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
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.
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.
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
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.
public function handleRequest($options, Request $request, Data $data, Context $context)
{
$book = $this->findBook($request);
$context->set('book', $book);
$data->set('title', $book->getTitle());
}
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.