Development

Documentation/fr_FR/book/1.0/trunk/09-Links-and-the-Routing-System

You must first sign up to be able to contribute.

Version 18 (modified by elkouo, 9 years ago)
Corrections de fautes

Cette partie de la documentation est en cours de traduction. Cela signifie qu'elle est traduite de manière soit incomplète, soit inexacte. En attendant que cette traduction soit terminée, vous pouvez consulter la version en anglais pour des informations plus fiables.

Chapitre 9 - Les liens et le système de routage

Au sein d'un framework web, les liens et les URL méritent une attention toutes particulière, car l'unique point d'entrée dans l'application (le contrôleur principal) et l'utilisation des helpers dans les gabarits, différencient complètement la façon dont les URL travaillent de leurs apparences. C'est ce que l'on appelle le routage. Bien plus qu'un gadget, le routage est un outil très utile pour rendre les applications web encore plus accessibles et sécurisées. Ce chapitre va vous expliquer tout ce dont vous avez besoin pour manipuler correctement les URL dans vos applications Symfony:

  • Qu'est-ce que le routage et comment fonctionne-t-il ?
  • Comment utiliser les liens helpers dans les gabarits pour permettre le routage des URL sortantes
  • Comment définir les règles de routage pour modifier l'apparence des URL

Vous trouverez aussi quelques astuces pour maitriser les performances de routage, et quelques petits plus.

Qu'est-ce que le routage ?

Le routage est un mécanisme qui réécrit les URL pour les rendre plus compréhensible. Mais pour bien comprendre, il faut se replonger dans les URL.

Les URL comme instruction serveur

Du navigateur au serveur, les URL véhiculent les informations nécessaires à la réalisation d'une action choisie par l'utilisateur. Une URL classique contient le chemin d'accès à un script ainsi que certains paramètres nécessaires à l'exécution d'une tache, comme dans l'exemple suivant:

http://www.example.com/web/controller/article.php?id=123456&format_code=6532

Cette URL contient des informations concernant l'architecture de l'application et concernant la base. D'habitude les développeurs cachent la structure de l'application dans l'interface. Ils préfèreront par exemple "Page de mon profile" comme titre de page plutôt que "QZ7.65". Révéler ce genre d'informations vitales de la structure interne d'une application va à l'encontre de cette pratique et présente de graves inconvénients.

  • Les données techniques apparaissant dans une URL provoquent de sérieuses failles de sécurité. Si on considère l'exemple précédent, que se passerait-il si une personne mal intentionnée changeait la valeur de l'id ? N'obtiendrait-il pas alors un accès direct à la base de donnée ? Ou que se passerait s'il essayait de changer le nom du script par admin.php par exemple ? Dans l'ensemble, les URL standards offrent un terrain propice au piratage de l'application et rendent la gestion de la sécurité beaucoup plus ardue.
  • L'inintelligibilité des URL les rend gênantes où qu'elles soient et diminue l'impacte du contenu correspondant. De plus, nos jours les URL n'apparaissent pas que dans les bar d'adresses, mais aussi lorsqu'un utilisateur survol un lien avec la souris ou dans les résultats de recherche. Lorsque les utilisateurs recherchent une information, il est préférable qu'ils ait une réponse aisément compréhensible plutôt qu'une URL confuse comme celle de la figure 9-1.

Figure 9-1 - Les URL apparaissent à plusieurs endroit comme dans les résultats des recherches

Les URL apparaissent à plusieurs endroit comme dans les résultats des recherches

  • Si l'URL est modifiée (comme par exemple, si un script ou un de ces paramètres changeait), tous les occurrences de cette URL devront être modifiées. Ceci implique plusieurs modifications lourdes et fastidieuses dans le contrôleur, ce qui n'est pas acceptable dans le cadre d'un développement agile.

Et cela pourrait être pire si Symfony n'utilisait pas le principe du contrôleur principal, c'est à dire si l'application possédait plusieurs scripts dans plusieurs répertoire directement accessibles par internet , comme ceux-ci:

http://www.example.com/web/gallery/album.php?name=my%20holidays
http://www.example.com/web/weblog/public/post/list.php
http://www.example.com/web/general/content/page.php?name=about%20us

Dans ce cas, les développeurs devront faire correspondre la structure de l'URL avec la structure des fichiers. Un vrai cauchemar de maintenance si la structure change

Les URL comme partie de l'interface

L'idée derrière le routage est de considérer l'URL comme partie de l'interface. L'application peut formater l'URL transmettre l'information à l'utilisateur et celui-ci peut utiliser l'URL pour accéder aux ressources de l'application.

Ceci est possible avec Symfony car l'URL présentée à l'utilisateur finale n'est pas liée à l'instruction serveur nécessaire à l'exécution de la requête. A la place, elle est liée à la ressource désirée et peut donc être formatée librement. Par exemple, Symfony peut comprendre l'URL suivante et pourtant afficher comme résultat la page cible de la première URL de ce chapitre:

http://www.example.com/articles/finance/2006/activity-breakdown.html

Les avantages sont immenses:

  • Les URL ont un réel sens et peuvent aider l'utilisateur à décider si la page ciblée par ce lien est véritablement ce qu'ils attendent. Un lien peut contenir des informations complémentaire à propos de la ressource qu'il concerne. C'est particulièrement utile pour les résultats des moteur de recherche. De plus, les URL apparaissent parfois sans aucune mention du titre de la page (imaginez que vous copiez une URL dans un email), et dans ce cas elle doivent signifier quelque chose par elles mêmes. L'illustration 9-2 vous donne un exemple d'URL accessible.

Figure 9-2 - Les URL peuvent véhiculer des informations complémentaires à propos de la page, comme une date de publication

Les URL peuvent véhiculer des informations complémentaires au sujet de la page, comme une date de publication

  • Les URL ont une meilleur présentation sur les documents papiers. Il est plus facile de les écrire et de les retenir. Une adresse du type http://www.example.com/controller/web/index.jsp?id=ERD4 sur votre carte de visite n'engendrera pas beaucoup de visites.

  • L'URL peut alors aussi devenir une sorte de ligne de commande permettant d'effectuer des actions précises ou récupérer des informations. Les applications offrant de telles possibilités sont plus rapide à utiliser pour les utilisateurs avancés.

    // Liste de résultat: ajouter une nouvelle balise pour affiner la recherche.
    http://del.icio.us/tag/symfony+ajax
    // Page du profile utilisateur: changer le nom pour accéder à un autre profile
    http://www.askeet.com/user/francois
    
  • Vous pouvez indépendamment changer le format de l'URL et le nom des actions/paramètres, avec une simple modification. Cela signifie que vous pouvez vous concentrer sur le développement et vous préoccuper des URL après sans devoir réorganiser votre application.

  • Dans le cas où vous réorganisiez complètement la structure de votre projet, les URL resteront inchangées pour le visiteur. Ce qui a aussi pour avantage de permettre le bookmark des pages dynamiques.

  • Les moteurs de recherche ont tendance à ne pas tenir compte des pages dynamiques au cours de l'indexation (extensions .php, .asp etc.). Mais vous pouvez formater les URL pour qu'elles donnent l'impression aux moteurs que les pages dynamiques sont des pages statiques. Il en résulte évidemment un meilleur référencement des pages de vos applications.

  • C'est plus sécurisé : Toutes les URL non reconnues seront redirigées vers une page précisée par le développeur. Les utilisateurs ne peuvent pas naviguer librement dans la structure de fichier en testant les URL. Les noms réels des scripts et des paramètres sont cachés.
    Le lien entre les URL présentées aux utilisateurs et les vrais noms des scripts et paramètres est réalisé par un système de routage basé sur des modèles modifiables dans la configuration du projet.

NOTE Qu'en est-il des ressources ? Fort heureusement, les URL des ressources (images, feuille de style et Javascript) ne sont pas visibles pour la navigation, il n'est donc pas nécessaire de les router. Dans Symfony, les ressources se trouvent dans le répertoire web et leur URL correspondent à leur localisation dans la structure de fichiers. Vous pouvez tout de même gérer des ressources dynamiques (les actions) en utilisant des URL générées avec le helper de la-dite ressource. Par exemple, pour afficher une image générée dynamiquement, vous pouvez utiliser la syntaxe suivante image_tag(url_for('captcha/image?key='.$key)).

Le fonctionnement

Symfony sépare les URL externe de leurs URI interne et le lien entre le deux est alors assuré par le système de routage. Afin de rendre les choses plus facile, la syntaxe des URI interne est similaire à celle des URL classique. Le listing 9-1 vous le montre.

Listing 9-1 - URL externes et URI internes

// Syntaxe d'une URI interne
<module>/<action>[?param1=value1][&param2=value2][&param3=value3]...

// exemple de d'une URI interne jamais affichée pour l'utilisateur final
article/permalink?year=2006&subject=finance&title=activity-breakdown

// Exemple d'une URL externe tel qu'elle est visible pour l'utilisateur final
http://www.example.com/articles/finance/2006/activity-breakdown.html

Le système de routage s'appuie sur une fichier de configuration particulier, routing.yml, dans lequel vous pouvez définir des règles de routage. L'exemple du listing 9-2 définit un modèle du style articles/*/*/* où vous remplacez les caractères génériques par le nom des éléments de contenu.

Listing 9-2 - Un exemple de règle de routage

article_by_title:
  url:    articles/:subject/:year/:title.html
  param:  { module: article, action: permalink }

Toute les requêtes envoyées à une application Symfony sont d'abord analysées par le système de routage (ce qui reste souple puisqu'elle sont toute gérées par un unique contrôleur principal). Le système de routage recherche une correspondance entre l'URL saisie et les modèles disponibles dans les règles de routage. S'il en trouve une, le noms génériques deviennent des paramètres et ils sont concaténés avec les valeurs présentes dans la clé param :. Le listing 9-3 vous illustre ce mécanisme

Listing 9-3 - Le système de routage intercepte les URL entrantes

// L'utilisateur a saisi (ou cliqué sur) cette URL externe 
http://www.example.com/articles/finance/2006/activity-breakdown.html
// Le contrôleur principal recherche une correspondance avec la règle `article_by`
// Le système de routage crée les paramètres suivants :
  'module'  => 'article'
  'action'  => 'permalink'
  'subject' => 'finance'
  'year'    => '2006'
  'title'   => 'activity-breakdown'

TIP L'extension .html des URL externe n'est que de la décoration et est ignorée par le système de routage. Son seul intérêt de a des pages dynamiques l'apparence de pages statiques. Vous verrez comment activer cela dans la partie "Configuration du routage" plus loin dans ce chapitre.

La requête est alors transmise à l'action permalink du module article, qui possède alors toutes les informations nécessaires pour accéder à l'article désiré.

Ce mécanisme doit être réversible et permettre à l'application d'afficher les URL adéquats. Pour ce faire vous devez fournir au système de routage suffisamment d'éléments pour qu'il puisse déterminer quelle règle appliquer. Vous ne devez en aucun cas saisir des hyperliens directement dans une balise <a> car vous le système de routage serait alors non utilisé. Vous préférez l'emploi des helpers adéquats comme le montre le listing 9-4.

Listing 9-4 - Le système de routage formate les URL sortantes dans les gabarits.

[php]
// Le helper `url_for()` transforme les URI internes en URL externes.
<a href="<?php echo url_for('article/permalink?subject=finance&year=2006&title=activity-breakdown') ?>">click here</a>

// Le helper trouve une correspondance avec la règle `article_by` 
// Le système de routage crée l'URL externe correspondante.
 => <a href="http://www.example.com/articles/finance/2006/activity-breakdown.html">click here</a>

// Le helper `link_to()` affiche directement un hyperlien
// et permet d'éviter le mélange de php et html
<?php echo link_to(
  'click here',
  'article/permalink?subject=finance&year=2006&title=activity-breakdown'
) ?>

// Etant donnée qu'en interne, `link_to()` appelle `url_for()`, on obtient le même résultat.
=> <a href="http://www.example.com/articles/finance/2006/activity-breakdown.html">click here</a>

Il en ressort donc que le mécanisme de routage est à double sens, et ne fonctionne que si vous utilisez le helper link_to() pour vos liens.

URL Rewriting ou la réécriture des URL "à la volée"

Avant d'aller plus loin, il est nécessaire de clarifier un point. Le nom du contrôleur principal (index.php or myapp_dev.php) n'apparait jamais les URI internes citées en exemples donner dans les chapitres précédents. Seul le contrôle principal détermine l'environnement, donc si tout les liens sont dépendants d'un environnement, le nom du contrôleur principal de cette environnement n'aura jamais besoin d'être précisé.

Il n'y a pas non plus de nom de script dans les exemples d'URI générées. C'est parce que, par défaut, les URL générées n'en possèdent pas non plus en environnement de production. Ce comportement est géré par le paramètres no_script_name du fichier setting.yml. Pistionnez le à off, comme le montre le listing 9-5, et toutes les URL générées contiendront le nom du contrôleur principal.

Listing 9-5 - Faire apparaitre le nom du contrôleur principal dans les URL, dans apps/myapp/settings.yml

prod:
  .settings
    no_script_name:  off

Dorénavant, les URL générées ressembleront à ça :

http://www.example.com/index.php/articles/finance/2006/activity-breakdown.html

Hors mis pour l'environnement de production, tous les paramètres no_script_name sont à off par défaut. Par conséquent, lorsque parcourez votre application dans l'environnement de développement le nom du contrôleur principal apparait toujours.

http://www.example.com/myapp_dev.php/articles/finance/2006/activity-breakdown.html

En production, le paramètre no_script_name est à on, ainsi les URL ne contiennent que les information de routage et restent "user-firnedly". Aucune information technique n'apparait dans ces URL.

http://www.example.com/articles/finance/2006/activity-breakdown.html

Mais comment diable fait l'application pour savoir quel contrôleur appeler ? C'est là que l'URL rewriting entre en jeu. Le serveur web peut être configurer pour appeler un script particulier lorsqu'il n'y en a pas dans l'URI.

Avec Apache, cela n'est possible que si vous avez activé l'extension mod_rewrite. Tous les projets symfony comprennent un fichier .htaccess qui ajoute le paramétrage du mod_rewrite à la configuration serveur et pour le répertoire web/. Le contenu par défaut de ce fichier est visible au listing 9-6.

Listing 9-6 - Default Rewriting Rules for Apache, in myproject/web/.htaccess Listing 9-6 - Les règles par défaut du rewriting pour Apache, dans myproject/web/.htaccess

<IfModule mod_rewrite.c>
  RewriteEngine On

  # on we skip all files with .something
  RewriteCond %{REQUEST_URI} \..+$
  RewriteCond %{REQUEST_URI} !\.html$
  RewriteRule .* - [L]

  # we check if the .html version is here (caching)
  RewriteRule ^$ index.html [QSA]
  RewriteRule ^([^.]+)$ $1.html [QSA]
  RewriteCond %{REQUEST_FILENAME} !-f

  # no, so we redirect to our front web controller
  RewriteRule ^(.*)$ index.php [QSA,L]
</IfModule>

Le serveur web inspecte l'URI qu'il reçoit. Si elle ne contient pas de suffixe et que la page cible n'est en cache (Chapter 12 covers caching), alors la requête est rattachée à index.php.

However, the web/ directory of a symfony project is shared among all the applications and environments of the project. It means that there is usually more than one front controller in the web directory. For instance, a project having a frontend and a backend application, and a dev and prod environment, contains four front controller scripts in the web/ directory:

index.php         // frontend in prod
frontend_dev.php  // frontend in dev
backend.php       // backend in prod
backend_dev.php   // backend in dev

The mod_rewrite settings can specify only one default script name. If you set no_script_name to on for all the applications and environments, all URLs will be interpreted as requests to the frontend application in the prod environment. This is why you can have only one application with one environment taking advantage of the URL rewriting for a given project.

TIP There is a way to have more than one application with no script name. Just create subdirectories in the web root, and move the front controllers inside them. Change the SF_ROOT_DIR constants definition accordingly, and create the .htaccess URL rewriting configuration that you need for each application.

Link Helpers

Because of the routing system, you should use link helpers instead of regular <a> tags in your templates. Don't look at it as a hassle, but rather as an opportunity to keep your application clean and easy to maintain. Besides, link helpers offer a few very useful shortcuts that you don't want to miss.

Hyperlinks, Buttons, and Forms

You already know about the link_to() helper. It outputs an XHTML-compliant hyperlink, and it expects two parameters: the element that can be clicked and the internal URI of the resource to which it points. If, instead of a hyperlink, you want a button, use the button_to() helper. Forms also have a helper to manage the value of the action attribute. You will learn more about forms in the next chapter. Listing 9-7 shows some examples of link helpers.

Listing 9-7 - Link Helpers for <a>, <input>, and <form> Tags

[php]
// Hyperlink on a string
<?php echo link_to('my article', 'article/read?title=Finance_in_France') ?>
 => <a href="/routed/url/to/Finance_in_France">my article</a>

// Hyperlink on an image
<?php echo link_to(image_tag('read.gif'), 'article/read?title=Finance_in_France') ?>
 => <a href="/routed/url/to/Finance_in_France"><img src="/images/read.gif" /></a>

// Button tag
<?php echo button_to('my article', 'article/read?title=Finance_in_France') ?>
 => <input value="my article" type="button"onclick="document.location.href='/routed/url/to/Finance_in_France';" />

// Form tag
<?php echo form_tag('article/read?title=Finance_in_France') ?>
 => <form method="post" action="/routed/url/to/Finance_in_France" />

Link helpers can accept internal URIs as well as absolute URLs (starting with http://, and skipped by the routing system) and anchors. Note that in real-world applications, internal URIs are built with dynamic parameters. Listing 9-8 shows examples of all these cases.

Listing 9-8 - URLs Accepted by Link Helpers

[php]
// Internal URI
<?php echo link_to('my article', 'article/read?title=Finance_in_France') ?>
 => <a href="/routed/url/to/Finance_in_France">my article</a>

// Internal URI with dynamic parameters
<?php echo link_to('my article', 'article/read?title='.$article->getTitle()) ?>

// Internal URI with anchors
<?php echo link_to('my article', 'article/read?title=Finance_in_France#foo') ?>
 => <a href="/routed/url/to/Finance_in_France#foo">my article</a>

// Absolute URL
<?php echo link_to('my article', 'http://www.example.com/foobar.html') ?>
 => <a href="http://www.example.com/foobar.html">my article</a>

Link Helper Options

As explained in Chapter 7, helpers accept an additional options argument, which can be an associative array or a string. This is true for link helpers, too, as shown in Listing 9-9.

Listing 9-9 - Link Helpers Accept Additional Options

[php]
// Additional options as an associative array
<?php echo link_to('my article', 'article/read?title=Finance_in_France', array(
  'class'  => 'foobar',
  'target' => '_blank'
)) ?>

// Additional options as a string (same result)
<?php echo link_to('my article', 'article/read?title=Finance_in_France','class=foobar target=_blank') ?>
 => <a href="/routed/url/to/Finance_in_France" class="foobar" target="_blank">my article</a>

You can also add one of the symfony-specific options for link helpers: confirm and popup. The first one displays a JavaScript confirmation dialog box when the link is clicked, and the second opens the link in a new window, as shown in Listing 9-10.

Listing 9-10 - 'confirm' and 'popup' Options for Link Helpers

[php]
<?php echo link_to('delete item', 'item/delete?id=123', 'confirm=Are you sure?') ?>
 => <a onclick="return confirm('Are you sure?');"
       href="/routed/url/to/delete/123.html">add to cart</a>

<?php echo link_to('add to cart', 'shoppingCart/add?id=100', 'popup=true') ?>
 => <a onclick="window.open(this.href);return false;"
       href="/fo_dev.php/shoppingCart/add/id/100.html">add to cart</a>

<?php echo link_to('add to cart', 'shoppingCart/add?id=100', array(
  'popup' => array('Window title', 'width=310,height=400,left=320,top=0')
)) ?>
 => <a onclick="window.open(this.href,'Window title','width=310,height=400,left=320,top=0');return false;"
       href="/fo_dev.php/shoppingCart/add/id/100.html">add to cart</a>

These options can be combined.

Fake GET and POST Options

Sometimes web developers use GET requests to actually do a POST. For instance, consider the following URL:

http://www.example.com/index.php/shopping_cart/add/id/100

This request will change the data contained in the application, by adding an item to a shopping cart object, stored in the session or in a database. This URL can be bookmarked, cached, and indexed by search engines. Imagine all the nasty things that might happen to the database or to the metrics of a website using this technique. As a matter of fact, this request should be considered as a POST, because search engine robots do not do POST requests on indexing.

Symfony provides a way to transform a call to a link_to() or button_to() helper into an actual POST. Just add a post=true option, as shown in Listing 9-11.

Listing 9-11 - Making a Link Call a POST Request

[php]
<?php echo link_to('go to shopping cart', 'shoppingCart/add?id=100', 'post=true') ?>
 => <a onclick="f = document.createElement('form'); document.body.appendChild(f);
                f.method = 'POST'; f.action = this.href; f.submit();return false;"
       href="/shoppingCart/add/id/100.html">go to shopping cart</a>

This <a> tag has an href attribute, and browsers without JavaScript support, such as search engine robots, will follow the link doing the default GET. So you must also restrict your action to respond only to the POST method, by adding something like the following at the beginning of the action:

[php]
$this->forward404If($request->getMethod() != sfRequest::POST);

Just make sure you don't use this option on links located in forms, since it generates its own <form> tag.

It is a good habit to tag as POST the links that actually post data.

Forcing Request Parameters As GET Variables

According to your routing rules, variables passed as parameters to a link_to() are transformed into patterns. If no rule matches the internal URI in the routing.yml file, the default rule transforms module/action?key=value into /module/action/key/value, as shown in Listing 9-12.

Listing 9-12 - Default Routing Rule

[php]
<?php echo link_to('my article', 'article/read?title=Finance_in_France') ?>
=> <a href="/article/read/title/Finance_in_France">my article</a>

If you actually need to keep the GET syntax--to have request parameters passed under the ?key=value form--you should put the variables that need to be forced outside the URL parameter, in the query_string option. All the link helpers accept this option, as demonstrated in Listing 9-13.

Listing 9-13 - Forcing GET Variables with the query_string Option

[php]
<?php echo link_to('my article', 'article/read', array(
  'query_string' => 'title=Finance_in_France'
)) ?>
=> <a href="/article/read?title=Finance_in_France">my article</a>

A URL with request parameters appearing as GET variables can be interpreted by a script on the client side, and by the $_GET and $_REQUEST variables on the server side.

SIDEBAR Asset helpers

Chapter 7 introduced the asset helpers image_tag(), stylesheet_tag(), and javascript_include_ tag(), which allow you to include an image, a style sheet, or a JavaScript file in the response. The paths to such assets are not processed by the routing system, because they link to resources that are actually located under the public web directory.

You don't need to mention a file extension for an asset. Symfony automatically adds .png, .js, or .css to an image, JavaScript, or style sheet helper call. Also, symfony will automatically look for those assets in the web/images/, web/js/, and web/css/ directories. Of course, if you want to include a specific file format or a file from a specific location, just use the full file name or the full file path as an argument. And don't bother to specify an alt attribute if your media file has an explicit name, since symfony will determine it for you.

[php]
<?php echo image_tag('test') ?>
<?php echo image_tag('test.gif') ?>
<?php echo image_tag('/my_images/test.gif') ?>
 => <img href="/images/test.png" alt="Test" />
    <img href="/images/test.gif" alt="Test" />
    <img href="/my_images/test.gif" alt="Test" />

To fix the size of an image, use the size attribute. It expects a width and a height in pixels, separated by an x.

[php]
<?php echo image_tag('test', 'size=100x20')) ?>
 => <img href="/images/test.png" alt="Test" width="100" height="20"/>

If you want the asset inclusion to be done in the <head> section (for JavaScript files and style sheets), you should use the use_stylesheet() and use_javascript() helpers in your templates, instead of the _tag() versions in the layout. They add the asset to the response, and these assets are included before the </head> tag is sent to the browser.

Using Absolute Paths

The link and asset helpers generate relative paths by default. To force the output to absolute paths, set the absolute option to true, as shown in Listing 9-14. This technique is useful for inclusions of links in an e-mail message, RSS feed, or API response.

Listing 9-14 - Getting Absolute URLs Instead of Relative URLs

[php]
<?php echo url_for('article/read?title=Finance_in_France') ?>
 => '/routed/url/to/Finance_in_France'
<?php echo url_for('article/read?title=Finance_in_France', true) ?>
 => 'http://www.example.com/routed/url/to/Finance_in_France'

<?php echo link_to('finance', 'article/read?title=Finance_in_France') ?>
 => <a href="/routed/url/to/Finance_in_France">finance</a>
<?php echo link_to('finance', 'article/read?title=Finance_in_France','absolute=true') ?>
 => <a href=" http://www.example.com/routed/url/to/Finance_in_France">finance</a>

// The same goes for the asset helpers
<?php echo image_tag('test', 'absolute=true') ?>
<?php echo javascript_include_tag('myscript', 'absolute=true') ?>

SIDEBAR The Mail helper

Nowadays, e-mail-harvesting robots prowl about the Web, and you can't display an e-mail address on a website without becoming a spam victim within days. This is why symfony provides a mail_to() helper.

The mail_to() helper takes two parameters: the actual e-mail address and the string that should be displayed. Additional options accept an encode parameter to output something pretty unreadable in HTML, which is understood by browsers but not by robots.

[php]
<?php echo mail_to('myaddress@mydomain.com', 'contact') ?>
 => <a href="mailto:myaddress@mydomain.com'>contact</a>
<?php echo mail_to('myaddress@mydomain.com', 'contact', 'encode=true') ?>
 => <a href="&#109;&#x61;... &#111;&#x6d;">&#x63;&#x74;... e&#115;&#x73;</a>

Encoded e-mail messages are composed of characters transformed by a random decimal and hexadecimal entity encoder. This trick stops most of the address-harvesting spambots for now, but be aware that the harvesting techniques evolve rapidly.

Routing Configuration

The routing system does two things:

  • It interprets the external URL of incoming requests and transforms it into an internal URI, to determine the module/action and the request parameters.
  • It formats the internal URIs used in links into external URLs (provided that you use the link helpers).

The conversion is based on a set of routing rules . These rules are stored in a routing.yml configuration file located in the application config/ directory. Listing 9-15 shows the default routing rules, bundled with every symfony project.

Listing 9-15 - The Default Routing Rules, in myapp/config/routing.yml

# default rules
homepage:
  url:   /
  param: { module: default, action: index }

default_symfony:
  url:   /symfony/:action/*
  param: { module: default }

default_index:
  url:   /:module
  param: { action: index }

default:
  url:   /:module/:action/*

Rules and Patterns

Routing rules are bijective associations between an external URL and an internal URI. A typical rule is made up of the following:

  • A unique label, which is there for legibility and speed, and can be used by the link helpers
  • A pattern to be matched (url key)
  • An array of request parameter values (param key)

Patterns can contain wildcards (represented by an asterisk, *) and named wildcards (starting with a colon, :). A match to a named wildcard becomes a request parameter value. For instance, the default rule defined in Listing 9-15 will match any URL like /foo/bar, and set the module parameter to foo and the action parameter to bar. And in the default_symfony rule, symfony is a keyword and action is named wildcard parameter.

The routing system parses the routing.yml file from the top to the bottom and stops at the first match. This is why you must add your own rules on top of the default ones. For instance, the URL /foo/123 matches both of the rules defined in Listing 9-16, but symfony first tests my_rule:, and as that rule matches, it doesn't even test the default: one. The request is handled by the mymodule/myaction action with bar set to 123 (and not by the foo/123 action).

Listing 9-16 - Rules Are Parsed Top to Bottom

my_rule:
  url:   /foo/:bar
  param: { module: mymodule, action: myaction }

# default rules
default:
  url:   /:module/:action/*

NOTE When a new action is created, it does not imply that you must create a routing rule for it. If the default module/action pattern suits you, then forget about the routing.yml file. If, however, you want to customize the action's external URL, add a new rule above the default one.

Listing 9-17 shows the process of changing the external URL format for an article/read action.

Listing 9-17 - Changing the External URL Format for an article/read Action

[php]
<?php echo url_for('my article', 'article/read?id=123') ?>
 => /article/read/id/123       // Default formatting

// To change it to /article/123, add a new rule at the beginning
// of your routing.yml
article_by_id:
  url:   /article/:id
  param: { module: article, action: read }

The problem is that the article_by_id rule in Listing 9-17 breaks the default routing for all the other actions of the article module. In fact, a URL like article/delete will match this rule instead of the default one, and call the read action with id set to delete instead of the delete action. To get around this difficulty, you must add a pattern constraint so that the article_by_id rule matches only URLs where the id wildcard is an integer.

Pattern Constraints

When a URL can match more than one rule, you must refine the rules by adding constraints, or requirements, to the pattern. A requirement is a set of regular expressions that must be matched by the wildcards for the rule to match.

For instance, to modify the article_by_id rule so that it matches only URLs where the id parameter is an integer, add a line to the rule, as shown in Listing 9-18.

Listing 9-18 - Adding a Requirement to a Routing Rule

article_by_id:
  url:   /article/:id
  param: { module: article, action: read }
  requirements: { id: \d+ }

Now an article/delete URL can't match the article_by_id rule anymore, because the 'delete' string doesn't satisfy the requirements. Therefore, the routing system will keep on looking for a match in the following rules and finally find the default rule.

SIDEBAR Permalinks

A good security guideline for routing is to hide primary keys and replace them with significant strings as much as possible. What if you wanted to give access to articles from their title rather than from their ID? It would make external URLs look like this:

http://www.example.com/article/Finance_in_France

To that extent, you need to create a new permalink action, which will use a slug parameter instead of an id one, and add a new rule for it:

article_by_id:
  url:          /article/:id
  param:        { module: article, action: read }
  requirements: { id: \d+ }

article_by_slug:
  url:          /article/:slug
  param:        { module: article, action: permalink }

The permalink action needs to determine the requested article from its title, so your model must provide an appropriate method.

[php]
public function executePermalink()
{
  $article = ArticlePeer::retrieveBySlug($this->getRequestParameter('slug');
  $this->forward404Unless($article);  // Display 404 if no article matches slug
  $this->article = $article;          // Pass the object to the template
}

You also need to replace the links to the read action in your templates with links to the permalink one, to enable correct formatting of internal URIs.

[php]
// Replace
<?php echo link_to('my article', 'article/read?id='.$article->getId()) ?>

// With
<?php echo link_to('my article', 'article/permalink?slug='.$article->getSlug()) ?>

Thanks to the requirements line, an external URL like /article/Finance_in_France matches the article_by_slug rule, even though the article_by_id rule appears first.

Note that as articles will be retrieved by slug, you should add an index to the slug column in the Article model description to optimize database performance.

Setting Default Values

You can give named wildcards a default value to make a rule work, even if the parameter is not defined. Set default values in the param: array.

For instance, the article_by_id rule doesn't match if the id parameter is not set. You can force it, as shown in Listing 9-19.

Listing 9-19 - Setting a Default Value for a Wildcard

article_by_id:
  url:          /article/:id
  param:        { module: article, action: read,
id: 1
 }

The default parameters don't need to be wildcards found in the pattern. In Listing 9-20, the display parameter takes the value true, even if it is not present in the URL.

Listing 9-20 - Setting a Default Value for a Request Parameter

article_by_id:
  url:          /article/:id
  param:        { module: article, action: read, id: 1, display: true }

If you look carefully, you can see that article and read are also default values for module and action variables not found in the pattern.

TIP You can define a default parameter for all the routing rules by defining the sf_routing_default configuration parameter. For instance, if you want all the rules to have a theme parameter set to default by default, add the line sfConfig::set('sf_routing_defaults', array('theme' => 'default')); to your application's config.php.

Speeding Up Routing by Using the Rule Name

The link helpers accept a rule label instead of a module/action pair if the rule label is preceded by an at sign (@), as shown in Listing 9-21.

Listing 9-21 - Using the Rule Label Instead of the Module/Action

[php]
<?php echo link_to('my article', 'article/read?id='.$article->getId()) ?>

// can also be written as
<?php echo link_to('my article', '@article_by_id?id='.$article->getId()) ?>

There are pros and cons to this trick. The advantages are as follows:

  • The formatting of internal URIs is done much faster, since symfony doesn't have to browse all the rules to find the one that matches the link. In a page with a great number of routed hyperlinks, the boost will be noticeable if you use rule labels instead of module/action pairs.
  • Using the rule label helps to abstract the logic behind an action. If you decide to change an action name but keep the URL, a simple change in the routing.yml file will suffice. All of the link_to() calls will still work without further change.
  • The logic of the call is more apparent with a rule name. Even if your modules and actions have explicit names, it is often better to call @display_article_by_slug than article/display.

On the other hand, a disadvantage is that adding new hyperlinks becomes less self-evident, since you always need to refer to the routing.yml file to find out which label is to be used for an action.

The best choice depends on the project. In the long run, it's up to you.

TIP During your tests (in the dev environment), if you want to check which rule was matched for a given request in your browser, develop the "logs and msgs" section of the web debug toolbar and look for a line specifying "matched route XXX." You will find more information about the web debug mode in Chapter 16.

Adding an .html Extension

Compare these two URLs:

http://myapp.example.com/article/Finance_in_France
http://myapp.example.com/article/Finance_in_France.html

Even if it is the same page, users and (robots) may see it differently because of the URL. The second URL evokes a deep and well-organized web directory of static pages, which is exactly the kind of websites that search engines know how to index.

To add a suffix to every external URL generated by the routing system, change the suffix value in the application settings.yml, as shown in Listing 9-22.

Listing 9-22 - Setting a Suffix for All URLs, in myapp/config/settings.yml

prod:
  .settings
    suffix:         .html

The default suffix is set to a period (.), which means that the routing system doesn't add a suffix unless you specify it.

It is sometimes necessary to specify a suffix for a unique routing rule. In that case, write the suffix directly in the related url: line of the routing.yml file, as shown in Listing 9-23. Then the global suffix will be ignored.

Listing 9-23 - Setting a Suffix for One URL, in myapp/config/routing.yml

article_list:
  url:          /latest_articles
  param:        { module: article, action: list }

article_list_feed:
  url:          /latest_articles.rss
  param:        { module: article, action: list, type: feed }

Creating Rules Without routing.yml

As is true of most of the configuration files, the routing.yml is a solution to define routing rules, but not the only one. You can define rules in PHP, either in the application config.php file or in the front controller script, but before the call to dispatch(), because this method determines the action to execute according to the present routing rules. Defining rules in PHP authorizes you to create dynamic rules, depending on configuration or other parameters.

The object that handles the routing rules is the sfRouting singleton. It is available from every part of the code by requiring sfRouting::getInstance(). Its prependRoute() method adds a new rule on top of the existing ones defined in routing.yml. It expects four parameters, which are the same as the parameters needed to define a rule: a route label, a pattern, an associative array of default values, and another associative array for requirements. For instance, the routing.yml rule definition shown in Listing 9-18 is equivalent to the PHP code shown in Listing 9-24.

Listing 9-24 - Defining a Rule in PHP

[php]
sfRouting::getInstance()->prependRoute(
  'article_by_id',                                  // Route name
  '/article/:id',                                   // Route pattern
  array('module' => 'article', 'action' => 'read'), // Default values
  array('id' => '\d+'),                             // Requirements
);

The sfRouting singleton has other useful methods for handling routes by hand: clearRoutes(), hasRoutes(), getRoutesByName(), and so on. Refer to the API documentation (http://www.symfony-project.org/api/symfony.html) to learn more.

TIP Once you start to fully understand the concepts presented in this book, you can increase your understanding of the framework by browsing the online API documentation or, even better, the symfony source. Not all the tweaks and parameters of symfony can be described in this book. The online documentation, however, is limitless.

Dealing with Routes in Actions

If you need to retrieve information about the current route--for instance, to prepare a future "back to page xxx" link--you should use the methods of the sfRouting object. The URIs returned by the getCurrentInternalUri() method can be used in a call to a link_to() helper, as shown in Listing 9-25.

Listing 9-25 - Using sfRouting to Get Information About the Current Route

[php]
// If you require a URL like
http://myapp.example.com/article/21

// Use the following in article/read action
$uri = sfRouting::getInstance()->getCurrentInternalUri();
 => article/read?id=21

$uri = sfRouting::getInstance()->getCurrentInternalUri(true);
 => @article_by_id?id=21

$rule = sfRouting::getInstance()->getCurrentRouteName();
 => article_by_id

// If you just need the current module/action names,
// remember that they are actual request parameters
$module = $this->getRequestParameter('module');
$action = $this->getRequestParameter('action');

If you need to transform an internal URI into an external URL in an action--just as url_for() does in a template--use the genUrl() method of the sfController object, as shown in Listing 9-26.

Listing 9-26 - Using sfController to Transform an Internal URI

[php]
$uri = 'article/read?id=21';

$url = $this->getController()->genUrl($uri);
 => /article/21

$url = $this->getController()->genUrl($uri, true);
=> http://myapp.example.com/article/21

Summary

Routing is a two-way mechanism designed to allow formatting of external URLs so that they are more user-friendly. URL rewriting is required to allow the omission of the front controller name in the URLs of one of the applications of each project. You must use link helpers each time you need to output a URL in a template if you want the routing system to work both ways. The routing.yml file configures the rules of the routing system and uses an order of precedence and rule requirements. The settings.yml file contains additional settings concerning the presence of the front controller name and a possible suffix in external URLs.