
What will be the controllers in PR2?
- Transfer

Symfony 2 is now in Preview Release (PR1). Judging by the number of Twitter posts by Fabien and Doctrine 2 developer Jonathan Wage from Sensio Labs, work on the framework is in full swing. For example, in recent days there have already been 4 new components that can be read about here . You can read more about the individual components in my translations about Finder and CssSelector . Also worth noting a large numberdiscussions regarding symfony 2 on google groups . In parallel with the development of the framework, the second branch of the popular ORM Doctrine and the TWIG template engine are intensively developing. All this, together with the development of PHP 5.3 itself, creates an image of such a growing technological organism, the development of which is very interesting to follow. A little later, buy a bottle of champagne, put it in a bar and look forward to the final release. Sorry a little distracted, let's follow the thoughts of the Symfony Community regarding the improvement of the controller mechanism when moving to the stage of the Symfony 2 PR2 framework (see links to the discussion at the end of the topic) and maybe even take an active part in it: the status of the RFC discussion - that is, you can also offer an interesting idea and thereby improve the framework.
Actually, this is not quite a translation, but nevertheless the main part of the material was taken from one source, so I decided to design it as a translation. The text is quite a lot, but structured and easy to read, so everything is in one topic.
Controllers
In Symfony 2, any valid callable construct can serve as a controller: a function, a class / object method, or a lambda / closure. In this topic, we will talk about the controller in the form of an object method, as the most common case.
In order to do its job (call Model to pass parameters to the View), the controller must have access to some parameters (strings) and services (objects):
- Parameters : coming from the request object (path variables (/ hello /: name), GET / POST parameters, HTTP headers) and global variables (which are processed by the Dependency Injector ).
- Services (“global” objects) received from Dependency Injector (such as a request object, User object, mail object ( Swift Mailer ), database connection, etc.)
- how easy / intuitive to create a new controller?
- how fast is its implementation?
- how easy is it to automatically test it (Unit tests)? [in the topic, I do not consider these issues, in additional links it is]
- how verbose / compact to access parameters and services?
- How does the implementation fit the separation concept and MVC design pattern?
Option 1. This is the mechanism of controllers in Symfony 2 now (PR1):
To provide access to services and parameters, Symfony inserts the container into the controller constructor (which is then stored in the protected property):
$controller = new Controller($container);
Access to options and services
When an action is executed, the method arguments are inserted through the coincidence of their names with path variables:
function showAction($slug){ ... }
The slug argument will be passed if the corresponding path has the slug: / articles /: slug variable . The transfer of path variables is as follows:
- if the argument name matches the path variable name, we use this value ($ slug in the example, even if it is not passed in the URL and the default value is defined);
- if not, and if the default value is specified for the argument, and if the argument is optional, we use the default value;
- if not, we throw an exception.
function showAction($slug)
{
// доступ к глобальным переменным происходит через контейнер
// так:
$global = $this->container->getParameter('max_per_page');
// или так:
$global = $this->container['max_per_page'];
// доступ к параметрам запроса, через сервис request
$limit = $this->container->request->getParameter('max');
// если объект запроса существует всегда, он может быть доступен через конструктор автоматически:
$limit = $this->request->getParameter('max');
}
Access to services is as follows:
function indexAction() {
// доступ довольно простой
$this->container->getUserService()->setAttribute(...);
// или более коротко через параметры контейнера
$this->container->user->setAttribute(...);
}
Advantages and disadvantages:
As obvious advantages, we can distinguish clarity, simplicity, good performance and convenient testing of containers of specific types.
Disadvantages:
- The controller is loaded with a container (entity separation);
- Pretty open access to the container, which requires accuracy from the developer;
- Access to options and services is somewhat verbose;
- Developers may start thinking in the so-called sfContext context. That is, if they have access to the container from the controller, it is easy to pass it to the model class, but this is not the best idea;
- During testing, the developer will be forced to familiarize himself with the implementation and know which services the controller has access to.
Option 2
This option differs from the previous one in working with parameters / services. Instead of passing the container to the constructor, we pass only the necessary parameters and services:
protected $user, $request, $maxPerPage;
function __construct(User $user, Request $request, $maxPerPage)
{
$this->user = $user;
$this->request = $request;
$this->maxPerPage = $maxPerPage;
}
In fact, it is not difficult to notice that the first option is to some extent a special case of this option, that is, the options are quite compatible.
Access to options and services
Access to parameters and methods is similar to the previous option, a little more brief:
function showAction($slug)
{
// если сервис определен в конструкторе, доступ более краткий
$limit = $this->request->getParameter('max');
// доступ к параметрам и сервисам прямой
$global = $this->maxPerPage;
$this->user->setAttribute(...);
}
Advantages and disadvantages:
Advantages:
- Gives great flexibility and is fully compatible with the first option;
- Enables type control when passing parameters / services;
- More clear dependencies;
- Not a big overhead code;
- The constructor requires all services and parameters (but in most cases only a few will be used) - but it is reassuring that you can use the container method in these cases;
- More boilerplate code: now the developer needs to store all transferred services in protected variables.
Option 3
Instead of inserting services into the constructor, in this option they are directly inserted into each controller method:
function showAction($slug, $userService, $doctrineManagerService, $maxPerPageParameter){ ... }
The argument can be a path variable, service, or a global parameter, while the rules for passing parameters must be clarified:- If the argument name ends with "Service", we use the appropriate service ($ userService in the example);
- If the argument name ends with "Parameter", we use the corresponding parameter with Dependency Injector ($ maxPerPageParameter in the example);
- If not, and if the argument name matches the path variable, we use it ($ slug in the example);
- In other cases, if the argument is not defined, throw an exception.
// передаем переменную пути `slug` и контейнер
function showAction($slug, Container $containerService){ ... }
// передаем Request и контейнер
function showAction(Request $requestService, Container $containerService){ ... }
Access to options and services
In this case, access to parameters and services is carried out almost directly:
function showAction($id, $userService, $doctrineManagerService) {
$user->setAttribute(...);
}
Advantages and disadvantages:
Advantages:
- Each action is independent and works autonomously;
- Inside the method, short and clear code;
- Good performance (since we ourselves analyze and analyze the arguments of the method);
- Very flexible option (you can use a full container if you want).
- If we have a large list of path variables and a list of services, the signature can be very verbose - but the fact that you can create a Request and a container in this case can be reassuring:
function showAction($year, $month, $day, $slug, $userService, $doctrineManagerService) { ... }
- Methods become similar to functions (since nothing unites them);
- Even if we pass services as arguments to a method, some of them may be optional for the method. In this case, we get even more extra code than accessing services from the container on demand.
Option 4
This option is a mixture of options 2 and 3. Services and parameters can be transferred both to the constructor and to the action methods:
protected $user;
function __construct(User $user) {
$this->user = $user;
}
function showAction($slug, $mailerService, $maxPerPageParameter){ ... }
In this option, there is no longer a problem that methods become similar to functions from PHP4. You can create global services for the class, and local for each action method. This approximation is 100% compatible with the first option, when everything is optional everywhere. You can use parameters in both the constructor and actions, or use the container created by default (or make it context sensitive).
It also allows you to emulate actions from the Symfony 1.x branch:
function showAction(Request $requestService){ ... }
Since this option is the most flexible, the documentation should contain best practices.
Option 5
Variant is an almost complete copy of Variant 4, except that path variables cannot be included in actions methods. This allows you to remove unnecessary suffixes (Parameter and Service). And access to the path variables occurs through access to the request object:
protected $user;
function __construct(User $user) {
$this->user = $user;
}
function showAction(Request $request, $mailer, $maxPerPage) {
$id = $request->getPathParameter('id');
// ...
}
Another good arrangement might be to include the Request object with the first argument (for a sequence approach).Option 6
Another alternative would be to use annotations to include options. This has not been much discussed yet, because Symfony 2 does not yet use annotations.
Advantages and disadvantages:
Advantages:
- Some third-party libraries have started using annotations (Doctrine 2, but so far optional).
- Additional code;
- PHP developers rarely use annotations;
- Annotations are not a native PHP language construct.
Useful links : RFC: Controllers in Symfony 2 and google groups discussion: part 1 , part 2 , part 3 .