RedBean : déterminer si un type de bean possède une propriété

L'un des gros avantages de l'ORM PHP RedBean 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.

Surcharger une route d'un contrôleur séparé dans Silex

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 (($route->getPath() == $path) && (count(array_intersect($methods, $route->getMethods())))) {
$methods_diff = array_diff($route->getMethods(), $methods);

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.

Un équivalent à nl2br pour Ruby on Rails utilisant simple_format

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 :

def nl2br(s, html_options = {})
simple_format(s, html_options, :sanitize => false).gsub(/<\/p>(\s*)<p([^>]*)>/, '<br /><br />')
end

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 :

<%= nl2br(my_content, { :class => 'americaine' }).html_safe %>

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 :

<%= nl2br(my_content).gsub(/<script([^>]*)>(.*?)<\/script>/u, '').html_safe %>

Tester la valeur d'un champ booléen en base de données avec Ruby on Rails et SQLite

Si vous développez un projet utilisant le framework Ruby on Rails et le SGBD SQLite, et qu'à cette occasion vous stockez des valeurs booléennes en base de données (correspondant à autant de champs :boolean dans vos modèles), vous constaterez peut-être qu'une requête comme celle-ci :

articles = Article.all(:conditions => 'active = 1')

...ne fonctionne pas comme vous l'attendiez. En effet, dans un tel environnement, les valeurs booléennes ne sont pas stockées en tant que TINYINT prenant 0 ou 1 comme valeur, mais plutôt comme VARCHAR(1) contenant 't' ou 'f'.

Vous pouvez bien sûr simplement modifier votre requête "en dur", mais cela ne vous garantit pas un fonctionnement correct indépendamment du SGBD utilisé (et il y a fort à parier que SQLite ne fera plus partie du paysage une fois en production).

Dans ce cas, il est préférable de passer directement la valeur booléenne attendue, et de laisser Rails se charger de la préparation de la requête :

Article.all(:conditions => ['active = :active', { :active => true }])

Ainsi, vous pouvez même utiliser une variable ou le retour d'une fonction de manière plus directe !

Styler un champ d'upload en CSS

En préambule, je tiens à rassurer ceux qui risquent l'énucléation à la vue du titre de cet article : non, je n'ai pas découvert de solution miracle capable d'outrepasser les limites intrinsèques des navigateurs quant à l'application de styles CSS sur les input[type=file] ! Il s'agit simplement d'une astuce que tout le monde ne connaît peut-être pas et peut se révéler un atout précieux dans la tâche fastidieuse qu'est le design de formulaires HTML.

L'idée est tout simplement de masquer l'input originel, et de transférer grâce à JavaScript son contrôle à d'autres éléments mis en place à cet effet. Commençons par le HTML :

<label>
<input type="file" name="my_file" id="upload" />
<input type="text" id="fake_upload" disabled="disabled" />
<button>Parcourir</button>
</label>

Ce markup est peut-être discutable, étant considéré comme invalide par le W3C : il me paraît personnellement sensé, s'agissant d'un cas très spécifique. À vous de voir !

Voyons ensuite le code CSS lié au fameux champ d'upload. Je passe volontairement sur le champ texte et le bouton, l'objectif étant de leur donner l'apparence que vous souhaitez !

label [type=file] {
position: absolute;
width: 0;
height: 0;
}

Pourquoi s'ennuyer à jouer sur les dimensions alors qu'il suffirait d'un classique mais efficace display: none, me demanderez-vous ? Tout simplement parce sur certains navigateurs (notamment Google Chrome sur Android à l'heure où j'écris ces lignes), les éléments masqués ne peuvent être la cible d'évènements JS. Le positionnement absolu est là pour sortir l'input du flux afin d'éviter un éventuel décalage de quelques pixels (pouvant se produire, et je choisis un exemple tout à fait au hasard, sur Internet Explorer).

Passons maintenant au code JS, pour lequel nous nous appuierons sur jQuery. La difficulté ici est que certains navigateurs (comme par exemple Google Chrome) transmettent automatiquement l'évènement click sur le label à l'input[type=file] qu'il contient, alors que d'autres (tel Mozilla Firefox) n'en font rien. Il nous faut donc ruser un brin :

$('label').on('click', function(e)
{
// Vérifions en premier lieu la cible "réelle" du clic/toucher
var target = e.target || e.srcElement;

// S'il s'agit du bouton, on transmet au champ d'upload
if ($(target).is('button')) {
$(this).find('[type=file]').click();
}

// Lors du clic simulé ci-dessus, ce même handler est appelé à nouveau
// On empêche donc l'éventuel comportement par défaut sauf dans ce cas précis
return $(target).is('[type=file]');
});

$('label [type=file]').on('change', function()
{
// Lorsqu'un fichier est choisi, on remplit notre "faux" input avec son nom
$(this).siblings('[type=text]').val($(this).val());
});

Cette astuce a été testée sans succès sur IE (dans ses versions 8 et 9) : si tout semble fonctionner en apparence, ce dernier refusera catégoriquement de procéder correctement à l'upload du fichier si la pop-up de sélection n'a pas été ouverte via un clic sur l'élément natif (sans JavaScript). Si vous avez une solution, je suis preneur ! Dans l'intervalle, je vous recommande de masquer les éléments décoratifs et de réafficher le "vrai" input[type=file] pour IE.