
Hello! We continue to follow 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 release of Symfony 2 (PR2). Under the cut there are 6 options for building the interface of the controller model MVC.
Symfony 2 is currently in the Preview Release (PR1) stage. Judging by the number of messages on
Twitter Fabien and developer Doctrine 2
Jonathan Wage from Sensio Labs, the work on the framework is in full swing. For example, in recent days, as many as 4 new components have appeared that can be found
here . More details about the individual components can be found in my translations about the
Finder and
CssSelector . Also worth noting is the large number of
discussions related to symfony 2 on google groups . In parallel with the development of the framework, the second branch of the popular
Doctrine ORM and the
TWIG template engine is
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 in a bar and look forward to the final release. Sorry, a little distracted, let's follow the thoughts of the Symfony Community about improving the mechanism of controllers during the transition 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: RFC discussion status propose an interesting idea and thereby improve the framework.
In fact, this is not quite a translation, but still the main part of the material is taken from one source, so I decided to issue it as a translation. There is a lot of text, but it is structured and easy to read, so everything is in one topic.
Controllers
In Symfony 2, the controller can be any valid callable construct: a function, a class / object method, or a lambda / closure. In this topic we will discuss the controller as an object method, as the most common case.
')
In order to do its work (call the Model to pass parameters to the View), the controller must have access to some parameters (strings) and services (objects):
- Parameters : requests coming from the object (path variables (/ hello /: name), GET / POST parameters, HTTP headers) and global variables (which are processed by Dependency Injector ).
- Services (“global” objects) received from a Dependency Injector (such as a request object, User object, email object ( Swift Mailer ), database connection, etc.)
The controller is the central part of the MVC view, so special care is needed when choosing the best version of its interface, and the path for passing parameters and access to services to it. The decision to choose the best interface option should be made considering these issues (in order of decreasing importance):
- How easy / intuitive to create a new controller?
- how fast is its implementation?
- How easy is it to test it automatically (unit tests)? [in the topic I do not consider these questions, in the additional links it is]
- How verbose / compact to access parameters and services?
- How does the implementation meet the concept of separation and the MVC design pattern?
As a matter of fact, this is the basis from the point of view of which we will evaluate various options for the 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 a container into the controller's constructor (which is then stored in the protected property):
$controller = new Controller($container);
Access to parameters and services
When an action is executed, the method's arguments are inserted by matching their names with the path variables:
function showAction($slug){ ... }
The slug argument will be passed if the corresponding path has the variable slug:
/ articles /: slug .
Passing path variables is this:
- if the name of the argument matches the name of the path variable, we use this value ($ slug in the example, even if it is not passed to the URL and the default value is defined);
- if not, and if the default value for the argument is defined, and if the argument is optional, we use the default value;
- 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 one can single out clarity, simplicity, good speed and convenient testing of containers of specific types.
Disadvantages:
- The controller is loaded with a container (separation of entities);
- Enough open access to the container, which requires accuracy from the developer;
- Access to parameters and services is somewhat verbose;
- Developers can start thinking in the so-called sfContext context. That is, if they have access to the container from the controller, it is easy to transfer it to the model class, but this is not the best idea;
- When testing, the developer will be forced to familiarize himself with the implementation and to know which services the controller has access to.
Option 2
This option is different from the previous work with the parameters / services. Instead of transferring to the container 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 parameters and services
Access to parameters and methods is similar to the previous version, 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;
- Gives you the ability to control types when passing parameters / services;
- Clearer dependencies;
- Not a big invoice code;
Disadvantages:
- 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 sample code: now the developer needs to store all transferred services in protected variables.
Option 3
Instead of inserting services into the constructor, in this version they are directly inserted into each controller method:
function showAction($slug, $userService, $doctrineManagerService, $maxPerPageParameter){ ... }
The argument can be a path variable, a service, or a global parameter, and the rules for passing parameters should be specified:
- If the argument name ends with “Service”, we use the corresponding service ($ userService in the example);
- If the argument name ends with “Parameter”, we use the corresponding parameter with the Dependency Injector ($ maxPerPageParameter in the example);
- If not, and if the name of the argument matches the path variable, we use it ($ slug in the example);
- In other cases, if the argument is not defined, we throw an exception.
If you do not want to describe all the 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 parameters 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:
- Every action is independent and autonomous;
- Inside the method is a short and clear code;
- Good performance (as we ourselves do the analysis and analysis of 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 such a case is reassuring:
function showAction($year, $month, $day, $slug, $userService, $doctrineManagerService) { ... }
- Methods become similar to functions (as nothing unites them);
- Even if we pass services as method arguments, 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 passed both to the constructor and to the actions methods:
protected $user;
function __construct(User $user) {
$ this ->user = $user;
}
function showAction($slug, $mailerService, $maxPerPageParameter){ ... }
In this variant there is no problem anymore, that the 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 default container (or make it context sensitive).
It also allows you to emulate actions from a symfony 1.x branch:
function showAction(Request $requestService){ ... }
Since this is the most flexible option, the documentation should contain best practices.
Option 5
An option is a nearly complete copy of option 4, except that path variables cannot be included in the actions methods. This allows you to remove unnecessary suffixes (Parameter and Service). And access to path variables occurs through a call 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 agreement may be to include the Request object as the first argument (for the approach sequence).
Option 6
Another alternative would be to use annotations to include parameters. This is not specifically discussed yet, because Symfony 2 does not use annotations yet.
Advantages and disadvantages:
Advantages:
- Some third-party libraries have begun to use annotations (Doctrine 2, but for now, optional).
Disadvantages:
- Additional code;
- PHP developers rarely use annotations;
- Annotations are not the native construction of the PHP language.
Additional information can be obtained from the additional links below. In the comments, it is interesting to read which version 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 discussion on google groups:
part 1 ,
part 2 ,
part 3 .