I use FOSRestBundle in Symfony 4 to API project. I use annotations and in controller I have for example
use FOS\RestBundle\Controller\Annotations as Rest;
/**
* #Rest\Get("/api/user", name="index",)
* #param UserRepository $userRepository
* #return array
*/
public function index(UserRepository $userRepository): array
{
return ['status' => 'OK', 'data' => ['users' => $userRepository->findAll()]];
}
config/packages/fos_rest.yaml
fos_rest:
body_listener: true
format_listener:
rules:
- { path: '^/api', priorities: ['json'], fallback_format: json, prefer_extension: false }
param_fetcher_listener: true
view:
view_response_listener: 'force'
formats:
json: true
Now I'd like to add custom header 'X-Total-Found' to my response. How to do it?
You are relying in FOSRestBundle ViewListener, so that gives you limited options, like not being able to pass custom headers. In order to achieve what you want, you will need to call $this->handleView() from your controller and pass it a valid View instance.
You can use the View::create() factory method or the controller $this->view() shortcut. Both take as arguments the array of your data, the status code, and a response headers array. Then, you can set up your custom header there, but you will have to do that for every call.
The other option you have, which is more maintainable, is register a on_kernel_response event listener/subscriber and somehow pass it the value of your custom header (you could store it in a request attribute for example).
Those are the two options you have. You may have a third one, but I cannot come up with it at the minute.
I ran into the same issue. We wanted to move pagination meta information to the headers and leave the response without an envelope (data and meta properties).
My Environment
Symfony Version 5.2
PHP Version 8
FOS Rest Bundle
STEP 1: Create an object to hold the header info
// src/Rest/ResponseHeaderBag.php
namespace App\Rest;
/**
* Store header information generated in the controller. This same
* object is used in the response subscriber.
* #package App\Rest
*/
class ResponseHeaderBag
{
protected array $data = [];
/**
* #return array
*/
public function getData(): array
{
return $this->data;
}
/**
* #param array $data
* #return ResponseHeaderBag
*/
public function setData(array $data): ResponseHeaderBag
{
$this->data = $data;
return $this;
}
public function addData(string $key, $datum): ResponseHeaderBag
{
$this->data[$key] = $datum;
return $this;
}
}
STEP 2: Inject the ResponseHeaderBag into the controller action
public function searchCustomers(
ResponseHeaderBag $responseHeaderBag
): array {
...
...
...
// replace magic strings and numbers with class constants and real values.
$responseHeaderBag->add('X-Pagination-Count', 8392);
...
...
...
}
STEP 3: Register a Subscriber and listen for the Response Kernel event
// config/services.yaml
App\EventListener\ResponseSubscriber:
tags:
- kernel.event_subscriber
Subscribers are a great way to listen for events.
// src/EventListener/ResponseSubscriber
namespace App\EventListener;
use App\Rest\ResponseHeaderBag;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class ResponseSubscriber implements EventSubscriberInterface
{
public function __construct(
protected ResponseHeaderBag $responseHeaderBag
){
}
/**
* #inheritDoc
*/
public static function getSubscribedEvents()
{
return [
KernelEvents::RESPONSE => ['addAdditionalResponseHeaders']
];
}
/**
* Add the response headers created elsewhere in the code.
* #param ResponseEvent $event
*/
public function addAdditionalResponseHeaders(ResponseEvent $event): void
{
$response = $event->getResponse();
foreach ($this->responseHeaderBag->getData() as $key => $datum) {
$response->headers->set($key, $datum);
}
}
}
Related
I'm trying to add a feature of multi language to my application using API Plateform and ReactJS.
I've installed StofDoctrineExtensionsBundle, I want to use the extension Translatable.
I send the local("EN" or "FR" etc) then I want to send response swtich the local.
use Gedmo\Translatable\Translatable;
/**
* #ApiResource()
* #ORM\Entity(repositoryClass=CountryRepository::class)
*/
class Country implements Translatable
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #Gedmo\Translatable
* #ORM\Column(type="string", length=255)
*/
private $name;
services.yml
App\EventSubscriber\LocaleSubscriber:
arguments: ['%kernel.default_locale%']
LocaleSubscriber.php
<?php
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class LocaleSubscriber implements EventSubscriberInterface
{
private $defaultLocale;
public function __construct(string $defaultLocale = 'en')
{
$this->defaultLocale = $defaultLocale;
}
public function onKernelRequest(RequestEvent $event)
{
$request = $event->getRequest();
if (!$request->hasPreviousSession()) {
return;
}
// try to see if the locale has been set as a _locale routing parameter
if ($locale = $request->attributes->get('_locale')) {
$request->getSession()->set('_locale', $locale);
} else {
// if no explicit locale has been set on this request, use one from the session
$request->setLocale($request->getSession()->get('_locale', $this->defaultLocale));
}
}
public static function getSubscribedEvents()
{
return [
// must be registered before (i.e. with a higher priority than) the default Locale listener
KernelEvents::REQUEST => [['onKernelRequest', 20]],
];
}
}
In the response of the webservice which allows me to get the list of countries there is no name field.
How can I get the names of the countries switch the language ?
Thanks.
AFAIK it is not very RESTfull to use the session, your api will no longer be stateless.
You can use the accept-language request header. Most browsers automatically send it with each request. Here is an event subscriber that puts it in symfonies request obect:
<?php
// src/EventSubscriber/LocaleSubscriber.php
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\HeaderUtils;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class LocaleSubscriber implements EventSubscriberInterface
{
public function onKernelRequest(RequestEvent $event)
{
$request = $event->getRequest();
$accept_language = $request->headers->get("accept-language");
if (empty($accept_language)) {
return;
}
$arr = HeaderUtils::split($accept_language, ',;');
if (empty($arr[0][0])) {
return;
}
// Symfony expects underscore instead of dash in locale
$locale = str_replace('-', '_', $arr[0][0]);
$request->setLocale($locale);
}
public static function getSubscribedEvents()
{
return [
// must be registered before (i.e. with a higher priority than) the default Locale listener
KernelEvents::REQUEST => [['onKernelRequest', 20]],
];
}
}
If you want the user to be able to choose the locale dynamically i suppose adding the accept-language request header from your own code with each request will override the default of the browser.
I made this EventSubscriber for my tutorial. Chapter 3 is about Localization and Internationalization. The api side was easy in comparision to the react client side.
I'm trying to show only selected fields in my REST action in controller.
I've found one solution - I can set groups in Entities/Models and select this group in annotation above action in my Controller.
But actually i don't want use groups, i want determine which fields i wanna expose.
I see one solution - I can create one group for every field in my Entities/Model. Like this:
class User
{
/**
* #var integer
*
* #Groups({"entity_user_id"})
*/
protected $id;
/**
* #var string
*
* #Groups({"entity_user_firstName"})
*/
protected $firstName;
/**
* #var string
*
* #Groups({"entity_user_lastName"})
*/
protected $lastName;
}
And then i can list fields above controller action.
My questions are:
Can I use better solution for this?
Can I list all groups? Like I can list all routes or all services.
This is mainly about serialization not about fosrestbundle itself.
The right way would be to create your own fieldserialization strategy.
This article got it down really nicely:
http://jolicode.com/blog/how-to-implement-your-own-fields-inclusion-rules-with-jms-serializer
It build a custom exclusion strategy as describeted here:
How do I create a custom exclusion strategy for JMS Serializer that allows me to make run-time decisions about whether to include a particular field?
Example code from first link for reference:
custom FieldExclusion strategy:
namespace Acme\Bundle\ApiBundle\Serializer\Exclusion;
use JMS\Serializer\Exclusion\ExclusionStrategyInterface;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\Context;
class FieldsListExclusionStrategy implements ExclusionStrategyInterface
{
private $fields = array();
public function __construct(array $fields)
{
$this->fields = $fields;
}
/**
* {#inheritDoc}
*/
public function shouldSkipClass(ClassMetadata $metadata, Context $navigatorContext)
{
return false;
}
/**
* {#inheritDoc}
*/
public function shouldSkipProperty(PropertyMetadata $property, Context $navigatorContext)
{
if (empty($this->fields)) {
return false;
}
$name = $property->serializedName ?: $property->name;
return !in_array($name, $this->fields);
}
}
Interface
interface ExclusionStrategyInterface
{
public function shouldSkipClass(ClassMetadata $metadata, Context $context);
public function shouldSkipProperty(PropertyMetadata $property, Context $context);
}
usage
in controller or where you need it:
$context = new SerializationContext();
$fieldList = ['id', 'title']; // fields to return
$context->addExclusionStrategy(
new FieldsListExclusionStrategy($fieldList)
);
// serialization
$serializer->serialize(new Pony(), 'json', $context);
You should be also able to mix and match with groups eg. you can also set $content->setGroups(['myGroup']) together with the fieldExclusio
I'm using Symfony2 with JMSSerializerBundle. And I'm new with last one =) What should I do in such case:
I have Image model. It contains some fields, but the main one is "name". Also, I have some models, which has reference to Image model. For example User and Application. User model has OneToOne field "avatar", and Application has OneToOne field "icon". Now, I want to serialize User instance and get something like
{
...,
"avatar": "http://example.com/my/image/path/image_name.png",
....
}
Also, I want to serialize Application and get
{
...,
"icon": "http://example.com/my/image/path/another_image_name.png",
...
}
I'm using #Inline annotation on User::avatar and Application::icon fields to reduce Image object (related to this field) to single scalar value (only image "name" needed). Also, my Image model has ExclusionPolicy("all"), and exposes only "name" field. For now, JMSSerializer output is
(For User instance)
{
...,
"name": "http://example.com/my/image/path/image_name.png",
...
}
(For Application instance)
{
...,
"name": "http://example.com/my/image/path/another_image_name.png",
...
}
The question is: How can I make JMSSerializer to preserve "avatar" and "icon" keys in serialized array instead of "name"?
Finally, I found solution. In my opinion, it is not very elegant and beautiful, but it works.
I told to JMSSerializer, that User::avatar and Application::icon are Images. To do that, I used annotation #Type("Image")
//src\AppBundle\Entity\User.php
//...
/**
* #var integer
*
* #ORM\OneToOne(targetEntity="AppBundle\Entity\Image")
* #ORM\JoinColumn(name="avatar", referencedColumnName="id")
*
* #JMS\Expose()
* #JMS\Type("Image")
*/
private $avatar;
//...
//src\AppBundle\Entity\Application.php
//...
/**
* #var integer
*
* #ORM\OneToOne(targetEntity="AppBundle\Entity\Image")
* #ORM\JoinColumn(name="icon", referencedColumnName="id")
*
* #JMS\Expose()
* #JMS\Type("Image")
*/
private $icon;
//...
I implemented handler, which serializes object with type Image to json.
<?php
//src\AppBundle\Serializer\ImageTypeHandler.php
namespace AppBundle\Serializer;
use AppBundle\Entity\Image;
use JMS\Serializer\Context;
use JMS\Serializer\GraphNavigator;
use JMS\Serializer\Handler\SubscribingHandlerInterface;
use JMS\Serializer\JsonSerializationVisitor;
use Symfony\Component\HttpFoundation\Request;
class ImageTypeHandler implements SubscribingHandlerInterface
{
private $request;
public function __construct(Request $request) {
$this->request = $request;
}
static public function getSubscribingMethods()
{
return [
[
'direction' => GraphNavigator::DIRECTION_SERIALIZATION,
'format' => 'json',
'type' => 'Image',
'method' => 'serializeImageToWebPath'
]
];
}
public function serializeImageToWebPath(JsonSerializationVisitor $visitor, Image $image = null, array $type, Context $context)
{
$path = $image ? "http://" . $this->request->getHost() . "/uploads/images/" . $image->getPath() : '';
return $path;
}
}
And the last step is to register this handler. I also injected request service to generate full web path to image in my handler.
app.image_type_handler:
class: AppBundle\Serializer\ImageTypeHandler
arguments: ["#request"]
scope: request
tags:
- { name: jms_serializer.subscribing_handler }
Also, you can use this workaround, to modify serialized data in post_serialize event.
I separated mobile and web requests with the help of kernel.view Event Listener.
The logic works like this:
if request is coming from mobile, then load xxx.mobile.twig
if request is coming from web, then load xxx.html.twig
This is working with my CustomBundle without any problem. In addition to it I'm using FOSUserBundle and HWIOAuthBundle with some of their routes. I checked var/logs/dev.log and I can't see kernel.view events regarding these bundles routes and eventually my listener cannot work with these bundles.
Could you give me an idea how could I bind to the kernel.view event for those bundles?
/**
* #param GetResponseForControllerResultEvent $event
* #return bool
*/
public function onKernelView(GetResponseForControllerResultEvent $event)
{
if (!$this->isMobileRequest($event->getRequest()->headers->get('user-agent'))) {
return false;
}
$template = $event->getRequest()->attributes->get('_template');
if (!$template) {
return false;
}
$templateReference = $this->templateNameParser->parse($template);
if ($templateReference->get('format') == 'html' && $templateReference->get('bundle') == 'CustomBundle') {
$mobileTemplate = sprintf(
'%s:%s:%s.mobile.twig',
$templateReference->get('bundle'),
$templateReference->get('controller'),
$templateReference->get('name')
);
if ($this->templating->exists($mobileTemplate)) {
$templateReference->set('format', 'mobile');
$event->getRequest()->attributes->set('_template', $templateReference);
}
}
}
There are a few things you should consider when debugging event related issues on Symfony2.
Events propagation can be stopped
it could be that another listener is listening for the very same event and is stopping the event propagation by calling $event->stopPropagation(). In that case make sure your listener is executed first (see point 2 below).
Event listeners have priorities
When defining a listener you can set its priority like shown below:
view_response_listener:
class: AppBundle\EventListener\ViewResponseListener
tags:
# The highest the priority, the earlier a listener is executed
# #see http://symfony.com/doc/2.7/cookbook/event_dispatcher/event_listener.html#creating-an-event-listener
- { name: kernel.event_listener, event: kernel.view, method: onKernelView, priority: 101 }
The other optional tag attribute is called priority, which defaults to 0 and it controls the order in which listeners are executed (the highest the priority, the earlier a listener is executed). This is useful when you need to guarantee that one listener is executed before another. The priorities of the internal Symfony listeners usually range from -255 to 255 but your own listeners can use any positive or negative integer.
Source: http://symfony.com/doc/2.7/cookbook/event_dispatcher/event_listener.html#creating-an-event-listener
Usually the dispatch of those events is done in the bootstrap file
Make sure regenerate your bootstrap file (and clear the cache now that you're at it!), especially if you're playing with priorities and/or your configuration.
composer run-script post-update-cmd
php app/console cache:clear --env=dev
The composer post-update-cmd will regenerate your bootstrap file but it will also do other things like reinstalling your assets which is probably something that you don't need. To just regenerate the bootstrap file check my answer here.
I find the solution, it is however a bit workaround, working properly now.
I put following function to my MobileTemplateListener.php file.
More details are here -> http://www.99bugs.com/handling-mobile-template-switching-in-symfony2/
/**
* #param GetResponseEvent $event
*/
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
if ($this->isMobileRequest($request->headers->get('user-agent')))
{
//ONLY AFFECT HTML REQUESTS
//THIS ENSURES THAT YOUR JSON REQUESTS TO E.G. REST API, DO NOT GET SERVED TEXT/HTML CONTENT-TYPE
if ($request->getRequestFormat() == "html")
{
$request->setRequestFormat('mobile');
}
}
}
/**
* Returns true if request is from mobile device, otherwise false
* #return boolean mobileUA
*/
private function isMobileRequest($userAgent)
{
if (preg_match('/(android|blackberry|iphone|ipad|phone|playbook|mobile)/i', $userAgent)) {
return true;
}
return false;
}
as a result when kernel.request event listener starts to handling, it is setting the format with value mobile
I was using FOSUserBundle through a child bundle to manipulate for my needs. You may find more details here -> https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/Resources/doc/overriding_controllers.md
for instance : we assume SecurityController.php
I created a file named SecurityController.php under my UserBundle It looks like following.
<?php
namespace Acme\UserBundle\Controller;
use Symfony\Component\HttpFoundation\RedirectResponse;
use FOS\UserBundle\Controller\SecurityController as BaseController;
use Symfony\Component\HttpFoundation\Request;
use Acme\UserBundle\Overrides\ControllerOverrideRenderTrait;
class SecurityController extends BaseController
{
use ControllerOverrideRenderTrait;
public function loginAction(Request $request)
{
$securityContext = $this->container->get('security.context');
$router = $this->container->get('router');
if ($securityContext->isGranted('IS_AUTHENTICATED_FULLY')) {
return new RedirectResponse($router->generate('my_profile_dashboard'), 307);
}
return parent::loginAction($request);
}
}
for all other FOS controllers I have to override render function. Which is provided by smyfony Symfony\Bundle\FrameworkBundle\Controller
But already my child bundle extends the FOSUserBundle's controllers, the only way to override this without duplicates of code is to use traits.
and I created one trait as following.
<?php
namespace Acme\UserBundle\Overrides;
use Symfony\Component\HttpFoundation\Response;
trait ControllerOverrideRenderTrait {
/**
* This overrides vendor/symfony/symfony/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php
* Renders a view.
*
* #param string $view The view name
* #param array $parameters An array of parameters to pass to the view
* #param Response $response A response instance
*
* #return Response A Response instance
*/
public function render($view, array $parameters = array(), Response $response = null)
{
$format = $this->getRequest()->getRequestFormat();
$view = str_replace('.html.', '.' . $format . '.', $view);
return $this->container->get('templating')->renderResponse($view, $parameters, $response);
}
}
The Original function provided by symfony is the following.
/**
* Renders a view.
*
* #param string $view The view name
* #param array $parameters An array of parameters to pass to the view
* #param Response $response A response instance
*
* #return Response A Response instance
*/
public function render($view, array $parameters = array(), Response $response = null)
{
return $this->container->get('templating')->renderResponse($view, $parameters, $response);
}
Basically my change replaces '.html.' part in template name by providing $format which setted via onKernelRequest EventListener.
don't forget to add your service definition in services.yml
services:
acme.frontend.listener.mobile_template_listener:
class: Acme\FrontendBundle\EventListener\MobileTemplateListener
arguments: ['#templating', '#templating.name_parser']
tags:
- { name: kernel.event_listener, event: kernel.view, method: onKernelView }
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
I would like to change the default behaviour of the #Template annotation which automatically renders the template named as the controller action.
So in an ArticleController.php
/**
* #Route("/new", name="article_new")
* #Method("GET")
* #Template()
*/
public function newAction()
{
// ...
return array();
}
would render Article/new.html.twig.
I want to change this to referr to the name of the route the action was called with so you could have multiple routes for an action each rendering a different template.
This is the way I currently do it (without #Template):
/**
* #Route("/new", name="article_new")
* #Route("/new_ajax", name="article_new_ajax")
* #Method("GET")
*/
public function newAction()
{
// ...
$request = $this->getRequest();
$route = $request->attributes->get('_route');
$template = 'AcmeDemoBundle:' . $route . '.html.twig';
return $this->render($template, array(
// ...
));
}
I wonder now if there is a way to change the behaviour of #Template to do exactly that. Is there a way to customize the annotations or just some aproach to make it more automated?
Any ideas?
I have now found a solution using the kernelView event. This is independet of the #Template annotation. The kernelView event fires whenever a controller action doesn't return a response object.
(This solution is based on Symfony 2.4)
event listener service:
services:
kernel.listener.route_view:
class: Acme\DemoBundle\Templating\RouteView
arguments: ["#request_stack", "#templating"]
tags:
- { name: kernel.event_listener, event: kernel.view }
event listener class:
namespace Acme\DemoBundle\Templating;
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
class RouteView
{
protected $controller;
protected $route;
protected $templating;
function __construct(RequestStack $requestStack, $templating)
{
$this->controller = $requestStack->getCurrentRequest()->attributes->get('_controller');
$this->route = $requestStack->getCurrentRequest()->attributes->get('_route');
$this->templating = $templating;
}
public function onKernelView(GetResponseForControllerResultEvent $event)
{
$controllerAction = substr($this->controller, strrpos($this->controller, '\\') + 1);
$controller = str_replace('Controller', '', substr($controllerAction, 0, strpos($controllerAction, '::')));
$template = 'AcmeDemoBundle:' . $controller . ':' . str_replace(strtolower($controller) . '_', '', $this->route) . '.html.twig';
$response = $this->templating->renderResponse($template, $event->getControllerResult());
$event->setResponse($response);
}
}
Now the controller behaves like this:
/**
* #Route("/new", name="article_new") -> Article:new.html.twig
* #Route("/new_ajax", name="article_new_ajax") -> Article:new_ajax.html.twig
* #Method("GET")
*/
public function newAction()
{
// ...
return array();
}
FOSRestBundle includes similar functionality to #Template but on class-level since my pull request if you use the #View annotation on class-level.
This can be useful if want to your template-filenames to reflect the action-names but not the route-names ( as opposed to what was asked for in the question ).
The rendered template will be i.e. ...
<controller-name>/<action-name>.html.twig
... for HTML views.
Example: AcmeBundle\Controller\PersonController::create() will render
AcmeBundle/Resources/views/Person/create.html.twig
Before the PR you had to annotate every method.
Annotating a method still gives the possibility to override template,template-variable and status-code though.
example:
/**
* #FOSRest\View(templateVar="testdata", statusCode=201)
*/
class PersonController implements ClassResourceInterface
{
public function newAction()
{
return $this->formHandler->createForm();
// template: Person/new.html.twig
// template variable is 'form'
// http status: 201
}
public function helloAction()
{
return "hello";
// template: Person/hello.html.twig
// template variable 'testdata'
// http status: 201
}
/**
* #FOSRest\View("AnotherBundle:Person:get", templatevar="person")
*/
public function getAction(Person $person)
{
return $person;
// template: AnotherBundle:Person:get
// template variable is 'person'
// http status: 201
}
/**
* #FOSRest\View("AnotherBundle:Person:overview", templatevar="persons", statusCode=200)
*/
public function cgetAction()
{
return $this->personManager->findAll();
// template: AnotherBundle:Person:overview
// template variable is 'persons'
// http status: 200
}
// ...