Drupal, renommer un champ CCK
CCK, comme tout drupalien le sait, permet d'étendre un type de contenu en ajoutant toute sorte de champs (dates, textes, images, liens, etc.). Ces champs portent un "nom machine" (field_XXX) utilisé comme nom de table ou de colonne dans la base de données, et bien évidement dans le code (généralement du thème).
Tout cela est très bien sauf qu'au bout d'un temps, entre les fautes de frappe, les modifications de spécifications, les nouvelles fonctionnalités, le nommage de ces champs peut manquer cruellement de cohérence. Et étrange, vous avez dit étrange, il n'existe dans CCK aucun moyen de les renommer.
Mais quelle idée ?!?
Lorsque je me suis renseigné pour savoir pourquoi manquait une fonctionnalité aussi basique, la réponse m'a assise par terre : "à cause de Views". Plus clairement, si les champs CCK sont utilisés dans une tripotée de vues, les renommer impliqueraient de les toutes les retoucher une à une. Voilà qui n'a pas vraiment amélioré mon appréciation de Views dans le cadre d'un site réalisés par des professionnels (à contrario d'une utilisation amateur de Drupal où views est très utile), mais ceci est une autre histoire.
L'autre raison qui m'a été évoqué est qu'un nom machine n'est qu'un nom machine, et qu'il n'a que besoin d'être unique pur une installation Drupal donnée. Un argument que je peux entendre même si pour moi la
Une autre raison qui rendait le renommage "peu utile" aurait sans doute été la faible taille des noms de champ, 32 caractères. Lorsque l'on sait que l'on perd déjà 6 caractères d'emblée (le préfixe "field_" dont je n'ai jamais compris l'intérêt), cela ne laisse pas grand chose pour faire un nommage propre (personnellement j'utilise field_{nom du type}_{nom du champ}). Une limitation qui doit dater d'un ancien temps car aujourd'hui pour MySQL et comme PostgreSQL, la taille maximum commune pour un nom de colonne est de 63 caractères. D'ailleurs si certains sont intéressés, j'ai fais un patch pour (je l'espère) corriger cela.
Bref, plein de raisons pour pas toucher au noms de champs et donc de ne pas implémenter dans CCK un système de renommage. Et de mon point de vue autant de raison de malgré tout vouloir garder un code propre et de le faire quant même...
Structure des champs CCK
Pour renommer un champ CCK, il faut un peu en comprendre la structure. Un champ est défini, entre autre, par son nom, son type et sa
- Si la cardinalité est de type 1:1 (Une seule valeur par champ), les données de ce champ seront stockées dans une table commune à tous les champs de cardinalité 1:1 d'un type de contenu donné nommée content_type_{nom du type de contenu}.
- Si la cardinalité est différente de 1:1 (de 2 à Multiples valeurs en CCK), les valeurs du champ sont stockées dans une table autonome nommée content_field_{nom du champ}.
Dans un cas comme dans l'autre, CCK va créer dans le table content_type_{nom du type de contenu} ou content_field_{nom du champ} les mêmes champs pour stocker les valeurs. La seule différence est que la table content_field_{nom du champ} disposera en plus d'un champ delta qui pour un même node (couple nid, vid) permet de différencier les N valeurs.
Reste les colonnes dédiés à la valeur du champ. Il peut y en avoir une seule, comme pour un type simple (entier ou chaîne sans formatage), jusqu'à un nombre illimité (par exemple 3 pour un champ de type filefield).
Processus de renommage
Le processus de renommage commence par déterminer sur quelle cardinalité se place le champ à renommer. Si c'est une cardinalité différente de 1:1, il nous faudra commencer par renommer la table dédiée :
ALTER TABLE content_field_nom_moche RENAME TO content_field_nom_jolirenommage d'une table/champ en PostgreSQL
La suite du processus est la même pour les deux types de stockage. Elle consiste tout d'abord à renommer les colonnes portant les valeurs du champ. Le nom de ces colonnes débutent tous par field_{nom du champ} suivi d'un caractère souligné et d'un identifiant (value pour un entier ou une chaîne, fid, list et data pour un champ image field, nid pour un champ nodereference, etc.). Le renommage de ces colonnes se fait sur la table content_type_{nom du type} pour un champ de cadinalité 1:1 ou sur la table que nous venons de renommer dans le cas contraire :
ALTER TABLE content_field_nom_joli RENAME COLUMN field_nom_moche_fid TO field_nom_joli_fid;
ALTER TABLE content_field_nom_joli RENAME COLUMN field_nom_moche_list TO field_nom_joli_list;
ALTER TABLE content_field_nom_joli RENAME COLUMN field_nom_moche_data TO field_nom_joli_data;Renommage des valeurs d'un champ fielfield
Dernière étape, elle aussi commune aux deux types de stockages, prévenir CCK du nouveau nommage. Cela consiste à remplacer l'ancien nom par le nouveau dans les tables content_node_field (la définition CCK d'un champ), content_node_field_instance (la définition CCK d'une instance de champ) et dans content_group_fields (la définition CCK d'un groupe de champs qui peut potentiellement contenir notre champ).
UPDATE content_node_field SET field_name='field_nom_joli' WHERE field_name='field_nom_moche'
UPDATE content_node_field_instance SET field_name='field_nom_joli' WHERE field_name='field_nom_moche'
UPDATE content_group_field SET field_name='field_nom_joli' WHERE field_name='field_nom_moche'Modification des définitions de champ CCK
Et voilà, c'est terminé.
Automatisation
Personnellement je me suis construit un petit module qui fait le travail à ma place car la fois où j'ai eu une centaines de champs à renommer, je n'allais pas faire tout cela à la main. Le module je le garde pour moi car je n'ai aucune envie d'en faire la maintenance :-) En revanche, voici sa procédure centrale pour PostgreSQL (à adapter pour MySQL).
function cck_rename_field($field_name, $new_name) {
// On demande à CCK la liste des champs
$fields = content_fields();
// On récupère notre champ à renommer
$field = $fields[$field_name];
$content_type = content_types($field['type_name']);
// un $field['db_storage'] à TRUE indique une cardinalité 1:1
if ($field['db_storage']) {
// Stockage par colonne : Sélection de la table à modifier
$table = "content_type_{$field['type_name']}";
}
else {
// Stockage par table : Renommage de la table
$old_table = "content_{$field_name}";
$table = "content_{$new_name}";
$query = "ALTER TABLE {{$old_table}} RENAME TO {{$table}}";
$result = db_query($query);
if (!$result) {
drupal_set_message(t("Unable to execute @query, sorry...", array('@query' => $query)));
return;
}
}
// On itére sur chaque colonne de stockage du champ
foreach ($field['columns'] as $column => $data) {
// Renommage de la colonne
$query = "ALTER TABLE {{$table}} RENAME COLUMN {$field_name}_{$column} TO {$new_name}_{$column}";
$result = db_query($query);
if (!$result) {
drupal_set_message(t("Unable to execute @query, sorry...", array('@query' => $query)));
return;
}
}
// On itére sur les 3 tables de définition de CCK pour change
// le nom du champ
foreach (array('content_node_field','content_node_field_instance','content_group_fields') as $table) {
$query = "UPDATE {{$table}} SET field_name='$new_name' WHERE field_name='$field_name'";
$result = db_query($query);
if (!$result) {
drupal_set_message(t("Unable to update @table, sorry...", array('@table' => $table)));
return;
}
}
// Ménage pour vider les caches CCK
content_clear_type_cache();
}procédure de renommage de champ
La procédure a été testé sur PostgreSQL 8.4, CCK 2.8 et mon patch pour atteindre les 63 caractères, mais soyez prudent quant à son utilisation, toujours faire des sauvegardes avant.
Conclusion
La conclusion de cette histoire serait sans doute un réflexion sérieuse sur le concept même de "nom machine". D'un point de vue pratique le besoin est réel. Il s'agit d'avoir pour des objets système (Champ, Context, Bloc, etc.) un identifiant unique pour une installation donnée et le même pour chaque machine où cette installation est répliquée. L'autre besoin réel est de disposer dans le code PHP d'identifiant, choisi par de développeur, permettant d'atteindre ces objets systèmes.
Là où l'approche adoptée par les modules (CCK, Context, Features, etc.) pose soucis est dans la résolution des deux problématiques en une seule passe. C'est à dire en laissant le développeur choisir un identifiant unique. Du coup tout modification de l'identifiant implique de nombreuse conséquence plus ou moins contrôlables, d'où le bridage posé par CCK interdisant son renommage. En même temps cet identifiant étant un aussi le nom "publique" de l'objet système utilisé dans notre code, empêcher son changement est aussi ridicule que de vous interdire de renommer une fonction ou une variable sous prétexte qu'elle est utilisée ailleurs.
Une solution possible pourrait consister à traiter les deux notions de manière autonome. Drupal pourrait proposer un SystemObjectAPI, permettant aux modules d'enregistrer des classes d'objets (champ, bloc, etc.) et des instances d'objet (champ XXX). L'API, lors de l'enregistrement d'une instance, créerait un
Cette API pourrait aussi fournir le moyen d'assigner un identifiant PHP à chaque instance d'objet système. Les modules utiliserait donc cet identifiant lorsqu'il s'agirait d'interagir avec le reste du monde. Dans le cas de CCK, ces identifiants seraient utilisés pour convertir les GUID en champs correspondant de l'objet Node.
Une telle API ne serait pas limité aux seuls identifiants PHP. Nous pourrions aussi imaginer l'utiliser pour définir les identifiants SQL utilisés pour les tables et colonnes. Ainsi chaque objet système pourrait avoir un nom machine fiable (le GUID), un identifiant PHP facile et joli à regarder, et un identifiant SQL qui ne pourrirait pas le schéma de base de données.
Et l'état de cela dans drupal 7 ? CCK est dans le cœur maintenant, ils ont peut être facilité la chose.
Apparemment il y a des méthodes toutes faites : http://api.drupal.org/api/group/field_crud/7
Ça règle pas le problème de sites déjà existant, mais on peut espérer que ça s'arrangera pour la suite :)
Pour Drupal 7, field_update_instance fait en effet le job sur le papier, je ne l'ai pas encore testé. En revanche ce qui est certain c'est que soit je suis aveugle, soit changer le nom d'un champ n'est toujours pas dispo en drupal 7 au niveau de l'interface (qui est moins frustrante qu'en Drupal 6 ou le champ existe, mais en lecture seul ;-).
Maintenant Drupal 7, je vais peut-être attendre un peu... Pour les nouveaux projets il faut attendre le portage d'un plus grand nombre de modules. Et pour ceux existant en Drupal 6, mes clients ne sont pas exactement prés de migrer vu le coût que cela semble représenter en développement (le chiffrage est pas encore terminé).
Enfin ce qui est un peu tristoune avec Drupal 7, c'est que côté design, on est sur du CCK en moins bien. Alors oui "wow, fields everywhere", mais tout en stockage par tables (plus de stockage par colonnes) avec obligation de passer par le cachefield qui va se retrouver en base de données vu qu'il sera trop gros pour tenir en mémoire (pour les sites avec beaucoup de champs et de contenus)... Là aussi je n'ai pas terminé les benchs mais je ne suis pas totalement fan de l'approche.
"si les champs CCK sont utilisés dans une tripotée de vues, les renommer impliqueraient de toutes les retoucher une à une. Voilà qui n'a pas vraiment amélioré mon appréciation de Views dans le cadre d'un site réalisés par des professionnels"
Bin c'est pareil pour une requete sql en dur : les selects cassent si le nom de la colonne visée disparait... Donc je suis pas sûr saisir ce que views vient faire dans cette histoire : on peut pas vraiment le blamer de perdre ses petits si la structure d'une table change.
Edit : merci d'avoir partager ce code pour le renommage des champs ;-)
Tu as raison je ne suis pas assez précis. Views stoque son paramétrage en base de donnée. Les requêtes SQL sont elles dans le code, comme les références aux champs CCK dans un template.
Du coup si tu modifies le nommage d'un champ, tu peux facilement le répercuter sur les requêtes et sur les templates en refactorisant ton code à l'aide d'un simple rechercher/remplacer. Ce qui n'est évidement pas possible avec Views. Ce qui est aussi le cas de tout autre module configuré à partir d'un nom de champ. D'où ma conclusion. Si Views est l'argument avançé par certains pour ne pas rendre le nom de champ modifiable, pour moi ce n'est juste qu'une conséquence de la gestion même des nommages de champ.
Cela dit, garder les vues dans la base c'est une très mauvaise idée.
Pour rechercher/remplacer c'est possible avec view si on fait ça bien : lors du passage en prod il faut exporter les vues pour les mettre dans un module, comme ça y'a plus a taper dans la base pour les construire. C'est dans du code donc le rechercher/remplacer marche pareil (je sais que tu n'es pas fan de features non plus, pas besoin de l'utiliser : view lui-même propose une méthode pour exporter, sans autre dépendance).
Alors déjà "c'est une mauvaise idée", je ne suis pas d'accord :-) De mon point de vue views est un outil avant tout conçu pour les néophytes avancés (l'IHM est un peu complexe tout de même), leur permettant de construire une page sans connaissances techniques excessives, Du coup si je le mets sur un projet c'est avant tout pour permettre à mes clients de créer/modifier certaines pages. Après des développeurs peuvent évidement utiliser cet outil, mais là j'avoue moins comprendre que quelqu'un qui maîtrise PHP et SQL trouve du plaisir à aller s'enquiquiner avec Views.
Maintenant je veux bien croire que c'est la bonne pratique. Mais si tel est le cas, elle devrait pet-être être entérinée par la logique même de Views. car si j'ai bien tout compris (c'est pas certain hein ;-), pour mettre des vues dans le code on passe par le hook des vues par défaut, qui sont donc suplantables en base de donnée (les gens font n'importe quoi, je sais, je sais :-) ce qui pour moi est encore pire que le problème d'origine. Il faudrait avoir un hook "view tout court" permettant de créer des vues "en lecture seule".
Enfin plutôt que faire de l'export/copier/coller à chaque fois que je modifie une vue, j'aurais tendance à automatiser cela avec Features, pas toi ?
bah voilà, pourquoi tu t'embetes à faire des requêtes sql customs alors qu'il suffit de CCK + Views + Features + Drush puis taper "drush features-update all" pour pouvoir faire un rechercher remplacer sur le nom du champ dans le code? c'est pourtant pas compliqué. (ps, ne pas oublier les différents patches qui vont bien pour chaque module). #drupalway
Quelle vilaine ironie ;p Me tente pas, je lutte là :)
Publier un nouveau commentaire