FOSRestBundle - neue API Methoden via Bundle hinzufügen
Eine kurze Anleitung wie man sein Symfony Projekt mit Hilfe eines Bundles um neue API Methoden erweitern kann, wenn man in seinem Hauptprojekt auf den folgenden Stack setzt:
- Symfony >= 4.3
- FOSRestbundle
- JMSSerializer
- NelmioApiDocBundle
Welche Ziele werden verfolgt?
Es geht hier nicht darum ein vollständig generisches Symfony Plugin zu entwickeln, welches in jedem Symfony Projekt lauffähig ist.
Vielmehr will ich heute ein Bundle für ein bestehendes Symfony-Projekt entwickeln, dessen eingesetzte Bundles bekannt sind. Dies ist z.B. für viele OpenSource Projekte interessant, deren Grundlagen ja offen zugänglich sind.
Es geht ebenfalls nicht darum, den Code eines vollständigen Bundles zu teilen, sondern nur die Rumpfmethoden und wichtigesten Erweiterungspunkte aufzuzeigen.
Der Code
Ich verwende ein PrependExtensionInterface
um die benötigten YAML Konfigurationen zu laden.
Statt der gezeigten Methodik mit YAML, könntet ihr auch genausogut XML verwenden oder die Configs direkt
als Array hinterlegen.
JMS Serializer Konfiguration
Unter Resources/config/jms_serializer.yaml
speichern wir die Konfiguration des Serializers:
jms_serializer:
metadata:
directories:
Demo:
namespace_prefix: "Acme\\DemoBundle"
path: "@DemoBundle/Resources/config/serializer"
warmup:
paths:
included:
- "%kernel.root_dir%/../vendor/acme/DemoBundle/Entity/"
Dann wollen wir dem JMSSerializer auch gleich noch mitteilen, welche Felder er wie ausgeben soll.
Das machen wir unter Resources/config/serializer/Entity.Demo.yml
:
Acme\DemoBundle\Entity\Demo:
exclusion_policy: All
custom_accessor_order: [id, page, pageSize]
properties:
id:
include: true
page:
include: true
pageSize:
exclude: true
virtual_properties:
getPageSize:
serialized_name: pageSize
exp: "object.getPageSize() === null ? 50 : object.getPageSize()"
type: int
Nelmio API Doc Konfiguration
Unter Resources/config/nelmio_api_doc.yaml
speichern wir die Konfiguration des API Doc Bundles:
nelmio_api_doc:
models:
names:
- { alias: DemoForm, type: Acme\DemoBundle\Form\DemoForm, groups: [Default, Entity, SomeGroup] }
- { alias: DemoEntity, type: Acme\DemoBundle\Entity\Demo, groups: [Default, Entity, SomeGroup] }
- { alias: DemoCollection, type: Acme\DemoBundle\Entity\Demo, groups: [Default, Collection, SomeGroup] }
Die Bundle Extension Klasse
Ich habe mich für den Weg entschieden, weil das Hauptprojekt ebenfalls auf YAMl setzt.
Unter DependencyInjection/DemoExtension.php
finden wir die Extension Klasse des Bundles:
namespace Acme\DependencyInjection;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\Yaml\Parser;
final class DemoExtension extends Extension implements PrependExtensionInterface
{
public function load(array $configs, ContainerBuilder $container)
{
// ...
}
public function prepend(ContainerBuilder $container)
{
$yamlParser = new Parser();
$config = $yamlParser->parse(
file_get_contents(__DIR__ . '/../Resources/config/jms_serializer.yaml')
);
$container->prependExtensionConfig('jms_serializer', $config['jms_serializer']);
$config = $yamlParser->parse(
file_get_contents(__DIR__ . '/../Resources/config/nelmio_api_doc.yaml')
);
$container->prependExtensionConfig('nelmio_api_doc', $config['nelmio_api_doc']);
}
Demo Rest Controller
Unter API/DemoController.php
wird der neue API Controller erstellt:
namespace Acme\DemoBundle\API;
use Acme\DemoBundle\Demo;
use FOS\RestBundle\Controller\Annotations as Rest;
use FOS\RestBundle\Controller\Annotations\RouteResource;
use FOS\RestBundle\Request\ParamFetcherInterface;
use FOS\RestBundle\View\View;
use FOS\RestBundle\View\ViewHandlerInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Swagger\Annotations as SWG;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
/**
* @RouteResource("Demo")
* @Security("is_granted('ROLE_USER')")
*/
final class DemoController extends AbstractController
{
/**
* @var ViewHandlerInterface
*/
private $viewHandler;
public function __construct(ViewHandlerInterface $viewHandler)
{
$this->viewHandler = $viewHandler;
}
/**
* Returns a collection of demos
*
* @SWG\Response(
* response=200,
* description="Returns a collection of demo entities",
* @SWG\Schema(
* type="array",
* @SWG\Items(ref="#/definitions/DemoCollection")
* )
* )
* @Rest\QueryParam(name="page", requirements="\d+", strict=true, nullable=true, description="The page to display, renders a 404 if not found (default: 1)")
* @Rest\QueryParam(name="size", requirements="\d+", strict=true, nullable=true, description="The amount of entries for each page (default: 50)")
*/
public function cgetAction(ParamFetcherInterface $paramFetcher): Response
{
$demo = new Demo();
if (null !== ($page = $paramFetcher->get('page'))) {
$demo->setPage($page);
}
if (null !== ($size = $paramFetcher->get('size'))) {
$demo->setPageSize($size);
}
$view = new View([$demo], 200);
$view->getContext()->setGroups(['Default', 'Collection', 'SomeGroup']);
return $this->viewHandler->handle($view);
}
// ...
}
Die Routen
Ich setze hier vorraus, dass Ihr Eure Rest Controller im Sub-Namespace und damit auch Verzeichnis API/
speichert (wie
in der Controller Klasse vorgeschlagen), um diese von den “normalen” Controllern zu trennen,
Diese Route muss in Eurem Hauptprojekt aktiviert werden.
demo.api:
resource: "@DemoBundle/API/"
type: rest
prefix: /api
Die restlichen Klassen und Bundle Logik sind ausführlich in der Symfony Dokumentation beschrieben, das werde ich hier nicht wiedergeben … daher sind wir auch schon: FERTIG!
Viel Spaß 😃