Je suis développeur web freelance et propose des formations à Symfony2 ! Contactez-moi pour en discuter.

Cet article est la traduction d’un article original de Fabien Potencier, à l’origine de Symfony2, disponible ici.

Vous pensez peut être que notre framework est déjà plutôt solide et vous avez probablement raison. Mais regardons quand même comment nous pouvons l’améliorer.

Actuellement, tous nos exemples utilisent du code procédural, mais souvenez vous que les contrôleurs peuvent être n’importe quel callback PHP valide. Convertissons notre contrôleur pour utiliser une classe dédiée :


class LeapYearController
{
public function indexAction($request)
{
if (is_leap_year($request->attributes->get('year'))) {
return new Response('Yep, this is a leap year!');
}

return new Response('Nope, this is not a leap year.');
}
}


Mettez à jour la définition de la route en conséquences :


$routes->add('leap_year', new Routing\Route('/is_leap_year/{year}', array(
'year' => null,
'_controller' => array(new LeapYearController(), 'indexAction'),
)));

La manipulation est plutôt simple et a du sens à partir du moment où l’on commence à créer plusieurs pages, mais vous avez sûrement remarqué un effet secondaire indésirable… la classe LeapYearController est toujours instanciée, même si l’URL demandé ne correspond par à la route leap_year. C’est pas terrible pour un aspect important : du point de vue performances, tous les contrôleurs de toutes les routes sont instanciés pour toutes les requêtes. Ce serait mieux si les contrôleurs étaient chargés uniquement lorsque c’est nécessaire, afin que seul le contrôleur associé à la route soit instancié.

Pour résoudre ce problème ainsi que plusieurs autres, installons et utilisons le composant HttpKernel :

{
"require": {
"symfony/class-loader": "2.1.*",
"symfony/http-foundation": "2.1.*",
"symfony/routing": "2.1.*",
"symfony/http-kernel": "2.1.*"
}
}

Le composant HttpKernel a plutôt fonctionnalités intéressantes, mais celle dont nous avons besoin maintenant, c’est le résolveur de contrôleur. Un résolveur de contrôleur sait comment déterminer le contrôleur à exécuter et les arguments à lui fournir à partir d’un objet Request. Tous les résolveurs de contrôleurs implémentent l’interface suivante :


namespace Symfony\Component\HttpKernel\Controller;

interface ControllerResolverInterface
{
function getController(Request $request);

function getArguments(Request $request, $controller);
}

La méthode getController() utilise les mêmes conventions que celles définies plus tôt. L’attribut de requête _controller doit contenir le contrôleur associé à la requête. En plus du système de callback PHP, getController() supporte également des chaines composées du nom d’une classe suivi par 2 « : » ainsi qu’un nom de méthode, tel que « class::method » :


$routes->add('leap_year', new Routing\Route('/is_leap_year/{year}', array(
'year' => null,
'_controller' => 'LeapYearController::indexAction',
)));

Pour que ce code marche, modifiez le code du framework pour utiliser le résolveur de contrôleurs du composant HttpKernel :


use Symfony\Component\HttpKernel;

$resolver = new HttpKernel\Controller\ControllerResolver();

$controller = $resolver->getController($request);
$arguments = $resolver->getArguments($request, $controller);

$response = call_user_func_array($controller, $arguments);

Bonus supplémentaire, le résolveur de contrôleurs gère correctement les erreurs pour vous : lorsque vous oubliez de définir un attribut _controller pour une route par exemple.

Maintenant, regardons comment les arguments du contrôleur sont devinés. getArguments() fait de l’introspection sur la signature du contrôleur pour déterminer quels arguments lui fournir en utilisant la réflection native de PHP.

La méthode indexAction() a besoin de l’objet Request en argument. getArguments() sait quand l’injecter si le type est correctement suggéré :


public function indexAction(Request $request)

// Ne marchera pas
public function indexAction($request)

Plus intéressant, getArguments() est également capable d’injecter n’importe quel attribut de requête; l’argument doit simplement avoir le même nom que l’attribut correspondant :

public function indexAction($year)

Vous pouvez également injecter l’objet Request et certains attributs en même temps (car l’association est faite à partir des noms de paramètres ou des types proposés, leur ordre n’a pas d’importance) :


public function indexAction(Request $request, $year)

public function indexAction($year, Request $request)

Enfin, vous pouvez également définir des valeurs par défaut pour les arguments qui correspondent à un attribut optionnel de la requête :
public function indexAction($year = 2012)

Contentons nous d’injecter l’attribut de requête $year dans notre contrôleur :


class LeapYearController
{
public function indexAction($year)
{
if (is_leap_year($year)) {
return new Response('Yep, this is a leap year!');
}

return new Response('Nope, this is not a leap year.');
}
}

Le résolveur de contrôleur se charge également de valider la méthode à appeler et ses arguments. En cas de problème, il lance une exception avec un message sympa expliquant le problème (la classe de contrôleur n’existe pas, la méthode n’est pas définie, une argument n’a pas d’attributs associés…).

Avec la grande flexibilité du résolveur de contrôleur par défaut, vous vous demandez peut être pourquoi on pourrait vouloir en implémenter un autre (Pourquoi y aurait-il une interface si ce n’était pas le cas ?). 2 exemples : dans Symfony2, getController() est amélioré pour supporter les « contrôleurs comme des services ».
Dans FrameworkExtraBundle, getArguments() est amélioré pour supporter les convertisseurs de paramètres, où les attributs de requêtes sont directement convertis en objets.

Concluons avec la nouvelle version de notre framework :

attributes->all());
ob_start();
include sprintf(__DIR__.'/../src/pages/%s.php', $_route);

return new Response(ob_get_clean());
}

$request = Request::createFromGlobals();
$routes = include __DIR__.'/../src/app.php';

$context = new Routing\RequestContext();
$context->fromRequest($request);
$matcher = new Routing\Matcher\UrlMatcher($routes, $context);
$resolver = new HttpKernel\Controller\ControllerResolver();

try {
$request->attributes->add($matcher->match($request->getPathInfo()));

$controller = $resolver->getController($request);
$arguments = $resolver->getArguments($request, $controller);

$response = call_user_func_array($controller, $arguments);
} catch (Routing\Exception\ResourceNotFoundException $e) {
$response = new Response('Not Found', 404);
} catch (Exception $e) {
$response = new Response('An error occurred', 500);
}

$response->send();

Repensez-y encore une fois : notre framework n’a jamais été aussi robuste et flexible, et il fait pourtant toujours moins de 40 lignes de code.

Répondre

Unable to load the Are You a Human PlayThru™. Please contact the site owner to report the problem.