PrestaShop : de l'usage du champ Position d'un modèle dans le contrôleur de back-office correspondant

Si vous développez régulièrement sur PrestaShop, il y a fort à parier que vous avez déjà écrit vous-même des classes héritant d'AdminController afin d'offrir à vos utilisateurs une interface en back-office leur permettant de gérer les fonctionnalités que vous mettez en place.

Dans le cas d'un module par exemple, vous avez peut-être suivi cet excellent tutoriel de manière à permettre l'administration des entités gérées par ledit module.

Si vous avez ensuite jugé qu'il serait utile d'ajouter aux entités en question un champ position, dont la valeur serait prise en compte lors de l'affichage en front-office (et se présentant en back-office de manière similaire à ce que l'on peut trouver sur les catégories par exemple), je peux cette fois affirmer sans trop risquer de me tromper que vous n'êtes pas près d'oublier la torture mentale qui a suivi. En effet, cet aspect a beau être totalement (ou presque) géré par PrestaShop nativement, l'absence totale de documentation à ce sujet peut s'avérer pour le moins handicapante. Je vous propose donc un petit récapitulatif !

Premièrement, votre modèle doit bien évidemment inclure le champ correspondant :

class MyModel extends ObjectModel
{
public $id_model; // clé primaire
public $position;
// ...

public static $definition = array(
// ...

'fields' => array(
// ...
'position' => array(
'type' => ObjectModel::TYPE_INT
),
// ...
)
);

J'affirmais tantôt que PrestaShop sait gérer "nativement" l'implémentation d'un champ position sur un modèle : ce n'est pas tout à fait vrai, étant donné que les parties du code source dédiées à cet aspect font référence en dur à des champs liés aux catégories (de produit ou de pages CMS), qui sont les seules à en faire usage par défaut. Nous allons donc devoir mettre un peu les mains dans le cambouis pour combler les trous et répliquer ce comportement pour nos propres entités.

Continuons donc sur notre modèle, et créons tout d'abord une méthode pour récupérer facilement la prochaine valeur de position disponible :

public static function getNextAvailablePosition()
{
$sql = 'SELECT position FROM '._DB_PREFIX_.self::$definition['table'].' ORDER BY position DESC';

$position = (int)Db::getInstance()->getValue($sql, false);
return $position + 1;
}

Nous allons ensuite nous assurer que ce champ est correctement renseigné lors de l'enregistrement en base de données d'une nouvelle instance de notre modèle, en étendant la méthode processAdd du contrôleur d'admin de notre module :

public function processAdd()
{
$object = parent::processAdd();
$object->position = MyModel::getNextAvailablePosition();

$object->id_model = $object->id; // apparemment nécessaire si votre clé primaire porte un nom autre qu'"id"
$object->update();

return $object;
}

Si vous testez ce que nous venons d'ajouter maintenant, tout devrait fonctionner correctement en apparence (du moins, en ce qui concerne les valeurs sauvegardées). Voyons maintenant comment "câbler" cette fonctionnalité sur l'interface d'administration.

Ajoutons tout d'abord notre champ position à la vue en liste de nos entités, dans notre classe fille de ModuleAdminController :

public function __construct()
{
// ...
$this->orderBy = 'position'; // on définit le critère de tri par défaut
$this->position_identifier = 'id_model'; // on rappelle la clé primaire du modèle (sans quoi les liens générés seront incorrects)

parent::__construct();

$this->fields_list = array(
// ...
'position' => array(
'title' => $this->l('Position'),
'position' => 'position' // toute la "magie" est là...
),
// ...
);

La nouvelle colonne que vous verrez apparaître contiendra soit des liens contenant des icônes en forme de flèche (le résultat souhaité !), soit des chiffres. Dans ce second cas, assurez-vous que votre liste est actuellement triée selon notre nouveau champ position dans le sens ascendant, ainsi que nous l'avons défini dans le code (en pratique, c'est le dernier critère sélectionné par l'utilisateur qui prévaut).

Une fois les liens permettant de modifier la position d'un élément visibles sur votre vue en liste, nul doute que vous allez sentir l'espoir renaître et les ténèbres se dissiper. Et pourtant, si vous cliquez sur l'un d'eux, vous aurez droit à un résultat des plus somptueux :

Fatal error: Call to undefined method MyModel::updatePosition() in [...]

En effet, la classe de base ObjectModel ne définit pas cette fonctionnalité : le soin est laissé aux classes filles de l'implémenter selon leurs propres besoins le cas échéant. En ce qui nous concerne, nous pouvons y procéder rapidement ainsi :

/**
* @param $way Sens du changement de position (0 si l'élément monte, 1 s'il descend)
* @param $position La position qu'aura l'élément _après_ ledit mouvement
*
* @return Booléen indiquant la réussite ou l'échec de l'opération
*/
public function updatePosition($way, $new_position)
{
$db = Db::getInstance();
$count = $db->getValue('SELECT COUNT(*) FROM '._DB_PREFIX_.self::$definition['table'], false);

if (($new_position >= 1) && ($new_position <= $count)) {
$old_position = $way ? ($new_position - 1) : ($new_position + 1);

if (($old_position >= 1) && ($old_position <= $count)) {
$sql = implode(
';',
array(
'UPDATE '._DB_PREFIX_.self::$definition['table'].' SET position = 0 WHERE position = '.(int)$new_position,
'UPDATE '._DB_PREFIX_.self::$definition['table'].' SET position = '.(int)$new_position.' WHERE position = '.(int)$old_position,
'UPDATE '._DB_PREFIX_.self::$definition['table'].' SET position = '.(int)$old_position.' WHERE position = 0'
)
);

// L'ancienne et la nouvelle position sont valides, on les intervertit
return $db->execute($sql);
}
}

return false;
}

Voilà qui est mieux ! N'oublions pas non plus de réassigner proprement la position des éléments en cas de suppression de l'un d'eux. À cette occasion, nous revenons sur le modèle et lui ajoutons la méthode suivante :

public function delete()
{
$position = $this->position;

if ($result = parent::delete()) {
Db::getInstance()->execute('UPDATE '._DB_PREFIX_.self::$definition['table'].' SET position = position-1 WHERE position > '.(int)$position);
}

return $result;
}

Enfin, en bonus, nous allons faire en sorte que le changement de position par drag'n'drop dans la colonne correspondante ait un réel effet (pour l'instant, la modification se produit visuellement mais n'est pas persistée) en ajoutant la méthode suivante à notre contrôleur :

public function ajaxProcessUpdatePositions()
{
if (isset($_POST['id']) && isset($_POST['way'])) {
$object = new MyModel($_POST['id']);

if ($object->id_model) {
$new_position = $object->position;
$_POST['way'] ? $new_position++ : $new_position--;

if ($object->updatePosition($_POST['way'], $new_position)) {
die;
}

die('{"hasError": true, errors: "Cannot update position"}');
}
}

die('{"hasError": true, errors: "This item can not be loaded"}');
}

Le nom de cette méthode obéit à une convention (secrète ?) de PrestaShop qui lui permet d'être exécutée automatiquement lorsque la requête AJAX idoine est lancée.

Ouf, nous y voilà parvenus ! J'ai écrit cet article à la suite de mes propres tâtonnements, aussi n'hésitez pas à le commenter si certains détails sont inexacts ou imprécis et que, fort de votre propre galère expérience, vous pouvez les améliorer.