Je viens d'achever le développement de Tao, un plugin jQuery permettant de modifier facilement le comportement d'un formulaire HTML. En effet, un simple appel du plugin suffit pour donner à ce formulaire la capacité de se soumettre via AJAX, en utilisant les éléments et attributs présents à l'origine. Le plugin accepte les mêmes options que la méthode ajax() de jQuery, ce qui est notamment utile pour la définition des callbacks.
Plus d'info sur la page GitHub du projet, sur laquelle vous pouvez aussi commenter et/ou forker si le coeur vous en dit !
Le framework frontend JavaScript MVCAngularJS a l'avantage d'être pensé de façon à pouvoir être utilisé uniquement sur un module donné, au lieu d'englober la totalité de l'application sur laquelle il est utilisé. Il est donc très pratique de s'en servir pour renforcer un élément complexe dans votre projet (comme un moteur de recherche devant interfacer une API JSON, par exemple). Néanmoins, il est parfois nécessaire de ruser un peu pour contourner son comportement lorsque le formulaire HTML que vous alimentez joyeusement à l'aide d'AngularJS est tout de même destiné à être envoyé côté serveur.
Tout d'abord, il faut savoir qu'AngularJS bloque automatiquement la soumission "classique" d'un formulaire sous son joug , sauf si celui-ci dispose d'un attribut action. Mais qu'en est-il si vous souhaitez déléguer la gestion du formulaire à AngularJS sous certaines conditions, ou plutôt jusqu'à un certain point ? Je pense notamment à la validation, un point que le framework gère très bien, mais pour lequel il peut être sympathique d'avoir un moyen de lui demander de nous rendre la main quand sa tâche est accomplie.
Il y a plusieurs solutions envisageables ; de mon côté, je privilégie la suivante, qui est limpide et facilement réutilisable. Voici la balise d'ouverture du formulaire :
Notez l'attribut personnalisé data-target qui vient remplacer l'action, ainsi que le listener pour l'évènement de soumission. Voici la méthode appelée par ce dernier, qui pour notre exemple se situe directement dans notre contrôleur :
$scope.submit = function() {
var $form = $element.find('form'); // à ajuster
if (/* insérez ici vos conditions */) {
$form.attr('action', $form.data('target')).submit(); // le formulaire sera désormais soumis de manière classique
}
};
Simplissime, non ? D'aucuns souligneront que procéder ainsi équivaut à un viol potentiel (pour toi, Google) du principe de base d'AngularJS qui interdit toute manipulation du DOM dans un contrôleur. À mes yeux, il s'agit plus d'un tour de passe-passe qu'autre chose, mais c'est vous qui voyez.
...d'autant que transformer ce code en une directive ne représente pas un obstacle insurmontable.
moi du Futur
Un autre point qui peut être surprenant lors des premiers pas avec AngularJS concerne la façon dont celui-ci remplit les select. En effet, aucun contrôle n'est possible sur l'attribut value des éléments option générés par la directive correspondante, à savoir ngOptions. Plutôt que de chercher un chemin de traverse compliqué, faisons preuve d'astuce :
<!-- Ne faites pas : -->
<select name="my_key" ng-model="myModel" ng-options="select o.label for o in myOptions"></select>
<!-- Mais faites plutôt : -->
<select ng-model="myModel" ng-options="select o.label for o in myOptions"></select>
<input type="hidden" name="my_key" value="{{ myModel }}" />
Ceci vous permet de gérer indépendamment le select et la valeur correspondante qui sera envoyée au serveur.
L'un des gros avantages de l'ORM PHPRedBean est la grande souplesse de son fonctionnement : vous n'avez jamais à vous soucier du schéma de votre base de données avant la mise en production, celui-ci étant généré au fur et à mesure du développement et des requêtes qui y sont effectuées par son intermédiaire. Toutefois, cet avantage peut devenir handicapant si, pour une raison ou un autre, vous avez besoin de savoir si tel ou tel type de "bean" possède (potentiellement, selon le schéma de la table correspondante) une propriété donnée.
Je vous propose aujourd'hui de surcharger la classe RedBean_Facade (R de son petit nom lors de l'utilisation de l'outil) afin de lui ajouter cette possibilité :
<?php
namespace My\Name\Space;
class RedBean extends \RedBean_Facade
{
public static function typeHasField($type, $field)
{
//return !! self::getCell('SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ? AND COLUMN_NAME = ?', array($type, $field));
// Mettons plutôt en oeuvre les moyens mis à disposition par RedBean, afin de ne pas se limiter à MySQL
try {
$columns = self::getColumns($type);
return isset($columns[$field]);
} catch (\RedBean_Exception_SQL $e) {
// La table n'existe pas encore
return false;
}
}
}
Cette nouvelle méthode s'utilisera tout bêtement ainsi :
use My\Name\Space\RedBean as R;
$type = $bean->getMeta('type'); // si vous ne le connaissez pas directement
if (R::typeHasField($type, 'fieldname')) {
// ...
Cette solution a été testée et fonctionne sur une base de données MySQL.
Si vous développez des applications web de moyenne envergure à l'aide du micro-framework PHP5.3+Silex, vous n'ignorez probablement pas (n'est-ce pas ?) que la structure de celui-ci peut être découplée, passant d'un unique fichier index.php à une multitude de contrôleurs distincts.
Tout cela est bien beau, mais peut l'être encore plus si vous organisez vos contrôleurs de manière à faire certains d'entre eux hériter d'autres. Ceci peut être particulièrement utile si vous avez plusieurs contrôleurs dont les fonctionnalités sont très similaires. Vous pouvez par exemple procéder ainsi :
<?php
namespace MyCoolApp\Controller;
// je zappe volontairement les use pour gagner du temps
abstract class AbstractController implements ControllerProviderInterface
{
public function connect(Application $app)
{
// Cette méthode est appelée par défaut par Silex lorsque vous "montez" un contrôleur dans votre application.
return $this->route($app);
}
protected function route($app, $extra_params = null)
{
$ctrl = $app['controllers_factory'];
$ctrl->get(
'/my-first-uri',
function () use ($app, $extra_params) {
// ...
}
);
$ctrl->get(
'/my-second-uri',
function () use ($app, $extra_params) {
// ...
}
);
return $ctrl;
}
}
<?php
namespace MyCoolApp\Controller;
// idem
class TrueController extends AbstractController
{
protected function route($app, $extra_params = array('param1' => 'value1', 'param2' => 'value2'))
{
// En une ligne, nous assignons à ce contrôleur toutes les routes déclarées dans son parent
// Celles-ci récupèrent du même coup des paramètres supplémentaires propres à chaque classe fille
$ctrl = parent::route($app, $extra_params);
// Nous pouvons déclarer d'autres routes ici le cas échéant...
return $ctrl;
}
}
Plutôt sympa, non ? Pourtant, il reste un souci de taille : lorsqu'il sélectionne la route qui sera utilisée pour répondre à une requête, Silex prend la première qu'il trouve. Dans ce que nous venons de mettre en place, les routes du parent sont définies avant celles de l'enfant ; ainsi, si nous faisons la chose suivante :
// Dans le parent
$ctrl->post(
'/my-post-uri',
function () use ($app) {
return new Response('Luke, je suis ton père !');
}
);
// Dans l'enfant (roooh)
$ctrl->post(
'/my-post-uri',
function () use ($app) {
return new Response('Areuh areuh');
}
);
Lorsqu'on appellera /my-post-uri dans le contrôleur (enfant) concerné, c'est le code du parent qui s'exécutera et la célèbre réplique de film qui s'affichera sous nos yeux ébahis.
Notez que dans certains cas (et notamment sur des routes utilisant la méthode GET), la version de l'enfant supplantera celle du parent. Je vous avoue ne pas avoir suffisamment plongé dans le code pour m'expliquer pourquoi, mais je serai ravi de l'apprendre si vous avez la réponse.
Il faudrait donc que nous ayons la possibilité de "supprimer" une route déclarée préalablement dans un contrôleur, afin de pouvoir la supplanter dans un tel cas. Hélas, $app['controllers_factory'], en tant qu'instance de Silex\ControllerCollection, ne nous le permet pas. Qu'à cela ne tienne, nous allons étendre cette classe afin de le lui apprendre ! Nous nommerons la méthode idoine cancel, afin d'éviter toute confusion avec la méthode delete :
<?php
namespace MyCoolApp\Whatever;
use Silex\ControllerCollection as BaseControllerCollection;
class ControllerCollection extends BaseControllerCollection
{
public function cancel($path, $methods = array('GET', 'POST', 'PUT', 'DELETE'))
{
$methods = array_map('strtoupper', (array)$methods);
foreach ($this->controllers as $key => $controller) {
$route = $controller->getRoute();
// La route courante nous intéresse si :
// - son path est identique à celui recherché
// - elle emploie une ou plusieurs méthodes parmi celles que nous voulons annuler pour ledit path
if (! count($methods_diff)) {
// Si nous éliminons toutes les méthodes pour cette route, nous pouvons la faire disparaître totalement
unset($this->controllers[$key]);
} else {
// Sinon, on redéfinit ses méthodes avec ce qui reste
$controller->getRoute()->setMethods($methods_diff);
}
}
}
return $this; // pour pouvoir chaîner cette méthode avec d'autres
}
}
Il nous faut ensuite, dans index.php, indiquer à Silex d'utiliser cette classe :
$app = new Silex\Application();
$app['controllers_factory'] = function () use ($app) {
return new MyCoolApp\Whatever\ControllerCollection($app['route_factory']);
};
// Faites ensuite vos $app->mount(...)
Enfin, cette méthode s'utilisera ainsi, dans la classe fille :
$ctrl->cancel('/my-post-uri', 'post');
$ctrl->post(
'/my-post-uri',
function() use ($app) {
return new Response('Areuh areuh');
}
);
Cette fois, la même requête recevra bien la réponse attendue.
Si vous avez fait vos premières armes en développement web sur PHP et avez fini par rejoindre les rangs des adeptes de Ruby on Rails, il est possible que la bonne vieille méthode nl2br vous manque un peu...
Rails propose, quant à lui, simple_format, qui transforme automatiquement vos sauts de ligne en balises HTML et encapsule votre contenu dans un paragraphe. Hélas, cette dernière traite systématiquement deux sauts de ligne consécutifs comme un nouveau paragraphe, et n'offre pas la possibilité de modifier ce comportement.
Voici donc une petite méthode à insérer dans un helper de votre projet et qui vous permettra de retrouver vos marques. Elle fonctionne comme simple_format, mais n'insère que des <br /> (un par \n) et aucun dans votre contenu :
Notez que dans mon cas, je passe d'office la valeur false au paramètre :sanitize des options de simple_format, au cas où la chaîne de caractères à traiter contiendrait déjà du HTML auquel je voudrais faire subir le même traitement. À vous d'ajuster selon votre préférence !
Cette méthode sera utilisée comme ceci dans vos templates ERB :
D'aucuns objecteront (à raison) que html_safe est trop permissif dans le cas d'une entrée utilisateur, raison pour laquelle je n'ai pas inclus son appel dans le helper. Le cas échéant, il vous est toujours possible de supprimer certaines balises indésirables avec, par exemple :