wizzaq / rest-bundle
Wizzaq RestBundle
Installs: 3
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 1
Forks: 0
Open Issues: 0
Type:symfony-bundle
Requires
- php: ^8.0
- ext-json: *
- symfony/cache: ^6.0
- symfony/config: ^6.0
- symfony/dependency-injection: ^6.0
- symfony/framework-bundle: ^6.0
- symfony/polyfill-php81: 1.x-dev
Requires (Dev)
- doctrine/coding-standard: ^9.0
- doctrine/orm: ^2.10 || ^3.0
- friendsofphp/proxy-manager-lts: ^1.0
- phpunit/phpunit: ^7.5 || ^8.0 || ^9.3 || ^10.0
- psalm/plugin-phpunit: ^0.16.1
- psalm/plugin-symfony: ^3
- psr/log: ^1.1.4|^2.0|^3.0
- symfony/deprecation-contracts: ^2.1|^3
- symfony/doctrine-bridge: ^6.0
- symfony/form: ^6.0
- symfony/phpunit-bridge: ^6.0
- symfony/property-info: ^6.0
- symfony/proxy-manager-bridge: ^6.0
- symfony/security-bundle: ^6.0
- symfony/twig-bridge: ^6.0
- symfony/validator: ^6.0
- symfony/web-profiler-bundle: ^6.0
- symfony/yaml: ^6.0
- twig/twig: ^1.34|^2.12|^3.0
- vimeo/psalm: ^4.7
Suggests
- symfony/doctrine-bridge: To use ProcessForm argument resolver.
- symfony/form: To use ProcessForm argument resolver.
README
Rest bundle what you always looked for.
Usage
ProcessForm argument
Wraps typical form handling to more clean code of action.
Configuration
# config/packages/wizzaq_rest.yaml wizzaq_rest: use_resolvers: true # set to false to completely disable resolvers
Simple example
Simple example of typical controller.
<?php namespace App\Controller; use App\Form\Filter\MyEntityFilterType; use App\Form\Dto\Filter\MyEntityFilter; use App\Repository\MyEntityRepository; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use Wizzaq\RestBundle\Attribute\ProcessForm; class MyEntityController extends AbstractController { #[Route('/my-entity', methods: 'GET')] public function list( #[ProcessForm(MyEntityFilterType::class, mapEntity: false, throwOnNotValid: false)] ?MyEntityFilter $filter = null, MyEntityRepository $repository ): Response { return $this->json(['list' => $repository->list($filter ?? new MyEntityFilter())]); } #[Route('/my-entity', methods: 'POST')] #[Route('/my-entity/{id}', methods: 'PUT')] public function edit( #[ProcessForm(MyEntityType::class)] MyEntity $entity, EntityManagerInterface $em ): Response { // $entity is processed by form and validated // Wizzaq\RestBundle\Exception\FormValidationException will be thrown if validation failed $em->persist($entity); $em->flush(); return new JsonResponse(['id' => $entity->getId()]); } }
All options
A number of options are available on the ProcessForm
attribute to
control behavior:
form
:string
(required)Form type class name to process
#[Route('/my-entity/{id}', methods: 'PUT')] public function edit( #[ProcessForm(form: MyEntityType::class)] MyEntity $entity, EntityManagerInterface $em ): Response { // $entity is processed by form and validated // Wizzaq\RestBundle\Exception\FormValidationException will be thrown if validation failed $em->persist($entity); $em->flush(); return new JsonResponse(['id' => $entity->getId()]); }
mapEntity
:null
|bool
(default:null
)If not null, then strictly notify
ProcessFormResolver
use or notEntityValueResolver
to resolve value before process by form.
Iffalse
, thennull
will be used as initial value to create form.
Ifnull
, thenProcessFormResolver
will try to guess it. Can be useful if action supports create and edit at once.⚠️ We recommend you always specify this option explicitly.
#[Route('/my-entity/{id}', methods: 'PUT')] public function edit( #[ProcessForm(form: MyEntityType::class, mapEntity: true)] MyEntity $entity, EntityManagerInterface $em ): Response { // $entity is processed by form and validated // Wizzaq\RestBundle\Exception\FormValidationException will be thrown if validation failed $em->persist($entity); $em->flush(); return new JsonResponse(['id' => $entity->getId()]); }
submit
:bool
(defaultfalse
)If true, then
$form->submit($data, false)
(yes, with$clearMissing
=false
) will we used instead of default$form->handleRequest($request)
#[Route('/my-entity', methods: 'GET')] public function list( #[ProcessForm(MyEntityFilterType::class, submit: true)] ?MyEntityFilter $filter = null, MyEntityRepository $repository ): Response { return $this->json(['list' => $repository->list($filter ?? new MyEntityFilter())]); }
throwOnNotValid
:bool
(defaulttrue
)By default
ProcessFormResolver
throwingFormValidationException
if validation failing.Set
throwOnNotValid
tofalse
if you want to process not valid form inside action.Processed form available by
\Wizzaq\RestBundle\Config\RestConfig::processedForm($request)... use Wizzaq\RestBundle\Config\RestConfig; ... #[Route('/my-entity/{id}', methods: ['GET', 'POST'])] public function edit( #[ProcessForm(form: MyEntityType::class, throwOnNotValid: false)] ?MyEntity $entity = null, Request $request, RestConfig $restConfig, EntityManagerInterface $em ): Response { if (null === $entity) { $this->addFlash( 'error', 'Entity not found!' ); return new RedirectResponse('/my-entity'); } $form = $restConfig->processedForm($request); if ($form->isSubmitted() && $form->isValid()) { // do some stuff here $em->persist($entity); $em->flush(); return new RedirectResponse('/my-entity'); } return $this->render('my_entity/edit.html.twig', [ 'form' => $form, ]); }Inherited from MapEntity:
id
If an
id
option is configured and matches a route parameter, then the resolver will find by the primary key
mapping
Configures the properties and values to use with the
findOneBy()
method: the key is the route placeholder name and the value is the Doctrine property name
exclude
Configures the properties that should be used in the
findOneBy()
method by excluding one or more properties so that not all are used
stripNull
If true, then when
findOneBy()
is used, any values that arenull
will not be used for the query.
objectManager
By default, the
EntityValueResolver
uses the default object manager, but you can configure this
evictCache
If true, forces Doctrine to always fetch the entity from the database instead of cache.
disabled
If true, the
ProcessFormResolver
will not try to replace the argument.
resolver
By default
ProcessFormResolver
will be used to resolve argument, but you can configure this
How it works.
ProcessForm
argument extends MapEntity and using EntityValueResolver
to find existing entity exactly as described in their doc.
After ProcessFormResolver
creating form with parameters:
- with object (or
null
) returned fromEntityValueResolver
as$data
- current
method
of form will be replaced with actual method from$request
, if method is notGET
and processing it according defined options.
If form valid it returns processed data from form.
if not, then it throws FormValidationException
or returns found object if throwOnNotValid
is false
Protocols
While developing an API we all have main listener to decode payload from JSON and put it back into request to process like:
public function onRequest(RequestEvent $event): void { $request = $event->getRequest(); if ('' === $request->getContent()) { return; } if ('json' !== $request->getContentTypeFormat()) { return; } try { $request->request->replace($request->toArray()); } catch (JsonException $e) { throw new BadRequestHttpException('Unable to parse request.', $e); } }
or you decoding it every time in actions 🤔
Now you can use WizzaqRestBundle
to automate it by two ways:
⚠️ Protocols handling enabled by default config
-
Simple add
_rest: true
to route default options inconfig/routes.yaml
:controllers: resource: ../src/Controller/Api/ type: attribute prefix: /api defaults: { _rest: true }
-
Use
Rest
argument. Can be applied to class or method:<?php namespace App\Controller; use App\Form\Filter\MyEntityFilterType; use App\Form\Dto\Filter\MyEntityFilter; use App\Repository\MyEntityRepository; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bridge\Twig\Attribute\Template; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Routing\Attribute\Route; use Wizzaq\RestBundle\Attribute\ProcessForm; use Wizzaq\RestBundle\Attribute\Rest; use Wizzaq\RestBundle\Exception\FormValidationException; class MyEntityController extends AbstractController { #[Route('/my-entity', methods: 'GET')] #[Rest(responseSerializationGroups: 'default')] public function list( #[ProcessForm(form: MyEntityFilterType::class, throwOnNotValid: false)] ?MyEntityFilter $filter = null, MyEntityRepository $repository ): array { return ['list' => $repository->list($filter ?? new MyEntityFilter())]; } #[Route('/my-entity/{id}', name: 'app.my_entity.get_or_update', methods: ['GET', 'POST'])] #[Route('/api/my-entity/{id}', name: 'api.my_entity.get_or_update', methods: ['GET', 'POST'])] #[Rest(routes: 'api.my_entity.get_or_update', responseSection: 'entity', responseSerializationGroups: ['with_related', 'default'])] #[Template('my_entity/edit.html.twig')] public function edit( #[ProcessForm(MyEntityType::class, throwOnNotValid: false)] MyEntity $entity, Request $request, RestConfig $restConfig, EntityManagerInterface $em ): array { $form = $restConfig->processedForm($request); if ($form->isSubmitted() && $form->isValid()) { // do some stuff here $em->persist($entity); $em->flush(); return $restConfig->isRest($request) ? ['entity' => $entity] : new RedirectResponse('/my-entity'); } elseif ($form->isSubmitted() && !$form->isValid() && $restConfig->isRest($request)) { throw new FormValidationException($form); } return [ 'form' => $form, 'entity' => $entity, ] } }
Configuration options
Full configuration options:
# config/packages/wizzaq_rest.yaml wizzaq_rest: use_protocols: true # set to false to completely disable `ProtocolListener` default_protocol: null # uses first defined protocol by default protocols: # available protocols rest: true # only one simple protocol defined by bundle right now ^_^ default_response_section: null # if not null, then only defined section of returned result will be used as response for rest route serializer: null # serializer service id (autoselect by default from jms_serializer/serializer)
Rest argument options
routes
:null
|string
|array
Apply only to defined routes
protocol
:?string
Protocol name to use
responseSection
:?string
Only defined section of returned result will be used as response
responseSerializationGroups
:null
|string
|array
Pass defined serialization groups to serializer when serialize response by protocol
Create own protocol
If you want to create you own protocol just implement Wizzaq\RestBundle\Protocol\NamedProtocolInterface
and it will be tagged as protocol by autoconfiguration
namespace App\Protocol; use Wizzaq\RestBundle\Config\RestConfig; use Wizzaq\RestBundle\Protocol\NamedProtocolInterface; use Wizzaq\RestBundle\Protocol\RestProtocol; class MyProtocol extends RestProtocol implements NamedProtocolInterface { public function __construct(RestConfig $restConfig, bool $debug = false, $serializer = null) { parent::__construct($restConfig, $debug, $serializer); } public function getProtocolName(): string { return 'my_rest'; } public function processResponse($response, Request $request): Response { return parent::processResponse(['success' => true, 'data' => $response], $request); } }
Or implement Wizzaq\RestBundle\Protocol\ProtocolInterface
and tag service manually with alias
attribute:
# config/services.yaml ... services: ... App\Protocol\MyProtocol: tags: - { name: 'wizzaq_rest.protocol', alias: 'my_rest' }
Then you can disable unnecessary default protocol:
# config/packages/wizzaq_rest.yaml wizzaq_rest: default_protocol: 'my_rest' # not nessesary if you have only one protocol protocols: rest: false
Bonus
CircularReferenceHandler
Lost CircularReferenceHandler for Symfony Serializer.
# config/packages/framework.yaml framework: ... serializer: circular_reference_handler: 'wizzaq_rest.serializer.circular_reference_handler'