Automatiser l'usage de LESS via la gestion des environnements de votre framework PHP

LESS est un préprocesseur CSS étendant les possibilités natives du langage, proposant notamment l'utilisation de variables ou encore de mixins (sortes de fonctions) permettant au développeur feignant efficace d'appliquer au CSS le sacro-saint principe du don't repeat yourself. Seulement voilà, qui dit préprocesseur dit compilation, ce qui implique un passage systématique par le terminal. Comment éviter cela ?

Le but du jeu est d'intercepter les appels aux feuilles de styles émis par les navigateurs des visiteurs, et d'y répondre en accord avec l'environnement d'exécution actuel : en développement, on renverra un code non compressé et fraîchement compilé, afin de nous mâcher le travail ; en production en revanche, on créera un fichier CSS minifié et statique à l'occasion de de la première requête, que l'on renverra tel quel lors des suivantes afin d'éviter une charge serveur inutile. Pour changer un peu, nous allons voir comment mettre cela en place avec le framework PHP Symfony2.

Voici tout d'abord comment rerouter les requêtes concernées vers une méthode dédiée d'un contrôleur, dans votre fichier routing.yml :

less_to_css:
pattern: /css/{file}.css
defaults: { _controller: MyBundle:MyController:css }
requirements:
file: ([a-z0-9_-]+)

Ladite méthode embarque le code suivant :

public function cssAction($file)
{
if (! file_exists(CSS_PATH.$file.'.less')) {
throw $this->createNotFoundException('Cette feuille de style est introuvable.');
}

if ($this->container->get('kernel')->getEnvironment() === 'prod') {
if (! file_exists(CSS_PATH.$file.'.css')) {
system('lessc -x '.CSS_PATH.$file.'.less > '.CSS_PATH.$file.'.css');
}
$response = new Response(file_get_contents(CSS_PATH.$file.'.css'));
} else {
$output = array();
exec('lessc '.CSS_PATH.$file.'.less', $output);
$response = new Response(implode(PHP_EOL, $output));
}

$response->headers->set('Content-Type', 'text/css');
return $response;
}

Voilà donc un compromis intéressant entre automatisme et délégation de la compilation au serveur ! Évidemment, ce type de solution ne sera probablement pas adapté à un site à fort trafic, mais aura au moins le mérite de vous libérer l'esprit jusqu'à la fin de la recette.

Utiliser plusieurs polices au format SVG avec un seul fichier

Dans , je démontrais comment réaliser un gain de performances en combinant plusieurs polices dans un même fichier JavaScript. À l'heure de l'explosion des règles CSS3 @font-face, il peut être utile de savoir que des possibilités similaires s'offrent à nous concernant les fichiers de polices au format SVG. En effet, ceux-ci utilisent le format XML, et acceptent donc parfaitement la déclaration simultanée de plusieurs polices en faisant se suivre les balises font :

<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg">
<metadata></metadata>
<defs>
<font id="police1">
...
</font>
<font id="police2">
...
</font>
</defs>
</svg>

Il est donc très facile de combiner plusieurs fichiers SVG, obtenus par exemple via le générateur en ligne de FontSquirrel.

Le CSS correspondant prend alors tout son sens :

@font-face {
// ...
src: url('fonts.svg#police1') format('svg');
}

@font-face {
// ...
src: url('fonts.svg#police2') format('svg');
}

Les ancres étant de toute façon nécessaires pour s'assurer de la compatibilité cross-browser de cette déclaration (notamment chez Opera), n'utiliser qu'un seul fichier SVG est un pur bénéfice !

Améliorer la validation des formulaires côté client avec FuelPHP

La création et la gestion de formulaires HTML fait bien évidemment partie des nombreux atouts de FuelPHP : le module en charge de ce travail a d'ailleurs l'intelligence d'appliquer l'attribut HTML5 required aux champs signalés comme obligatoires en ce qui concerne la validation côté serveur. En revanche, celle-ci comporte d'autres règles (maxlength, pattern avec une expression régulière...) possédant des équivalents côté client qui, pour leur part, ne sont pas répliqués. Voyons comment changer cela !

Le code concerné se trouve dans la méthode add_rule de la classe Field, située dans le fichier fuel/core/classes/fieldset/field.php, il suffit donc d'étendre cette classe côté application en réécrivant ladite méthode. Pour ce faire, créez le fichier fuel/app/classes/fieldset/field.php et éditez-le ainsi :

<?php

class Fieldset_Field extends Fuel\Core\Fieldset_Field
{
public function add_rule($callback)
{
$args = array_slice(func_get_args(), 1);
$this->rules[] = array($callback, $args);

switch ($callback) {
case 'required':
$this->set_attribute('required', 'required');
break;

case 'max_length':
$this->set_attribute('maxlength', $args[0]);
break;

case 'match_pattern':
$this->set_attribute('pattern', $args[0])->set_attribute('title', $args[0]);
break;

case 'min_length':
if ($this->get_attribute('pattern') === null) {
$this->set_attribute('pattern', '.{'.$args[0].',}');
}
break;
}

return $this;
}
}

Nous venons donc de remplacer le simple test sur l'attribut required présent à l'origine par une structure switch testant les différentes règles de validation gérées par FuelPHP que nous voulons implémenter côté client.

Nous allons également en profiter pour modifier la méthode utilisée par la règle match_pattern : les expressions régulières devant être entourées d'un délimiteur en PHP mais pas dans le code HTML, ajoutons ce délimiteur directement avec ladite méthode, qui se trouve dans la classe Validation du fichier fuel/core/classes/validation.php (à étendre tout comme la précédente) :

<?php

class Validation extends Fuel\Core\Validation
{
public function _validation_match_pattern($val, $pattern)
{
return $this->_empty($val) || preg_match('~'.$pattern.'~', $val) > 0;
}
}

Pour terminer, il suffit de déclarer l'extension à FuelPHP via le fichier fuel/app/bootstrap.php : je vous invite à vous référer , dont la fin décrit une démarche identique.

Vous obtenez ainsi une validation plus riche côté client ! D'autres règles de validation de FuelPHP sont évidemment implémentables de la même façon, n'hésitez pas à partager votre code le cas échéant.

Rendre les URLs par défaut sensibles à la casse avec FuelPHP

En utilisant FuelPHP, je me suis rendu compte qu'une URL ne dépendant pas d'une règle de routage personnalisée (c'est-à-dire traduite directement en noms de contrôleurs et de méthodes) n'était pas sensible à la casse. Les maniaques comme moi y verront sans nul doute un risque potentiel de duplicate content... Heureusement, la flexibilité du framework fait que l'on peut modifier ce comportement très simplement !

Primo, créons le fichier fuel/app/classes/router.php, et éditons-le comme suit :

<?php

class Router extends Fuel\Core\Router
{
protected static function parse_segments($segments, $namespace = '', $module = false)
{
$temp_segments = $segments;
$case_sensitive = \Config::get('routing.case_sensitive', true);

foreach (array_reverse($segments, true) as $key => $segment) {
if (($case_sensitive) && ($segment != mb_strtolower($segment))) {
return false;
}

$class = $namespace.'Controller_'.\Inflector::words_to_upper(implode('_', $temp_segments));
array_pop($temp_segments);

if (class_exists($class)) {
return array(
'controller' => $class,
'action' => isset($segments[$key + 1]) ? $segments[$key + 1] : null,
'method_params' => array_slice($segments, $key + 2),
);
}
}

if ($module) {
$class = $namespace.'Controller_'.ucfirst($module);

if (class_exists($class)) {
return array(
'controller' => $class,
'action' => isset($segments[0]) ? $segments[0] : null,
'method_params' => array_slice($segments, 1),
);
}
}
return false;
}
}

Les deux lignes importantes (les seules ajoutées à la fonction d'origine) sont celle où l'on déclare la variable $case_sensitive (pour récupérer la valeur du paramètre de configuration éponyme), et celle où on l'utilise pour vérifier le cas échéant la valeur de chaque segment de l'URL courante. En retournant false si l'un d'entre eux comporte des majuscules, on déclenchera automatiquement une erreur 404 bienvenue.

Il ne nous reste qu'à informer FuelPHP de l'existence de cette extension de classe, en modifiant le fichier fuel/app/bootstrap.php :

Autoloader::add_classes(array(
// Add classes you want to override here
// Example: 'View' => APPPATH.'classes/view.php',
'Router' => APPPATH.'classes/router.php'
));

Notez bien qu'en toute logique, ceci s'appliquera également aux segments de l'URL correspondant aux éventuelles variables GET ; si ce comportement est gênant dans votre cas, il suffira d'affiner un peu la vérification effectuée.

Créer une extension Twig pour FuelPHP

À compter de sa version 1.1, le framework PHP FuelPHP dispose du package Parser, qui lui permet d'utiliser un moteur de templates externe. Compatible avec un certain nombre d'entre eux, il l'est notamment avec celui qui nous intéresse aujourd'hui : Twig, le moteur de templates de Symfony2.

Si l'installation de ce dernier au sein du framework est plutôt évidente, même sans passer par Composer (télécharger Twig, extraire le sous-répertoire lib/Twig de l'archive obtenue, le placer tel quel dans fuel/app/vendor, ajouter si nécessaire Parser à l'autoload, sabrer le champagne), le moyen d'utiliser des extensions de Twig peut le paraître nettement moins, alors qu'il n'en est rien ! Voyez plutôt :

Commençons par écrire notre extension ; voici l'exemple (bidon) que j'ai utilisé pour mes tests :

<?php

class MyExtension extends \Twig_Extension
{
public function getName()
{
return 'my_extension';
}

public function getFilters()
{
return array(
'my_filter' => new \Twig_Filter_Function('my_function')
);
}
}

function my_function($s)
{
$salt = 'FuelPHP rules';
return md5($salt.$s);
}

Par souci de simplicité, j'ai placé ce fichier directement dans fuel/app/classes/myextension.php. Copions ensuite le fichier de configuration fuel/packages/parser/config/parser.php dans fuel/app/config/parser.php, et éditons ce dernier pour y déclarer notre extension :

'View_Twig' => array(
// ...
'extensions' => array(
'Twig_Fuel_Extension',
'MyExtension'
),
),

Et c'est tout ! Vous pouvez désormais utiliser le filtre my_filter directement dans vos templates Twig sous FuelPHP.