by Geoffrey Bachelet
Have you ever wondered what happens to a HTTP request when it reaches a symfony application? If yes, then you are in the right place. This chapter will explain in depth how symfony processes each request in order to create and return the response. Of course, just describing the process would lack a bit of fun, so we'll also have a look at some interesting things you can do and where you can interact with this process.
The Bootstrap
It all begins in your application's controller. Say you have a frontend
controller with a dev
environment (a very classic start for any symfony
project). In this case, you'll end up with a front controller located at
web/frontend_dev.php
.
What exactly happens in this file? In just a few lines of code, symfony
retrieves the application configuration and creates an instance of
sfContext
, which is responsible for dispatching the request. The application
configuration is necessary when creating the sfContext
object, which is
the application-dependent engine behind symfony.
tip
Symfony already gives you quite a bit of control on what happens here allowing
you to pass a custom root directory for your application as the fourth
argument of ProjectConfiguration::getApplicationConfiguration()
as well as
a custom context class as the third (and last) argument of
sfContext::createInstance()
(but remember it has to extend sfContext
).
Retrieving the application's configuration is a very important step. First,
sfProjectConfiguration
is responsible for guessing the application's
configuration class, usually ${application}Configuration
, located in
apps/${application}/config/${application}Configuration.class.php
.
sfApplicationConfiguration
actually extends ProjectConfiguration
, meaning
that any method in ProjectConfiguration
can be shared between all applications.
This also means that sfApplicationConfiguration
shares its constructor
with both ProjectConfiguration
and sfProjectConfiguration
. This is
fortunate since much of the project is configured inside the
sfProjectConfiguration
constructor. First, several useful values are
computed and stored, such as the project's root directory and the symfony
library directory. sfProjectConfiguration
also creates a new event
dispatcher of type sfEventDispatcher
, unless one was passed as the fifth
argument of ProjectConfiguration::getApplicationConfiguration()
in the
front controller.
Just after that, you are given a chance to interact with the configuration
process by overriding the setup()
method of ProjectConfiguration
. This
is usually the best place to enable / disable plugins (using
sfProjectConfiguration::setPlugins()
,
sfProjectConfiguration::enablePlugins()
,
sfProjectConfiguration::disablePlugins()
or
sfProjectConfiguration::enableAllPluginsExcept()
).
Next the plugins are loaded by sfProjectConfiguration::loadPlugins()
and the developer has a chance to interact with this process through the
sfProjectConfiguration::setupPlugins()
that can be overriden.
Plugin initialization is quite straight forward. For each plugin, symfony
looks for a ${plugin}Configuration
(e.g. sfGuardPluginConfiguration
) class
and instantiates it if found. Otherwise, sfPluginConfigurationGeneric
is
used. You can hook into a plugin's configuration through two methods:
${plugin}Configuration::configure()
, before autoloading is done${plugin}Configuration::initialize()
, after autoloading
Next, sfApplicationConfiguration
executes its configure()
method,
which can be used to customize each application's configuration before
the bulk of the internal configuration initialization process begins in
sfApplicationConfiguration::initConfiguration()
.
This part of symfony's configuration process is responsible for many things
and there are several entry points if you want to hook into this process.
For example, you can interact with the autoloader's configuration
by connecting to the autoload.filter_config
event. Next, several very
important configuration files are loaded, including settings.yml
and
app.yml
. Finally, a last bit of plugin configuration is available through
each plugin's config/config.php
file or configuration class's initialize()
method.
If sf_check_lock
is activated, symfony will now check for a lock file (the
one created by the project:disable
task, for example). If the lock is found,
the following files are checked and the first available is included, followed
immediately by termination of the script:
apps/${application}/config/unavailable.php
,config/unavailable.php
,web/errors/unavailable.php
,lib/vendor/symfony/lib/exception/data/unavailable.php
,
Finally, the developer has one last chance to customize the application's
initialization through the sfApplicationConfiguration::initialize()
method.
Bootstrap and configuration summary
- Retrieval of the application's configuration
ProjectConfiguration::setup()
(define your plugins here)- Plugins are loaded
${plugin}Configuration::configure()
${plugin}Configuration::initialize()
ProjectConfiguration::setupPlugins()
(setup your plugins here)${application}Configuration::configure()
autoload.filter_config
is notified- Loading of
settings.yml
andapp.yml
${application}Configuration::initialize()
- Creation of an
sfContext
instance
sfContext
and Factories
Before diving into the dispatch process, let's talk about a vital part of the symfony workflow: the factories.
In symfony, factories are a set of components or classes that your
application relies on. Examples of factories are logger
, i18n
, etc.
Each factory is configured via factories.yml
, which is compiled by a
config handler (more on config handlers later) and converted into PHP
code that actually instantiates the factory objects (you can view this
code in your cache in the
cache/frontend/dev/config/config_factories.yml.php
file).
note
Factory loading happens upon sfContext
initialization. See
sfContext::initialize()
and sfContext::loadFactories()
for more information.
At this point, you can already customize a large part of symfony's behavior
just by editing the factories.yml
configuration. You can even replace symfony's
built-in factory classes with your own!
note
If you're interested in knowing more about factories,
The symfony reference book
as well as the
factories.yml
file itself are invaluable resources.
If you looked at the generated config_factories.yml.php
, you may have
noticed that factories are instantiated in a certain order. That order is
important since some factories are dependent on others (for example, the
routing
component obviously needs the request
to retrieve the information
it needs).
Let's talk in greater details about the request
. By default, the
sfWebRequest
class represents the request
. Upon instantiation,
sfWebRequest::initialize()
is called, which gathers relevant information such as the GET / POST parameters
as well as the HTTP method. You're then given an opportunity to add your own
request processing through the request.filter_parameters
event.
Using the request.filter_parameter
event
Let's say you're operating a website exposing a public API to your users. The API
is available through HTTP, and each user wanting to use it must provide a valid
API key through a request header (for example X_API_KEY
) to be validated by
your application. This can be easily achieved using the
request.filter_parameter
event:
class apiConfiguration extends sfApplicationConfiguration { public function configure() { // ... $this->dispatcher->connect('request.filter_parameters', array( $this, 'requestFilterParameters' )); } public function requestFilterParameters(sfEvent $event, $parameters) { $request = $event->getSubject(); $api_key = $request->getHttpHeader('X_API_KEY'); if (null === $api_key || false === $api_user = Doctrine_Core::getTable('ApiUser')->findOneByToken($api_key)) { throw new RuntimeException(sprintf('Invalid api key "%s"', $api_key)); } $request->setParameter('api_user', $api_user); return $parameters; } }
You will then be able to access your API user from the request:
public function executeFoobar(sfWebRequest $request) { $api_user = $request->getParameter('api_user'); }
This technique can be used, for example, to validate webservice calls.
note
The request.filter_parameters
event comes with a lot of information about
the request, see the
sfWebRequest::getRequestContext()
method for more information.
The next very important factory is the routing. Routing's initialization is
fairly straightforward and consists mostly of gathering and setting specific
options. You can, however, hook up to this process through the
routing.load_configuration
event.
note
The routing.load_configuration
event gives you access to the current
routing object's instance (by default,
sfPatternRouting
).
You can then manipulate registered routes through a variety of methods.
routing.load_configuration
event usage example
For example, you can easily add a route:
public function setup() { // ... $this->dispatcher->connect('routing.load_configuration', array( $this, 'listenToRoutingLoadConfiguration' )); } public function listenToRoutingLoadConfiguration(sfEvent $event) { $routing = $event->getSubject(); if (!$routing->hasRouteName('my_route')) { $routing->prependRoute('my_route', new sfRoute( '/my_route', array('module' => 'default', 'action' => 'foo') )); } }
URL parsing occurs right after initialization, via the
sfPatternRouting::parse()
method. There are quite a few methods involved, but it suffices to say that by the
time we reach the end of the parse()
method, the correct route has been found,
instantiated and bound to relevant parameters.
note
For more information about routing, please see the Advanced Routing
chapter of this
book.
Once all factories have been loaded and properly setup, the
context.load_factories
event is triggered. This event is important since
it's the earliest event in the framework where the developer has access to all
of symfony's core factory objects (request, response, user, logging, database,
etc.).
This is also the time to connect to another very useful event:
template.filter_parameters
. This event occurs whenever a file is rendered by
sfPHPView
and allows the developer to control the parameters actually passed to the template.
sfContext
takes advantage of this event to add some useful parameters to each
template (namely, $sf_context
, $sf_request
, $sf_params
, $sf_response
and $sf_user
).
You can connect to the template.filter_parameters
event in order to add
additional custom global parameters to all templates.
Taking advantage of the template.filter_parameters
event
Say you decide that every single template you use should have access to a
particular object, say a helper object. You would then add the following
code to ProjectConfiguration
:
public function setup() { // ... $this->dispatcher->connect('template.filter_parameters', array( $this, 'templateFilterParameters' )); } public function templateFilterParameters(sfEvent $event, $parameters) { $parameters['my_helper_object'] = new MyHelperObject(); return $parameters; }
Now every template has access to an instance of MyHelperObject
through
$my_helper_object
.
sfContext
summary
- Initialization of
sfContext
- Factory loading
- Events notified:
- Global templates parameters added
A Word on Config Handlers
Config handlers are at the heart of symfony's configuration system. A config
handler is tasked with understanding the meaning behind a configuration
file. Each config handler is simply a class that is used to translate a set of
yaml configuration files into a block of PHP code that can be executed as
needed. Each configuration file is assigned to one specific config handler in the
config_handlers.yml
file.
To be clear, the job of a config handler is not to actually parse the yaml
files (this is handled by sfYaml
). Instead each config handler creates a set
of PHP directions based on the YAML information and saves those directions
to a PHP file, which can be efficiently included later. The compiled
version of each YAML configuration file can be found in the cache directory.
The most commonly used config handler is most certainly
sfDefineEnvironmentConfigHandler
,
which allows for environment-specific configuration settings.
This config handler takes care to fetch only the configuration settings
of the current environment.
Still not convinced? Let's explore
sfFactoryConfigHandler
.
This config handler is used to compile factories.yml
, which is one of the
most important configuration file in symfony. This config handler is very
particular since it converts a YAML configuration file into the PHP code that
ultimately instantiate the factories (the all-important components we talked about
earlier). Not your average config handler, is it?
The Dispatching and Execution of the Request
Enough said about factories, let's get back on track with the dispatch process.
Once sfContext
is finished initializing, the final step is to call the
controller's dispatch()
method,
sfFrontWebController::dispatch()
.
The dispatch process itself in symfony is very simple. In fact,
sfFrontWebController::dispatch()
simply pulls the module and action names
from the request parameters and forwards the application via
sfController::forward()
.
note
At this point, if the routing could not parse any module name or action name
from the current url, an
sfError404Exception
is
raised, which will forward the request to the error 404 handling module (see
sf_error_404_module
and
sf_error_404_action
).
Note that you can raise such an exception from anywhere in your application to
achieve this effect.
The forward
method is responsible for a lot of pre-execution checks as
well as preparing the configuration and data for the action to be executed.
First the controller checks for the presence of a
generator.yml
file for the current module. This check is performed first (after some basic
module / action name cleanup) because the generator.yml
config file (if
it exists) is responsible for generating the base actions class for the module
(through its
config handler, sfGeneratorConfigHandler
).
This is needed for the next step, which checks if the module and action
exists. This is delegated to the controller, through
sfController::actionExists()
,
which in turn calls the
sfController::controllerExists()
method. Here again, if the actionExists()
method fails, an sfError404Exception
is raised.
note
The
sfGeneratorConfigHandler
is
a special config handler that takes care of instantiating the right generator
class for your module and executing it. For more information about config
handlers, see A word on config handler in this chapter.
Also, for more information about the generator.yml
, see
chapter 6 of the symfony Reference Book.
There's not much you can do here besides overriding the
sfApplicationConfiguration::getControllerDirs()
method in the application's configuration class. This method returns an array
of directories where the controller files live, with an additional parameter
to tell symfony if it should check whether controllers in each directory are
enabled via the sf_enabled_modules
configuration option from settings.yml
.
For example, getControllerDirs()
could look something like this:
/** * Controllers in /tmp/myControllers won't need to be enabled * to be detected */ public function getControllerDirs($moduleName) { return array_merge(parent::getControllerDirs($moduleName), array( '/tmp/myControllers/'.$moduleName => false )); }
note
If the action does not exist, an sfError404Exception
is thrown.
The next step is to retrieve an instance of the controller containing the action.
This is handled via the
sfController::getAction()
method, which, like actionExists()
is a facade for the
sfController::getController()
,
method. Finally, the controller instance is added to the action stack
.
note
The action stack is a FIFO (First In First Out) style stack which holds all
actions executed during the request. Each item within the stack is wrapped in
an sfActionStackEntry
object. You can always access the stack with
sfContext::getInstance()->getActionStack()
or
$this->getController()->getActionStack()
from within an action.
After a little more configuration loading, we'll be ready to execute our action.
The module-specific configuration must still be loaded, which can be
found in two distinct places. First symfony looks for the module.yml
file
(normally located in apps/frontend/modules/yourModule/config/module.yml
)
which, because it's a YAML config file, uses the config cache. Additionally,
this configuration file can declare the module as internal, using the
mod_yourModule_is_internal
setting which will cause the request to fail at
this point since an internal module cannot be called publicly.
note
Internal modules were formerly used to generate email content (through
getPresentationFor()
, for example). You should now use other techniques,
such as partial rendering ($this->renderPartial()
) instead.
Now that module.yml
is loaded, it's time to check for a second time that the
current module is enabled. Indeed, you can set the mod_$moduleName_enabled
setting to false
if you want to disable the module at this point.
note
As mentioned, there are two different ways of enabling or disabling a module.
The difference is what happens when the module is disabled. In the first case,
when the sf_enabled_modules
setting is checked, a disabled module will cause
an
sfConfigurationException
to be thrown. This should be used when disabling a module permanently. In
the second case, via the mod_$moduleName_enabled
setting, a disabled
module will cause the application to forward to the disabled module (see the
sf_module_disabled_module
and
sf_module_disabled_action
settings). You should use this when you want to temporarily disable a module.
The final opportunity to configure a module lies in the config.php
file
(apps/frontend/modules/yourModule/config/config.php
) where you can place
arbitrary PHP code to be run in the context of the
sfController::forward()
method (that is, you have access to the sfController
instance via the $this
variable, as the code is literally run inside the sfController
class).
The Dispatching Process Summary
sfFrontWebController::dispatch()
is calledsfController::forward()
is called- Check for a
generator.yml
- Check if the module / action exists
- Retrieve a list of controllers directories
- Retrieve an instance of the action
- Load module configuration through
module.yml
and/orconfig.php
The Filter Chain
Now that all the configuration has been done, it's time to start the real work. Real work, in this particular case, is the execution of the filter chain.
note
Symfony's filter chain implements a design pattern known as chain of responsibility. This is a simple yet powerful pattern that allows for chained actions, where each part of the chain is able to decide whether or not the chain should continue execution. Each part of the chain is also able to execute both before and after the rest of the chain's execution.
The configuration of the filter chain is pulled from the current module's
filters.yml
,
which is why the action instance is needed. This is your chance to modify the
set of filters executed by the chain. Just remember that the rendering filter
should always be the first in the list (we will see why later). The default
filters configuration is as follow:
note
It is strongly advised that you add your own filters between the security
and the cache
filter.
The Security Filter
Since the rendering
filter waits for everyone to be done before doing anything, the
first filter that actually gets executed is the security
filter. This filter
ensures that everything is right according to the
security.yml
configuration file. Specifically, the filter forwards an unauthenticated user
to the login
module / action and a user with insufficient credentials to
the secure
module / action. Note that this filter is only executed if
security is enabled for the given action.
The Cache Filter
Next comes the cache
filter. This filter takes advantage of its ability to
prevent further filters from being executed. Indeed, if the cache is activated,
and if we have a hit, why even bother executing the action? Of course, this
will work only for a fully cacheable page, which is not the case for the vast
majority of pages.
But this filter has a second bit of logic that gets executed after the
execution filter, and just before the rendering filter. This code is
responsible for setting up the right HTTP cache headers, and placing the page
into the cache if necessary, thanks to the
sfViewCacheManager::setPageCache()
method.
The Execution Filter
Last but not least, the execution
filter will, finally, take care of
executing your business logic and handling the associated view.
Everything starts when the filter checks the cache for the current action. Of
course, if we have something in the cache, the actual action execution is
skipped and the Success
view is then executed.
If the action is not found in the cache, then it is time to execute the
preExecute()
logic of the controller, and finally to execute the action
itself. This is accomplished by the action instance via a call to
sfActions::execute()
.
This method doesn't do much: it simply checks that the action is callable, then calls
it. Back in the filter, the postExecute()
logic of the action is now executed.
note
The return value of your action is very important, since it will determine
what view will get executed. By default, if no return value is found,
sfView::SUCCESS
is assumed (which translates to, you guessed it, Success
, as in
indexSuccess.php
).
One more step ahead, and it's view time. The filter checks for two special
return values that your action may have returned, sfView::HEADER_ONLY
and sfView::NONE
. Each does exactly what their names say: sends HTTP
headers only (internally handled via
sfWebResponse::setHeaderOnly()
)
or skips rendering altogether.
note
Built-in view names are: ALERT
, ERROR
, INPUT
, NONE
and SUCCESS
. But you can
basically return anything you want.
Once we know that we do want to render something, we're ready to get into the final step of the filter: the actual view execution.
The first thing we do is retrieve an sfView
object through the sfController::getView()
method. This object can come from
two different places. First you could have a custom view object for this specific action
(assuming the current module/action is, let's keep it simple,
module/action) actionSuccessView
or module_actionSuccessView
in a file
called apps/frontend/modules/module/view/actionSuccessView.class.php
.
Otherwise, the class defined in the mod_module_view_class
configuration
entry will be used. This value defaults to sfPHPView
.
tip
Using your own view class gives you a chance to run some view specific logic,
through the sfView::execute()
method. For example, you could instantiate your own template engine.
There are three rendering modes possible for rendering the view:
sfView::RENDER_NONE
": equivalent tosfView::NONE
, this cancels any rendering from being actually, well, rendered.sfView::RENDER_VAR
: populates the action's presentation, which is then accessible through its stack entry'ssfActionStackEntry::getPresentation()
method.sfView::RENDER_CLIENT
, the default mode, will render the view and feed the response's content.
note
Indeed, the rendering mode is used only through the
sfController::getPresentationFor()
method that returns the rendering for a
given module / action
The Rendering Filter
We're almost done now, just one very last step. The filter chain has almost
finished executing, but do you remember the rendering filter? It's been waiting
since the beginning of the chain for everyone to complete their work so that it can
do its own job. Namely, the rendering filter sends the response content to the browser, using
sfWebResponse::send()
.
Summary of the filter chain execution
- The filter chain is instantiated with configuration from the
filters.yml
file - The
security
filter checks for authorizations and credentials - The
cache
filter handles the cache for the current page - The
execution
filter actually executes the action - The
rendering
filter send the response throughsfWebResponse
Global Summary
- Retrieval of the application's configuration
- Creation of an
sfContext
instance - Initialization of
sfContext
- Factories loading
- Events notified:
request.filter_parameters
routing.load_configuration
context.load_factories
- Global templates parameters added
sfFrontWebController::dispatch()
is calledsfController::forward()
is called- Check for a
generator.yml
- Check if the module / action exists
- Retrieve a list of controllers directories
- Retrieve an instance of the action
- Load module configuration through
module.yml
and/orconfig.php
- The filter chain is instantiated with configuration from the
filters.yml
file - The
security
filter checks for authorizations and credentials - The
cache
filter handles the cache for the current page - The
execution
filter actually executes the action - The
rendering
filter send the response throughsfWebResponse
Final Thoughts
That's it! The request has been handled and we're now ready for another one. Of course, we could write an entire book about symfony's internal processes, so this chapter serves only as an overview. You are more than welcome to explore the source by yourself - it is, and always will be, the best way to learn the true mechanics of any framework or library.
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.