When writing Shopware plugins, you will usually use events like Enlight_Controller_Dispatcher_ControllerPath_Frontend_Test
or Enlight_Controller_Action_PreDispatch_Frontend_Listing
. But where do those names originate from and what is the
"big picture" behind it?
This post will explain what happens "behind the scenes", when Shopware boots up and handles a request.
Dispatch Loop
Dispatching is the process of handling a Request
object, extracting the relevant Module
, Controller
and Action
from it,
instantiating the correct controller and making this controller handle the request.
As controllers can forward to other controllers, this is actually a loop - Shopware will run through that loop, until no
additional requests needs to be handled.
Where it all begins: The front controller
Whenever a request hits your server, your web server will redirect it to the shopware.php
file. There shopware will
instantiate the \Shopware\Kernel
, which implements \Symfony\Component\HttpKernel\HttpKernelInterface
. The handle
method of the kernel will then bootstrap the rest of Shopware (e.g. plugins) and start the dispatch loop.
The actual dispatch loop is handled by \Enlight_Controller_Front::dispatch
.
This front controller will take care of the following tasks:
- run the router on the request object, so the router can figure out, which
Module
,Controller
andAction
needs to be requested - run the dispatch loop, until there is nothing more to process
- within any loop iteration, trigger the
\Enlight_Controller_Dispatcher_Default::dispatch
which will take care of the rest
The front controller also emits some very early events, that might be useful in some cases:
Enlight_Controller_Front_StartDispatch
: At the very beginning, before the front controller will actually handle the requestEnlight_Controller_Front_RouteStartup
: Before the front controller will pass theRequest
object to the router in order to populate itEnlight_Controller_Front_RouteShutdown
: Just after the router handled theRequest
object. Now the routing information is availableEnlight_Controller_Front_DispatchLoopStartup
: Before the dispatch loopEnlight_Controller_Front_PreDispatch
: Before the currentRequest
object is passed to theDispatcher
Enlight_Controller_Front_PostDispatch
: After the currentRequest
object is passed to the `DispatcherEnlight_Controller_Front_DispatchLoopShutdown
: After the dispatch loop
Router
To get an impression of what the router does, let's look at this example: We open the URL http://localhost/media/living/
and look at the request object before routing:
object(Enlight_Controller_Request_RequestHttp)[379]
private 'validDeviceTypes' =>
array (size=3)
0 => string 'desktop' (length=7)
1 => string 'tablet' (length=6)
2 => string 'mobile' (length=6)
protected '_paramSources' =>
array (size=2)
0 => string '_GET' (length=4)
1 => string '_POST' (length=5)
protected '_requestUri' => string '/media/living/' (length=18)
protected '_baseUrl' => string '/media' (length=6)
protected '_basePath' => string '/media' (length=6)
protected '_pathInfo' => string '/living/' (length=12)
protected '_params' =>
array (size=0)
empty
protected '_rawBody' => null
protected '_aliases' =>
array (size=0)
empty
protected '_dispatched' => boolean false
protected '_module' => null
protected '_moduleKey' => string 'module' (length=6)
protected '_controller' => null
protected '_controllerKey' => string 'controller' (length=10)
protected '_action' => null
protected '_actionKey' => string 'action' (length=6)
As you can see, module
, controller
and action
are null
. After the routing (the router can be found in engine/Shopware/Components/Routing
)
the request
object will look like this:
object(Enlight_Controller_Request_RequestHttp)[379]
private 'validDeviceTypes' =>
array (size=3)
0 => string 'desktop' (length=7)
1 => string 'tablet' (length=6)
2 => string 'mobile' (length=6)
protected '_paramSources' =>
array (size=2)
0 => string '_GET' (length=4)
1 => string '_POST' (length=5)
protected '_requestUri' => string '/media/living/' (length=18)
protected '_baseUrl' => string '/media' (length=6)
protected '_basePath' => string '/media' (length=6)
protected '_pathInfo' => string '/living/' (length=12)
protected '_params' =>
array (size=0)
empty
protected '_rawBody' => null
protected '_aliases' =>
array (size=0)
empty
protected '_dispatched' => boolean false
protected '_module' => string 'frontend' (length=8)
protected '_moduleKey' => string 'module' (length=6)
protected '_controller' => string 'cat' (length=3)
protected '_controllerKey' => string 'controller' (length=10)
protected '_action' => string 'index' (length=5)
protected '_actionKey' => string 'action' (length=6)
This time, the relevant information is set, the router resolved the SEO URL /media/living/
to the correct module
, controller
and aciton
. Now everything is prepared to handle the request
object by the dispatcher.
Dispatcher
The Shopware dispatcher will handle a request in \Enlight_Controller_Dispatcher_Default::dispatch
. First of all, it will build
a controller name from the request object, so e.g.
[
'module' => 'frontend',
'controller' => 'listing',
'action' => 'index',
]
will become Shopware_Controllers_Frontend_Listing
. The controller dispatcher will then emit the event
Enlight_Controller_Dispatcher_ControllerPath_Frontend_Listing
, so that plugin developers can register this controller.
If no plugin returns a result, the Dispatcher will try to find this controller in Shopware's controller directory engine/Shopware/Controllers
.
Either way, the dispatcher will now have a path for the controller, e.g. engine/Shopware/Controllers/Frontend/Listing.php
which it now will include and instantiate. Furthermore it injects the Request
, Response
, Container
and Front
object into that controller.
As it uses a proxy object for the controller, all controllers are hookable by default.
Finally the dispatcher will call the dispatch
method on the controller. As every controller in Shopware extends from
Enlight_Controller_Action
, you might not have seen this method, yet - but it is always there.
Controller dispatching
Now \Enlight_Controller_Action::dispatch
will handle the actual dispatch for the current controller. First of all, an \Enlight_Controller_ActionEventArgs
object is created - it will be passed to all pre- and post dispatch events of that controller.
After that, the PreDispatch
events are triggered, for the current controller:
Enlight_Application::Instance()->Events()->notify(
__CLASS__ . '_PreDispatch',
$args
);
Enlight_Application::Instance()->Events()->notify(
__CLASS__ . '_PreDispatch_' . $moduleName,
$args
);
Enlight_Application::Instance()->Events()->notify(
__CLASS__ . '_PreDispatch_' . $this->controller_name,
$args
);
This will generate events like:
Enlight_Controller_Action_PreDispatch
Enlight_Controller_Action_PreDispatch_Frontend
Enlight_Controller_Action_PreDispatch_Frontend_Listing
depending of the current module and controller. As you see, the Enlight_Controller_Action
prefix of the event name
directly derives from the Enlight_Controller_Action
class, that emits the events. The first event does not have a module or
controller name in it - so every controller will emit it - that's why this event is always available.
The second event just appends the module name to the event name - that's the reason, why there is a Enlight_Controller_Action_PreDispatch_Frontend
event for every single controller in the frontend.
After emitting all the events, the controller dispatcher will call the preDispatch
method of the controller itself. So if
you have implemented the preDispatch
method in your controller, it will now be executed.
Next the event Enlight_Controller_Action_Frontend_Listing_Index
is emitted. This is the "action" event - if a plugin
subscribed to it, it can handle the controller action instead of the default action implementation. So instead of executing
Shopware_Controllers_Frontend_Listing::indexAction
only the plugin subscriber will be executed.
If no plugin registered to that event, the controller dispatcher will execute the actual controller action, in our case
Shopware_Controllers_Frontend_Listing::indexAction
.
Now the postDispatch
method of the controller is executed - just like the preDispatch
method before. Finally the post dispatch events
are emitted, that you will know from most plugins:
if ($this->Request()->isDispatched() && !$this->Response()->isException() && $this->View()->hasTemplate()) {
Enlight_Application::Instance()->Events()->notify(
__CLASS__ . '_PostDispatchSecure_' . $this->controller_name,
$args
);
Enlight_Application::Instance()->Events()->notify(
__CLASS__ . '_PostDispatchSecure_' . $moduleName,
$args
);
Enlight_Application::Instance()->Events()->notify(
__CLASS__ . '_PostDispatchSecure',
$args
);
}
Enlight_Application::Instance()->Events()->notify(
__CLASS__ . '_PostDispatch_' . $this->controller_name,
$args
);
Enlight_Application::Instance()->Events()->notify(
__CLASS__ . '_PostDispatch_' . $moduleName,
$args
);
Enlight_Application::Instance()->Events()->notify(
__CLASS__ . '_PostDispatch',
$args
);
As you can see, the events
- Enlight_Controller_Action_PostDispatchSecure_Frontend_Listing
- Enlight_Controller_Action_PostDispatchSecure_Frontend
- Enlight_Controller_Action_PostDispatchSecure
will only be emitted, when no exception occurred, a template is available and the request has actually been handled ("dispatched"). This is, why we call them "secure", as you as a plugin developer can rely on this events indicating a "proper" dispatch and don't need to check for e.g. exceptions by yourself. That is the reason, why we highly recommend this events over the non-secure events, which will be emitted afterwards:
- Enlight_Controller_Action_PostDispatch_Frontend_Listing
- Enlight_Controller_Action_PostDispatch_Frontend
- Enlight_Controller_Action_PostDispatch
Where does the template come from?
At this point, we have seen the whole dispatching in Shopware. Perhaps you will ask yourself, where the template comes into
play and when it is rendered. This is something \Enlight_Controller_Plugins_ViewRenderer_Bootstrap
will take care of.
At the end of a request (PostDispatch), it will get the \Enlight_View
object from the controller, render the template and
append the result to the response object. The output of e.g. the rendered template will then happen in shopware.php
, where it all started
in the \Symfony\Component\HttpFoundation\Response::send
method.
Understanding the {action} plugin
As you might know, Shopware provides a nice Smarty plugin called action
, that will allow you, to render results from other
controller calls into your template. If an HTTP cache is enabled, this will happen using ESI tags.
If no HTTP cache is enabled, Shopware is able to handle those requests without performing an additional HTTP request - it
will just enter a second dispatch loop and will make it handle the sub request. The resulting template of that sub request
can then be just printed out, so that it is included in the surrounding main request.
This way, Shopware does not need to run a full bootstrapping process again - all resources like plugins, services in the DI container
or the database connection are shared.
Symfony and Enlight
As you have seen, the actual bootstrapping of Shopware happens with a Symfony Kernel, that's why we are "Symfony http kernel compliant" and can e.g. use the Symfony HTTP revers proxy for caching. Internally, Shopware will convert the Symfony request object to an Enlight request object, handle it in the dispatch loop and convert the resulting Enlight response object back to a Symfony response object. As Enlight is basically a thin layer above the Zend framework, the dispatch loop is heavily inspired by zend framework, the Zend controller basics might provide you additional insights.