[Widget] sfWidgetFormDoctrineJQueryAutocompleterAndClear

Pour faire suite à mon billet sur le widget sfWidgetFormDoctrineJQueryAutocompleter, je vous propose mon petit widget sfWidgetFormDoctrineJQueryAutocompleterAndClear.

Il propose un bouton permettant de vider le contenu du champ texte mais également du champ caché d’autocompletion.

Il fonctionne exactement de la même manière que le widget original, il ne faut rien de plus et rien de moins (l’icône est intégrée en base64).

Voici le code :

class sfWidgetFormDoctrineJQueryAutocompleterAndClear extends sfWidgetFormDoctrineJQueryAutocompleter
{
  public function render($name, $value = null, $attributes = array(), $errors = array())
  {
    return parent::render($name, $value, $attributes, $errors).
           ' <img src="data:image/png;base64,'.$this->getImage().'" alt="" id="'.$this->getDeleteId($name).'" />'.
           sprintf(<<<EOF
<script type="text/javascript">
  $("#%s").bind("mouseover", function(e){
    $("#%s")[0].style.cursor = "pointer";
  });
  $("#%s").bind("click", function(e){
    $("#%s")[0].value = "";
    $("#%s")[0].value = "";
    return false;
  });

</script>
EOF
      ,
      $this->getDeleteId($name),
      $this->getDeleteId($name),
      $this->getDeleteId($name),
      $this->generateId('autocomplete_'.$name),
      $this->generateId($name)
    );
  }

  private function getDeleteId($value)
  {
    return $this->generateId("delete_autocomplete_value_".$value);
  }

  private function getImage()
  {
    return "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJdSURBVDjLpZP7S1NhGMf9W7YfogSJboSEUVCY8zJ31trcps6zTI9bLGJpjp1hmkGNxVz4Q6ildtXKXzJNbJRaRmrXoeWx8tJOTWptnrNryre5YCYuI3rh+8vL+/m8PA/PkwIg5X+y5mJWrxfOUBXm91QZM6UluUmthntHqplxUml2lciF6wrmdHriI0Wx3xw2hAediLwZRWRkCPzdDswaSvGqkGCfq8VEUsEyPF1O8Qu3O7A09RbRvjuIttsRbT6HHzebsDjcB4/JgFFlNv9MnkmsEszodIIY7Oaut2OJcSF68Qx8dgv8tmqEL1gQaaARtp5A+N4NzB0lMXxon/uxbI8gIYjB9HytGYuusfiPIQcN71kjgnW6VeFOkgh3XcHLvAwMSDPohOADdYQJdF1FtLMZPmslvhZJk2ahkgRvq4HHUoWHRDqTEDDl2mDkfheiDgt8pw340/EocuClCuFvboQzb0cwIZgki4KhzlaE6w0InipbVzBfqoK/qRH94i0rgokSFeO11iBkp8EdV8cfJo0yD75aE2ZNRvSJ0lZKcBXLaUYmQrCzDT6tDN5SyRqYlWeDLZAg0H4JQ+Jt6M3atNLE10VSwQsN4Z6r0CBwqzXesHmV+BeoyAUri8EyMfi2FowXS5dhd7doo2DVII0V5BAjigP89GEVAtda8b2ehodU4rNaAW+dGfzlFkyo89GTlcrHYCLpKD+V7yeeHNzLjkp24Uu1Ed6G8/F8qjqGRzlbl2H2dzjpMg1KdwsHxOlmJ7GTeZC/nesXbeZ6c9OYnuxUc3fmBuFft/Ff8xMd0s65SXIb/gAAAABJRU5ErkJggg==";
  }
}

Et à quoi cela ressemble au final :

sfWidgetFormTextareaTinyMCEWithLang

Je souhaitais vous faire partager mon petit widget TinyMCE basé sur celui intégré au plugin sfFormExtraPlugin.

Pourquoi réécrire un widget existant ? Parce que celui-ci n’acceptait pas le paramètre de langue !

Quelque part, il s’agit plus d’un correctif du widget mais bon, il s’avère que j’en ai souvent besoin donc je le partage ici :

class sfWidgetFormTextareaTinyMCEWithLang extends sfWidgetFormTextarea
{
  protected function configure($options = array(), $attributes = array())
  {
    $this->addOption('theme', 'advanced');
    $this->addOption('language', 'en');
    $this->addOption('width');
    $this->addOption('height');
    $this->addOption('config', '');
  }

  public function render($name, $value = null, $attributes = array(), $errors = array())
  {
    $textarea = parent::render($name, $value, $attributes, $errors);

    $js = sprintf(<<<EOF
<script type="text/javascript">
  tinyMCE.init({
    mode:                              "exact",
    elements:                          "%s",
    language:                          "%s",
    theme:                             "%s",
    %s
    %s
    theme_advanced_toolbar_location:   "top",
    theme_advanced_toolbar_align:      "left",
    theme_advanced_statusbar_location: "bottom",
    theme_advanced_resizing:           true
    %s
  });
</script>
EOF
    ,
      $this->generateId($name),
      $this->getOption('language'),
      $this->getOption('theme'),
      $this->getOption('width')  ? sprintf('width:                             "%spx",', $this->getOption('width')) : '',
      $this->getOption('height') ? sprintf('height:                            "%spx",', $this->getOption('height')) : '',
      $this->getOption('config') ? ",\n".$this->getOption('config') : ''
    );

    return $textarea.$js;
  }
}

Et pour l’utiliser :

  • dans votre classe de formulaire
$this->widgetSchema['widget'] = new sfWidgetFormTextareaTinyMCEWithLang(sfConfig::get('app_tinymce_config'));
  • dans le fichier app.yml
all:
  tinymce:
    width:  550
    height: 125
    language: fr
    config: |
      plugins: "paste",
      paste_auto_cleanup_on_paste : true,
      theme_advanced_buttons1: "bullist,pasteword,bold,italic,underline,fontselect,fontsizeselect",
      theme_advanced_buttons2: "",
      theme_advanced_buttons3: "",
      theme_advanced_buttons4: ""
    theme: <?php echo sfConfig::get('app_tinymce_theme','advanced') ?>

[Admin generator] Paramètre ‘table method’

Une petite astuce que j’utilise depuis longtemps mais que je n’avais jamais pensé à partager : optimiser le nombre de requêtes Doctrine et par la même occasion, un module d’admin generator.

J’ai un module d’admin generator article lié au modèle Article.

  • schema.yml
Article:
  connection: doctrine
  actAs:
    Timestampable: ~
    Taggable: ~
    Sluggable:
      fields: [titre]
      name: slug
      type: string
      length: 255
      unique: true
      canUpdate: true
  columns:
    titre:
      type: string(255)
      notnull: true
    # et d'autres champs
    id_auteur:
      type: integer
      notnull: true
  relations:
    Auteur:  { class: vjGuardUserProfile, onDelete: SET NULL, onUpdate: CASCADE, local: id_auteur, foreign: id }
  • generator.yml
 list:
 title: Liste des articles
 display: [titre, date_publication, Auteur]
 max_per_page: 5
 table_method: retrieveBackend
  • ArticleTable.class.php
public function retrieveBackend(Doctrine_Query $q)
{
  $rootAlias = $q->getRootAlias($q);
  $q->leftJoin($rootAlias.'.Auteur au');
  return $q;
}

Dans cet exemple, je passe de 6 requêtes Doctrine pour l’affichage de 5 lignes à 3 requêtes.

Ce n’est pas énorme mais sur 20 lignes et 3 relations, on passe de 63 requêtes à

3 requêtes !

Sisi !

Alors, abusez-en !

symfony 1.3.6 et 1.4.6 publiées (security fix inside)

Deux nouvelles mises à jour de symfony ont été publiées ce soir. Estampillées 1.3.6 et 1.4.6, elles corrigent une faille dans la gestion du cache des templates.

Après la dernière mise à jour, c’est une nouvelle fois dans l’urgence que sont corrigées des failles de sécurité !

Il est temps de mettre à jour !

Source

Symfony 2 n’existe pas ! Symfony2 oui !

Votre mission si vous l’acceptée sera de ne plus parler de Symfony 2 mais de Symfony2 !

MYSQL – Importer des données en latin dans une base en utf8

Une petite astuce que je viens de découvrir et qui me sauve la vie !

Problème : J’ai une base en latin1 et une réplication en utf8. Comment migrer les données de l’une vers l’autre en ligne de commande (sur deux serveurs de version totalement différentes) ?

Solution :

# on dump la base initiale en latin1
mysqldump -h host1 -u login -p'password' database > save.sql
# on importe les données dans la nouvelle base en utf8
mysql -h host2 -u login -p'password' --default_character_set utf8 database < save.sql

Petit mais costaud !

[Tuto] Bien utiliser le widget sfWidgetFormDoctrineJQueryAutocomplete

Enfin un tutoriel ! Cela faisait bien longtemps que je n’en avais pas publié (par faute de temps …) !

Sujet du jour : sfWidgetFormDoctrineJQueryAutocomplete

Ce widget propose un champ input de type text qui, lorsque vous tapez des données, effectue une recherche en ajax dans la base de données (ici, nous travaillons avec Doctrine) et dans le cas de résultats, qui vous les retranscrit dans une liste placée sous le champ input.

L’idée étant de rechercher rapidement et simplement des données dans une liste plus ou moins longue. Qui plus est, cela permet de charger plus rapidement la page et par la même occasion, d’éviter de créer des listes très longues.

Voici une représentation du widget en fonction :

Alors, pour le mettre en œuvre, il faut au préalable installer le plugin sfFormExtraPlugin qui l’intègre (l’installation est standard).

Pour bien comprendre la technique, je vais vous décrire chaque étape importante et vous fournir la structuration des données.

  • le schéma de données (schema.yml)
options:
  collate: utf8_general_ci
  charset: utf8
Maitre:
  connection: doctrine
  columns:
    titre:
      type: varchar(255)
      notnull: true
Eleve:
  connection: doctrine
  columns:
    titre:
      type: varchar(255)
      notnull: true
    id_maitre:
      type: integer
      notnull: true
  relations:
    Maitre: { onDelete: cascade, onUpdate: cascade, local: id_maitre, foreign: id }

J’ai ensuite généré ma base de données et deux modules d’admin generator avec Doctrine.
Je me retrouve donc avec un module de gestion des Maîtres et un autre des Elèves. Du fait du schéma, lorsqu’on rajoute un élève, on doit choisir son maître parmi la liste déroulante de maîtres. Nous placerons donc ici notre autocomplete.

Avant de l’intégrer, voici comment se présente le formulaire d’ajout d’élève :

La première chose qu’on travaille généralement dans ce cas est l’affichage d’un texte à la place des identifiants :

  • lib/model/doctrine/Maitre.class.php
class Maitre extends BaseMaitre
{
  public function __toString()
  {
    return $this->titre;
  }
}

Ce qui nous donne :

Jusque là, rien de particulier mais la liste paraît assez longue tout de même (899 entrées !) et en environnement dev, cache vidé, cette simple page met plus de 0,8s à apparaître. C’est trop.

Remplaçons cette liste déroulante par un champ autocomplete !

  • lib/form/doctrine/EleveForm.class.php
  public function configure()
  {
    $this->widgetSchema['id_maitre']->setOption('renderer_class', 'sfWidgetFormDoctrineJQueryAutocompleter');
    $this->widgetSchema['id_maitre'] = new sfWidgetFormDoctrineJQueryAutocompleter(
      array(
        'model' => "Maitre",
        'url'   => url_for("@ajax_maitre"),
        'config' => '{ max: 50}'
      )
    );
  }

On remarque qu’on remplace bien le widget d’origine (sfWidgetFormDoctrineChoice) par l’autocomplete.

On remarque également qu’on lui passe en option le modèle avec lequel travaillé et une route symfony mais qu’on peut également lui passer le nombre de lignes à afficher (ici 50).

Utilisant le helper Url, il faut penser à le charger ici :

  public function configure()
  {
    sfProjectConfiguration::getActive()->loadHelpers('Url');
    // ....
  }

Il faut donc maintenant créer cette route

  • apps/your_app/config/routing.yml
ajax_maitre:
  url: /ajax-maitre
  param: { module: maitre, action: ajaxMaitre }

Cette route est simple à comprendre : on charge la méthode executeAjaxMaitre du fichiers actions.class.php du module maitre.

Créons donc cette méthode alors !

  • apps/your_app/modules/maitre/actions/actions.class.php
  public function executeAjaxMaitre(sfWebRequest $request)
  {
    // si ce n'est pas une requête ajax, on renvoit vers du 404
    $this->forward404unless($request->isXmlHttpRequest());
    // c'est de l'ajax, on retourne donc du json
    $this->getResponse()->setContentType('application/json');
    $choices = array();
    // on récupère les données de la base de données
    $maitres = Doctrine::getTable('Maitre')->getMaitreAutocompletion($request->getParameter('q'), $request->getParameter('limit'));
    // on boucle sur les données et on charge les données dans un nouveau tableau, celui qu'on va retourner
    foreach($maitres->getData() as $m)
    {
      $choices[$m->id] = $m->titre;
    }
    // s'il y a des données, on retourne le tableau encodé en json
    if($choices != array())
    {
      return $this->renderText(json_encode($choices));
    }
  }

Il nous reste à créer la méthode getMaitreAutocompletion du modèle Maitre

  • lib/model/doctrine/MaitreTable.class.php
  public function getMaitreAutocompletion($q, $limit){
    $dq = Doctrine_Query::create()
            ->select("m.id, m.titre")
            ->from("Maitre m")
            ->where("m.titre LIKE ?","%".$q."%")
            ->limit($limit)
            ->orderBy('m.titre ASC');
    return $dq->execute();
  }

Dernière chose importante avant de tester, il faudrait peut-être intégrer jQuery non ? Commençons par télécharger la dernière version minifiée sur le site officiel, copions là dans web/js/jquery.min.js puis

  • apps/your_app/config/view.yml
  javascripts:    [jquery.min.js]

That’s all folks !

Regardons alors le résultat :

Quoi de plus simple, je vous le demande !

Ainsi s’achève ce petit tutoriel qui, je l’espère, vous sera bien utile !

A bientôt pour de nouvelles aventures sur symfony !

EDIT : Merci à Jonathan T pour l’astuce concernant le nombre de lignes affichées lors de l’autocompletion !

Bienvenue sur la nouvelle version de mon blog !

Adieu OverBlog ! Bonjour OVH et Wordpress !

Il était temps de faire quelque chose pour rendre ce petit blog plus friendly user et surtout, pour me permettre de vous faire partager proprement et lisiblement du code (merci SyntaxHighlighter !).

Je ne vais pas promettre des posts toutes les semaines, je ne pourrai pas mais par contre, j’ai déjà quelques idées sur mes prochains billets (principalement sur du formulaire, je ne vous en dis pas plus !).

Welcome back and see you soon !

MYSQL – Modifier le character set de tous les champs/tables d’une database

Je pense que comme la plupart des utilisateurs de MySQL, il vous est arrivé de devoir changer l’encodage des caractères d’une base de données, d’une table ou pire, des champs et quel cauchemar …

Je viens de trouver un joli petit script php qui le fait assez proprement. Il suffit de renseigner le character_set initial, celui recherché, les identifiants de connexion à la base de données puis de lancer le script et enfin, d’exécuter les requêtes fournies :

#!/usr/bin/php
<?php

// FICHIER convertCharset.php

// this script will output the queries need to change all fields/tables to a different collation
// it is HIGHLY suggested you take a MySQL dump prior to running any of the generated
// this code is provided as is and without any warranty

  set_time_limit(0);

  // Connexion BDD
	define("USER","username");
	define("PASS",'password');
	define("HOST","your_host_name");
	define("DB",$argv[1]);

  $path_tmp = "/tmp/";
  $mysql_params = "-h ".HOST." -u ".USER." -p'".PASS."'";

  // collation you want to change:
  $convert_from = 'latin1_swedish_ci';
  //$convert_from = 'utf8_unicode_ci';

  // collation you want to change it to:
  $convert_to   = 'utf8_general_ci';

  // character set of new collation:
  $character_set= 'utf8';

  $show_alter_table = true;
  $show_alter_field = true;

  // DB login information

  mysql_connect(HOST, USER, PASS);
  mysql_select_db(DB);

  $rs_tables = mysql_query(" SHOW TABLES ") or die(mysql_error());
  $sql = "";

  while ($row_tables = mysql_fetch_row($rs_tables)) {
      $table = mysql_real_escape_string($row_tables[0]);

      // Alter table collation
      // ALTER TABLE `account` DEFAULT CHARACTER SET utf8
      if ($show_alter_table) {
          $sql.= "ALTER TABLE `$table` DEFAULT CHARACTER SET $character_set;\r\n";
      }

      $rs = mysql_query(" SHOW FULL FIELDS FROM `$table` ") or die(mysql_error());
      while ($row=mysql_fetch_assoc($rs)) {

          if ($row['Collation']!=$convert_from)
              continue;

          // Is the field allowed to be null?
          if ($row['Null']=='YES') {
              $nullable = ' NULL ';
          } else {
              $nullable = ' NOT NULL';
          }

          // Does the field default to null, a string, or nothing?
          if ($row['Default']=='NULL') {
              $default = " DEFAULT NULL";
          } else if ($row['Default']!='') {
              $default = " DEFAULT '".mysql_real_escape_string($row['Default'])."'";
          } else {
              $default = '';
          }

          // Alter field collation:
          // ALTER TABLE `account` CHANGE `email` `email` VARCHAR( 50 ) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL
          if ($show_alter_field) {
              $field = mysql_real_escape_string($row['Field']);
              $sql.= "ALTER TABLE `$table` CHANGE `$field` `$field` $row[Type] CHARACTER SET $character_set COLLATE $convert_to $nullable $default; \r\n";
          }
      }
  }

system("mysqldump $mysql_params ".DB." > ".$path_tmp."before-conv-charset-".DB.".sql");
$fp = fopen($path_tmp.'conv-charset-'.DB.'.sql', 'w');
fwrite($fp, $sql);
fclose($fp);
system("mysql $mysql_params ".DB." < ".$path_tmp."conv-charset-".DB.".sql");
?>

Il ne reste plus qu’à exécuter le script :

php convertCharset.php DATABASE_NAME

Magie !

symfony 1.3.5 et 1.4.5 publiées (security fix inside)

Deux nouvelles mises à jour de symfony ont été publiées en milieu de soirée : 1.3.5 et 1.4.5.

Elles corrigent un certain nombre de tickets mais intègre surtout un fix de sécurité pour Doctrine et Propel !

Il est temps de mettre à jour !

Source