An AJAX request to one of my controller actions currently returns the full page HTML.
I only want it to return the HTML (.phtml contents) for that particular action.
The following code poorly solves the problem by manually disabling the layout for the particular action:
$viewModel = new ViewModel();
$viewModel->setTerminal(true);
return $viewModel;
How can I make my application automatically disable the layout when an AJAX request is detected? Do I need to write a custom strategy for this? Any advice on how to do this is much appreciated.
Additionally, I've tried the following code in my app Module.php - it is detecting AJAX correctly but the setTerminal() is not disabling the layout.
public function onBootstrap(EventInterface $e)
{
$application = $e->getApplication();
$application->getEventManager()->attach('route', array($this, 'setLayout'), 100);
$this->setApplication($application);
$this->initPhpSettings($e);
$this->initSession($e);
$this->initTranslator($e);
$this->initAppDi($e);
}
public function setLayout(EventInterface $e)
{
$request = $e->getRequest();
$server = $request->getServer();
if ($request->isXmlHttpRequest()) {
$view_model = $e->getViewModel();
$view_model->setTerminal(true);
}
}
Thoughts?
Indeed the best thing would be to write another Strategy. There is a JsonStrategy which can auto-detect the accept header to automatically return Json-Format, but as with Ajax-Calls for fullpages, there it's good that it doesn't automatically do things, because you MAY want to get a full page. Above mentioned solution you mentioned would be the quick way to go.
When going for full speed, you'd only have one additional line. It's a best practice to always return fully qualified ViewModels from within your controller. Like:
public function indexAction()
{
$request = $this->getRequest();
$viewModel = new ViewModel();
$viewModel->setTemplate('module/controller/action');
$viewModel->setTerminal($request->isXmlHttpRequest());
return $viewModel->setVariables(array(
//list of vars
));
}
I think the problem is that you're calling setTerminal() on the view model $e->getViewModel() that is responsible for rendering the layout, not the action. You'll have to create a new view model, call setTerminal(true), and return it. I use a dedicated ajax controller so there's no need of determining whether the action is ajax or not:
use Zend\View\Model\ViewModel;
use Zend\Mvc\MvcEvent;
use Zend\Mvc\Controller\AbstractActionController;
class AjaxController extends AbstractActionController
{
protected $viewModel;
public function onDispatch(MvcEvent $mvcEvent)
{
$this->viewModel = new ViewModel; // Don't use $mvcEvent->getViewModel()!
$this->viewModel->setTemplate('ajax/response');
$this->viewModel->setTerminal(true); // Layout won't be rendered
return parent::onDispatch($mvcEvent);
}
public function someAjaxAction()
{
$this->viewModel->setVariable('response', 'success');
return $this->viewModel;
}
}
and in ajax/response.phtml simply the following:
<?= $this->response ?>
Here's the best solution (in my humble opinion). I've spent almost two days to figure it out. No one on the Internet posted about it so far I think.
public function onBootstrap(MvcEvent $e)
{
$eventManager= $e->getApplication()->getEventManager();
// The next two lines are from the Zend Skeleton Application found on git
$moduleRouteListener = new ModuleRouteListener();
$moduleRouteListener->attach($eventManager);
// Hybrid view for ajax calls (disable layout for xmlHttpRequests)
$eventManager->getSharedManager()->attach('Zend\Mvc\Controller\AbstractController', MvcEvent::EVENT_DISPATCH, function(MvcEvent $event){
/**
* #var Request $request
*/
$request = $event->getRequest();
$viewModel = $event->getResult();
if($request->isXmlHttpRequest()) {
$viewModel->setTerminal(true);
}
return $viewModel;
}, -95);
}
I'm still not satisfied though. I would create a plugin as a listener and configure it via configuration file instead of onBootstrap method. But I'll let this for the next time =P
I replied to this question and seems it maybe similar - Access ViewModel variables on dispatch event
Attach an event callback to the dispatch event trigger. Once this event triggers it should allow you to obtain the result of the action method by calling $e->getResult(). In the case of an action returning a ViewModel it should allow you to do the setTerminal() modification.
aimfeld solution works for me, but in case some of you experiment issues with the location of the template, try to specify the module:
$this->viewModel->setTemplate('application/ajax/response');
The best is to use JsonModel which returns nice json and disable layout&view for you.
public function ajaxCallAction()
{
return new JsonModel(
[
'success' => true
]
);
}
I had this problem before and here is a quikc trick to solved that.
First of all, create an empty layout in your layout folder module/YourModule/view/layout/empty.phtml
You should only echo the view content in this layout this way <?php echo $this->content; ?>
Now In your Module.php set the controller layout to layout/empty for ajax request
namespace YourModule;
use Zend\Mvc\MvcEvent;
class Module {
public function onBootstrap(MvcEvent $e) {
$sharedEvents = $e->getApplication()->getEventManager()->getSharedManager();
$sharedEvents->attach(__NAMESPACE__, 'dispatch', function($e) {
if ($e->getRequest()->isXmlHttpRequest()) {
$controller = $e->getTarget();
$controller->layout('layout/empty');
}
});
}
}
public function myAjaxAction()
{
....
// View - stuff that you returning usually in a case of non-ajax requests
View->setTerminal(true);
return View;
}
Related
I am trying to add event for Laminas Framework that will fire when \Laminas\Mvc\MvcEvent::EVENT_DISPATCH is triggered. But absolutelly nothing happends, like this triggers not exists. What am I doing wrong?
This is the code under the module\Application\src\Module.php:
use Laminas\ModuleManager\ModuleManager;
use Laminas\Mvc\MvcEvent;
class Module
{
public function init(ModuleManager $moduleManager)
{
ini_set("display_errors", '1');
$eventManager = $moduleManager->getEventManager();
$eventManager->attach(MvcEvent::EVENT_DISPATCH, [$this, 'onDispatch']);
}
public function onDispatch(\Laminas\EventManager\Event $event)
{
var_dump('ok');die;
}
}
I think you need use another method in Module it's should be something like this:
use Laminas\Mvc\MvcEvent;
class Module
{
public function onBootstrap(MvcEvent $event)
{
$application = $event->getApplication();
$eventManager = $application->getEventManager();
$eventManager->attach(MvcEvent::EVENT_DISPATCH, [$this, 'onDispatch']);
}
public function onDispatch(MvcEvent $event)
{
var_dump('ok');
die;
}
}
In this case it onBootstrap. Hope help you
On init you'll need to get the shared event manager from the module manager:
<?php
use Laminas\ModuleManager\Feature\InitProviderInterface;
use Laminas\ModuleManager\ModuleManagerInterface;
use Laminas\Mvc\Application;
use Laminas\Mvc\MvcEvent;
final class Module implements InitProviderInterface
{
public function init(ModuleManagerInterface $manager): void
{
$sharedEventManager = $manager->getEventManager()->getSharedManager();
$sharedEventManager->attach(
Application::class,
MvcEvent::EVENT_DISPATCH,
function () {
var_Dump('dispatch from init');
}
);
}
}
The SharedEventManager is usually (or should be) shared between all event manager instances. This makes it possible to call or create events from other event manager instances. To differentiate between event names an identifier is used (so you can have more then one event with the same name). All MvcEvents belong to the Laminas\Mvc\Application identifier. Laminas\ModuleManager\ModuleManager has it's own EventManager instance, that is why you'll need to add the event to the SharedEventManager (init() is called by the ModuleManager and Laminas\ModuleManager\ModuleEvent is used).
onBootstrap() will be called by Laminas\Mvc\Application, that why you get the correct EventManager instance there.
As #Dimitry suggested: you should add that event in onBootstrap() as the dispatching process is part of the application and not the module manager. In init() you should only add bootstrap events.
And btw: you should use the Laminas\ModuleManager\Feature\* interfaces to make your application a bit more robust to future updates.
Using symfony 3, i have multiple controllers and a number of actions all required to render and handle the same form. I'm sure there is a simpler and easier way to do this instead of repeating the form handling code 6 times in every action in every controller.
eg
Controller 1{
action1(){
//same form handling
}
action2(){
//same form handling
}
action3(){
//same form handling
}
action4(){
//same form handling
}
}
I was wondering if anyone could enlighten me as to how to do this. Thanks
You could just add some helper methods to your Controller
private function getForm()
{
// Create form
return $this->createForm(YourType::class);
}
private function handleForm(Form $form, Request $request)
{
// Handle the form
$form->handleRequest($request);
// Do some stuff
}
maybe you can create a service that can handle the request ...
so you will create your form in your controllers, actions, handle the request from your service, and create the view in the controllers, actions again to render it.
Seams feasible to me...
Hope this will help.
[Edit]
If you don't want to create a service only for this form,
you can :
mkdir a folder Handler in your bundle
create a file xxxHandler.php
inside it create a class xxxHandler like
class xxxHandler {
public function __construct(Form $form, Request $request, EntityManager $em, $session) {
$this->form = $form;
$this->request = $request;
$this->em = $em;
$this->session = $session;
}
public function process() {
if ($this->request->getMethod() == 'POST') {
$this->form->bindRequest($this->request);
if ($this->form->isValid()) {
$this->onSuccess($this->form->getData());
return true;
}
}
return false;
}
public function onSuccess(YourEntity $entity) {
$this->em->persist($entity);
$this->em->flush();
}
}
and in your controllers
instanciate a xxxHandler and give all parameters required. (don't forget use statements)
and call method proccess() in a 'if'
something like
$form = $this->createForm(new yourType, $yourEntity);
$formHandler = new ProspectHandler($form, $this->get('request'), $em, $session);
if ($formHandler->process()) {
//do wathever you want
}
PS: this is old symfony2 methods, modify it a little bit to make it work in symfony3
I have implemented following code to run a code on before any action of any controller. However, the beforeFilter() function not redirecting to the route I have specified. Instead it takes the user to the location where the user clicked.
//My Listener
namespace Edu\AccountBundle\EventListener;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
class BeforeControllerListener
{
public function onKernelController(FilterControllerEvent $event)
{
$controller = $event->getController();
if (!is_array($controller))
{
//not a controller do nothing
return;
}
$controllerObject = $controller[0];
if (is_object($controllerObject) && method_exists($controllerObject, "beforeFilter"))
//Set a predefined function to execute Before any controller Executes its any method
{
$controllerObject->beforeFilter();
}
}
}
//I have registered it already
//My Controller
class LedgerController extends Controller
{
public function beforeFilter()
{
$commonFunction = new CommonFunctions();
$dm = $this->getDocumentManager();
if ($commonFunction->checkFinancialYear($dm) == 0 ) {
$this->get('session')->getFlashBag()->add('error', 'Sorry');
return $this->redirect($this->generateUrl('financialyear'));//Here it is not redirecting
}
}
}
public function indexAction() {}
Please help, What is missing in it.
Thanks Advance
I would suggest you follow the Symfony suggestions for setting up before and after filters, where you perform your functionality within the filter itself, rather than trying to create a beforeFilter() function in your controller that is executed. It will allow you to achieve what you want - the function being called before every controller action - as well as not having to muddy up your controller(s) with additional code. In your case, you would also want to inject the Symfony session to the filter:
# app/config/services.yml
services:
app.before_controller_listener:
class: AppBundle\EventListener\BeforeControllerListener
arguments: ['#session', '#router', '#doctrine_mongodb.odm.document_manager']
tags:
- { name: kernel.event_listener, event: kernel.controller, method: onKernelController }
Then you'll create your before listener, which will need the Symony session and routing services, as well as the MongoDB document manager (making that assumption based on your profile).
// src/AppBundle/EventListener/BeforeControllerListener.php
namespace AppBundle\EventListener;
use Doctrine\ODM\MongoDB\DocumentManager;
use Symfony\Bundle\FrameworkBundle\Routing\Router;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use AppBundle\Controller\LedgerController;
use AppBundle\Path\To\Your\CommonFunctions;
class BeforeControllerListener
{
private $session;
private $router;
private $documentManager;
private $commonFunctions;
public function __construct(Session $session, Router $router, DocumentManager $dm)
{
$this->session = $session;
$this->router = $router;
$this->dm = $dm;
$this->commonFunctions = new CommonFunctions();
}
public function onKernelController(FilterControllerEvent $event)
{
$controller = $event->getController();
if (!is_array($controller)) {
return;
}
if ($controller[0] instanceof LedgerController) {
if ($this->commonFunctions->checkFinancialYear($this->dm) !== 0 ) {
return;
}
$this->session->getFlashBag()->add('error', 'Sorry');
$redirectUrl= $this->router->generate('financialyear');
$event->setController(function() use ($redirectUrl) {
return new RedirectResponse($redirectUrl);
});
}
}
}
If you are in fact using the Symfony CMF then the Router might actually be ChainRouter and your use statement for the router would change to use Symfony\Cmf\Component\Routing\ChainRouter;
There are a few additional things here you might want to reconsider - for instance, if the CommonFunctions class needs DocumentManager, you might just want to make your CommonFunctions class a service that injects the DocumentManager automatically. Then in this service you would only have to inject your common functions service instead of the document manager.
Either way what is happening here is that we are checking that we are in the LedgerController, then checking whether or not we want to redirect, and if so we overwrite the entire Controller via a callback. This sets the redirect response to your route and performs the redirect.
If you want this check on every single controller you could simply eliminate the check for LedgerController.
.
$this->redirect() controller function simply creates an instance of RedirectResponse. As with any other response, it needs to be either returned from a controller, or set on an event. Your method is not a controller, therefore you have to set the response on the event.
However, you cannot really set a response on the FilterControllerEvent as it is meant to either update the controller, or change it completely (setController). You can do it with other events, like the kernel.request. However, you won't have access to the controller there.
You might try set a callback with setController which would call your beforeFilter(). However, you wouldn't have access to controller arguments, so you won't really be able to call the original controller if beforeFilter didn't return a response.
Finally you might try to throw an exception and handle it with an exception listener.
I don't see why making things this complex if you can simply call your method in the controller:
public function myAction()
{
if ($response = $this->beforeFilter()) {
return $response;
}
// ....
}
public function onKernelController(FilterControllerEvent $event)
{
$request = $event->getRequest();
$response = new Response();
// Matched route
$_route = $request->attributes->get('_route');
// Matched controller
$_controller = $request->attributes->get('_controller');
$params = array(); //Your params
$route = $event->getRequest()->get('_route');
$redirectUrl = $url = $this->container->get('router')->generate($route,$params);
$event->setController(function() use ($redirectUrl) {
return new RedirectResponse($redirectUrl);
});
}
Cheers !!
Assuming I have an application using a lot of AJAX requests.
Is there a way to edit Symfony behavior and autommatically call indexAjaxAction instead of indexAction when my request is AJAX made ?
I already know that I can test if a request is Ajax with the Request::isXmlHttpRequest() method but I want it to be autommatic (i.e without testing in each controllerAction).
Does a service/bundle already makes it ?
Example :
<?php
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class FooController extends Controller
{
public function indexAction($vars)
{
$request = $this->getRequest();
if($request->isXmlHttpRequest()) {
return $this->indexAjaxAction($vars);
}
// Do Stuff
}
public function indexAjaxAction($vars){ /* Do AJAX stuff */ }
}
becomes
<?php
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class FooController extends Controller
{
public function indexAction($vars) { }
public function indexAjaxAction($vars) { }
// Other functions
}
One way would be to use a slightly modified controller resolver that would be used instead of the current controller resolver in the regular KttpKernel::handleRaw process.
Please note that I may be wrong in my thinking here and it is untested.
The controller resolver class has the id controller_resolver.class which you could overwrite with your custom one in your config using
In your app/config/config.yml...
.. config stuff ..
parameters:
controller_resolver.class: Acme\SomeBundle\Controller\ControllerResolver
And then in your new ControllerResolver...
namespace Acme\SomeBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\ControllerResolver
as BaseControllerResolver;
class ControllerResolver extends BaseControllerResolver
{
/**
* {#inheritdoc
*/
public function getArguments(Request $request, $controller)
{
if (is_array($controller) && $request->isXmlHttpRequest()) {
$action = preg_replace(
'/^(.*?)Action$/',
'$1AjaxAction',
$controller[1]
);
try {
$r = new \ReflectionMethod($controller[0], $action);
return $this->doGetArguments(
$request,
$controller,
$r->getParameters()
);
} catch( \Exception $e) {
// Do nothing
}
}
return parent::getArguments($request, $controller);
}
}
This class just extends the current controller resolver and will attempt to use the youractionAjaxAction if it exists in the controller and then falls back to the regular resolver if it gets an error (method not found);
Alternatively you could just use...
if (is_array($controller) && $request->isXmlHttpRequest()) {
$controller[1] = preg_replace(
'/^(?P<action>.*?)Action$/',
'$1AjaxAction',
$controller[1]
);
}
return parent::getArguments($request, $controller);
.. which would just update the called action and then send it through to the regular resolver with no fall back, meaning that every action that could be called using an XmlHttpRequest would require a corresponding AjaxAction.
You may want to look into FOSRestBundle for Symfony, it can be very useful if you have 1 action that can either return json data or rendered html template depending on the request method
I would like set all actions because when these return a ViewModel class, this has the parameter setTerminal = true by default.
I want that my aplication have this behavior because the 90% of my calls are AJAX.
Thanks in advance.
Check the Creating and Registering Alternate Rendering and Response Strategies.
http://framework.zend.com/manual/2.0/en/modules/zend.view.quick-start.html#creating-and-registering-alternate-rendering-and-response-strategies.
namespace Application;
class Module
{
public function onBootstrap($e)
{
// Register a "render" event, at high priority (so it executes prior
// to the view attempting to render)
$app = $e->getApplication();
$app->getEventManager()->attach('render', array($this, 'registerJsonStrategy'), 100);
}
public function registerJsonStrategy($e)
{
$app = $e->getTarget();
$locator = $app->getServiceManager();
$view = $locator->get('Zend\View\View');
$jsonStrategy = $locator->get('ViewJsonStrategy');
// Attach strategy, which is a listener aggregate, at high priority
$view->getEventManager()->attach($jsonStrategy, 100);
}
}