Symfony Internals
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.
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 ofsfContext::createInstance()
(but remember it has to extendsfContext
).
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).
Factory loading happens upon
sfContext
initialization. SeesfContext::initialize()
andsfContext::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!
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.
The
request.filter_parameters
event comes with a lot of information about the request, see thesfWebRequest::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.
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.
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()
.
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 (seesf_error_404_module
andsf_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.
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 thegenerator.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 )); }
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
.
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 withsfContext::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.
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.
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 ansfConfigurationException
to be thrown. This should be used when disabling a module permanently. In the second case, via themod_$moduleName_enabled
setting, a disabled module will cause the application to forward to the disabled module (see thesf_module_disabled_module
andsf_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.
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:
It is strongly advised that you add your own filters between the
security
and thecache
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.
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 inindexSuccess.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.
Built-in view names are:
ALERT
,ERROR
,INPUT
,NONE
andSUCCESS
. 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
.
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.
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.
インデックス
Document Index
関連ページリスト
Related Pages
- Introduction
- Advanced Routing
- Enhance your Productivity
- Emails
- Custom Widgets and Validators
- Advanced Forms
- Extending the Web Debug Toolbar
- Advanced Doctrine Usage
- Taking Advantage of Doctrine Table Inheritance
- Symfony Internals
- Windows and symfony
- Developing for Facebook
- Leveraging the Power of the Command Line
- Playing with symfony's Config Cache
- Working with the symfony Community
- Appendix A - JavaScript code for sfWidgetFormGMapAddress
- About the Authors
- Appendix B - Custom Installer Example
- Appendix C - License
日本語ドキュメント
Japanese Documents
- 2012/07/04 Dia 17: Busca
- 2012/06/26 Giorno 15: Web Service
- 2012/06/26 Giorno 3: Il ~Modello dei dati~
- 2012/06/26 Day 15: Web Services
- 2012/06/26 Dia 3: O Modelo de Dados