What will be the controllers in PR2?

Original author: Fabien Potencier
  • Transfer
Hello! We continue to monitor the development of the Symfony 2 framework . In this topic, we will try to follow the discussion: what will be the mechanism of controllers in the new Symfony 2 (PR2) release. Under cat 6 options for constructing the controller interface model MVC.

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.)
The controller is the central part of the MVC view, therefore, special care is required when choosing the best option for its interface, and the way to pass parameters and access to services to it. The decision to choose the best interface should be made considering these issues (in order of decreasing importance):
  1. how easy / intuitive to create a new controller?
  2. how fast is its implementation?
  3. how easy is it to automatically test it (Unit tests)? [in the topic, I do not consider these issues, in additional links it is]
  4. how verbose / compact to access parameters and services?
  5. How does the implementation fit the separation concept and MVC design pattern?
Actually, this is the basis from which we will evaluate the various options for controller interfaces for Symfony 2.

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:
  1. 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);
  2. if not, and if the default value is specified for the argument, and if the argument is optional, we use the default value;
  3. if not, we throw an exception.
Access to the parameters is as follows:
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:
  1. The controller is loaded with a container (entity separation);
  2. Pretty open access to the container, which requires accuracy from the developer;
  3. Access to options and services is somewhat verbose;
  4. 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;
  5. 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:
  1. Gives great flexibility and is fully compatible with the first option;
  2. Enables type control when passing parameters / services;
  3. More clear dependencies;
  4. Not a big overhead code;
Disadvantages:
  1. 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;
  2. 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:
  1. If the argument name ends with "Service", we use the appropriate service ($ userService in the example);
  2. If the argument name ends with "Parameter", we use the corresponding parameter with Dependency Injector ($ maxPerPageParameter in the example);
  3. If not, and if the argument name matches the path variable, we use it ($ slug in the example);
  4. In other cases, if the argument is not defined, throw an exception.
If you do not want to describe all services / parameters in the method signature, you can use the container (as done in the previous version):
// передаем переменную пути `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).
Disadvantages:
  • 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).
Disadvantages:
  • Additional code;
  • PHP developers rarely use annotations;
  • Annotations are not a native PHP language construct.
Further information can be obtained from the additional links below. In the comments, it is interesting to read which option you like best, or maybe someone will offer something new. I personally like options 1 and 5.

Useful links : RFC: Controllers in Symfony 2 and google groups discussion: part 1 , part 2 , part 3 .

Also popular now: