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
}
// ...
Related
I have a single Symfony website which has 2 domains:
landing (www.landing.com)
main domain (www.main.com
I should configure it so that every request which matches the landing host (www.landing.com) redirects to the homepage. Requests on the main domain should work as usual.
Is it possible? I tried with this but it redirects only the homepage:
/**
* #Route("/", name="landing", host="www.landing.com")
*/
public function landingAction()
{
return $this->render('default/landing.html.twig');
}
/**
* #Route("/", name="homepage")
*/
public function indexAction(Request $request)
{
return $this->render('default/index.html.twig');
}
I ended up using a listener and check the host of the current page. This way I can even parameterize the host.
This is the complete code:
<?php
namespace AppBundle\EventListener;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
class LandingListener
{
private $landingPageHost;
private $router;
public function __construct($landingPageHost, $router)
{
$this->landingPageHost = $landingPageHost;
$this->router = $router;
}
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
$host = $request->getHost();
$isHomepage = 'homepage' == $request->attributes->get('_route');
// landing page domain
if (false !== stripos($host, $this->landingPageHost) && !$isHomepage) {
$url = $this->router->generate('homepage');
$response = new RedirectResponse($url);
$event->setResponse($response);
}
}
}
This is the service:
app.landing_listener:
class: AppBundle\EventListener\LandingListener
arguments: ['%landing_page_host%', '#router']
tags:
- { name: kernel.event_listener, event: kernel.request }
You could forward the request of www.landing.com to homepage route.
Tested in Symfony 3.3
Though not very common, you can also forward to another controller internally with the forward() method. Instead of redirecting the user's browser, this makes an "internal" sub-request and calls the defined controller.
The forward() method returns the Response object that is returned from that controller, just change the host with theirs:
/**
* #Route("/", name="landing", host="localhost")
*/
public function landingAction()
{
// return $this->render('default/landing.html.twig');
$response = $this->forward('AppBundle:Default:index');
//further modify the response or return it directly
return $response;
}
/**
* #Route("/", name="homepage")
*/
public function indexAction(Request $request)
{
return $this->render('default/index.html.twig');
}
Even , you can pass arguments to the resulting controller:
$response = $this->forward('AppBundle:Something:fancy', array(
'name' => $name,
'color' => 'green',
))
Reference: https://symfony.com/doc/3.3/controller/forwarding.html
I need replace string in view after rendered.
My all controllere use annotaion #Template("path").
My controller:
...
class AboutController extends Controller
{
/**
* #Route("/about-us", name="about")
* #Method("GET")
* #Template("#AppBundle/Resources/views/About/index.html.twig")
*/
public function indexAction()
{
}
}
...
I know to do it without annotaion:
...
class AboutController extends Controller
{
/**
* #Route("/about-us", name="about")
* #Method("GET")
*/
public function indexAction()
{
$content = $this->renderView('AppBundle/Resources/views/About/index.html.twig', []);
$content = str_replace('my text', 'my new text', $content);
return new Response($content);
}
}
...
How I can do it with annotaion (#template)?
I think you should use Symfony's Event system onKernelResponse
This will allow you to grab the response after controller action return it and modify the response before sending it.
To subscribe an event follow Syfmony's doc example.
You did not tell us which version of Symfony you are using, those links are 3.4.
Hope this helps.
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);
}
}
}
I'm trying to set up a Contact Form and all is going well. Set up my Controller with ->send(), all works fine (takes a bit of time). When I set it up to work with ->queue(), seems to work fine (no delay), job is set up, mail is sent when I dispatch. But this time my mail template does not include the data sent to the Mailer.
My Controller:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Mail\Contact;
use Illuminate\Support\Facades\Mail;
class PagesController extends Controller
{
public function sendContact(Request $request)
{
Mail::to('webform#email.com')
->queue(new Contact($request));
return redirect('/contact')->with('status', 'Message sent. Thanks!');
}
}
My Mailer (App\Mail\Contact):
class Contact extends Mailable
{
use Queueable, SerializesModels;
/**
* Create a new message instance.
*
* #return void
*/
public function __construct()
{
}
/**
* Build the message.
*
* #return $this
*/
public function build(Request $request)
{
$subject = 'Web Message from: ' . $request->name;
return $this->from('myemail#email.com')
->subject($subject)
->view('emails.contact-template')
->with([
'name' =>$request->name,
'email' => $request->email,
'message' => $request->message,
'date' => $request->date,
]);
}
}
The problem was that I needed to declare the variables as public. Below is the solution that eventually worked:
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Http\Request;
use Illuminate\Contracts\Queue\ShouldQueue;
class Contact extends Mailable
{
use Queueable, SerializesModels;
public $request;
public $name;
public $from;
/**
* Create a new message instance.
*
* #return void
*/
public function __construct(Request $request)
{
$this->request = $request->all();
$this->name = $request->name;
}
/**
* Build the message.
*
* #return $this
*/
public function build()
{
$subject = 'Webform messsage from: ' . $this->name;
$from = 'webform#mail.com';
return $this
->from( $from )
->subject($subject)
->view('emails.contact-template');
}
}
How can one access the Request object inside Twig Extension?
namespace Acme\Bundle\Twig;
use Twig_SimpleFunction;
class MyClass extends \Twig_Extension
{
public function getFunctions()
{
return array(
new Twig_SimpleFunction('xyz', function($param) {
/// here
$request = $this->getRequestObject();
})
);
}
public function getName() {
return "xyz";
}
}
As requested in the comments, here's the prefered way of injecting a request into any service. It works with Symfony >= 2.4.
Injecting the request and putting our service in the request scope is no longer recommended. We should use the request stack instead.
namespace AppBundle\Twig;
use Symfony\Component\HttpFoundation\RequestStack;
class MyClass extends \Twig_Extension
{
private $requestStack;
public function __construct(RequestStack $requestStack)
{
$this->requestStack = $requestStack;
}
public function getFunctions()
{
$requestStack = $this->requestStack;
return array(
new \Twig_SimpleFunction('xyz', function($param) use ($requestStack) {
$request = $requestStack->getCurrentRequest();
})
);
}
public function getName()
{
return "xyz";
}
}
app/config/services.yml
app.twig_extension:
class: AppBundle\Twig\MyExtension
arguments:
- '#request_stack'
tags:
- { name: twig.extension }
Docs:
the request stack API
the request stack announcement
Register your extension as a service and give it the container service:
# services.yml
services:
sybio.twig_extension:
class: %sybio.twig_extension.class%
arguments:
- #service_container
tags:
- { name: twig.extension, priority: 255 }
Then retrieve the container by your (twig extension) class constructor and then the request:
<?php
// Your class file:
// ...
class MyClass extends \Twig_Extension
{
/**
* #var ContainerInterface
*/
protected $container;
/**
* #var Request
*/
protected $request;
/**
* Constructor
*
* #param ContainerInterface $container
*/
public function __construct($container)
{
$this->container = $container;
if ($this->container->isScopeActive('request')) {
$this->request = $this->container->get('request');
}
}
// ...
Note that testing the scope is usefull because there is no request when running console command, it avoids warnings.
That's it, you are able to use the request !
I would suggest setting 'needs_environment' => true for your Twig_SimpleFunction, which then will add \Twig_Environment as first argument of your function. Then in your function you can find the request like this:
$request = $twig->getGlobals()['app']->getRequest();
So the whole function will look like this:
...
public function getFunctions() {
return [
new \Twig_SimpleFunction('xyz', function(\Twig_Environment $env) {
$request = $twig->getGlobals()['app']->getRequest();
}, [
'needs_environment' => true,
]),
];
}
...