Table des matières
Prérequis technique
- Connaissance de PHP
- Connaissance du fonctionnement des moteurs de templates (Twig, Smarty…)
- Connaissance du fonctionnement de PrestaShop
Sources
- https://devdocs.prestashop.com/1.7/modules/
- CINS Slider v2
Introduction
Vous utilisez PrestaShop en tant que développeur ou agence web et ne trouvez pas le module adapté au besoin exprimé par votre client ? C’est normal, chaque projet à ses spécificités et même si beaucoup de modules existent pour ce CMS, il faut parfois mettre les mains à la pâte ! C’est pourquoi chez CINS, quand c’est nécessaire, nous développons nos propres modules PrestaShop afin de répondre aux besoins sur mesure de chaque projet.
Pour rentrer dans le vif du sujet, nous allons vous exposer un cas concret pour vous montrer comment nous nous y prenons pour le développement de nos modules PrestaShop dans sa version 1.7.
Pour contextualiser le besoin : créer un diaporama avec plusieurs diapositives visibles sur le même écran. Chaque diapositive devant être administrable indépendamment pour son contenu tout en gardant une homogénéité d’ensemble pour le choix des couleurs, tailles du texte…
Avant de foncer tête baissée, il est important de s’imaginer globalement ce dont on va avoir besoin au niveau de l’administration du module et au niveau de son affichage.
- Pour le back-office : une page de configuration générale pour l’ensemble du diaporama et une autre page lisant les diapositives
- Pour le front-office : un bloc présentant le diaporama
1 / Création du module et système de hook
Prêt ? Alors c’est parti ! Il va nous falloir créer le squelette d’un module PrestaShop. Commencez par créer un dossier dans le dossier /modules
à la racine de votre boutique en ligne.
Astuce : Attention, le nom de votre module ne doit pas comporter d’espaces, seulement des caractères alphanumériques, des tirets, des underscores et le tout en minuscule
Dans ce dossier, vous pouvez ajouter l’image du module qui l’identifiera en back-office en ajoutant une image nommée logo.png
(une image en .png ou .gif en 32x32px est attendue). Toujours au même endroit, créez un fichier PHP portant le même nom que votre dossier. Ce dernier sera le fichier principal de votre module. Pour notre exemple, nous avons appelé notre dossier /cinsslider
contenant le fichier PHP cinsslider.php
.
Dans ce fichier, commencez par insérer le code minimal suivant :
name = 'cinsslider';
$this->author = 'CINS';
$this->version = '1.0.0';
$this->bootstrap = true;
parent::__construct();
$this->displayName = $this->l('CINS Slider');
$this->description = $this->l('Add configurable slider in home page');
$this->confirmUninstall = $this->l('Are you sure you want to uninstall?');
$this->ps_versions_compliancy = array('min' => '1.7.0.0', 'max' => _PS_VERSION_);
}
public function install()
{
return parent::install()
}
public function uninstall()
{
parent::uninstall();
}
}
La première partie concerne la sécurité et permet d’empêcher les utilisateurs malveillants de charger directement ce fichier.
La classe étend Module et nous permet, simplement en renseignant les différents éléments dans son constructeur, de voir s’afficher notre module en backoffice dans le catalogue des modules. Pour l’instant, toutes les chaînes de caractères que nous renseignons sont en anglais. Nous reviendrons dessus plus tard lorsque l’on verra le système de traduction des modules. En fonction du module que l’on souhaite développer, cette classe peut en étendre d’autre comme PaymentModule pour un module de moyen de paiement par exemple. Une fonction install()
et uninstall()
pour les actions faites à l’installation et la désinstallation du module. Nous reviendrons sur ces deux fonctions très vite !
Revenez à la racine de votre module et créez un dossier /views
contenant un dossier /js
, /css
et /templates
. Dans les dossiers /js
et /css
, vous allez pouvoir créer vos propres fichiers js et css que nous inclurons par la suite dans notre module. Dans le dossier /templates
, créez un dossier /hook
. A l’intérieur de ce dernier, créez un fichier de template .tpl qui sera le fichier d’affichage de référence de notre module. Bien sûr, rien ne nous empêche de créer notre propre arborescence de template et de fichiers si nécessaire.
Voici l’arborescence minimale attendue pour un module :
├── module_name.php
├── logo.png
├── config.xml
└── views
├── css
├── js
└── templates
Astuce : Le fichier config.xml va être automatiquement généré par Prestashop et permet d’optimiser le chargement du module depuis le back-office donc ne vous en souciez-pas.
Pour ajouter vos templates dans le code de notre module, dans cinsslider.php
, nous allons définir notre fichier de template de référence en rajoutant une variable à notre classe :
class Cinsslider extends Module {
private $templateFile;
et y définir son chemin dans le constructeur :
$this->templateFile = 'module:cinsslider/views/templates/hook/cinsslider.tpl';
Pour inclure les fichiers CSS et js que nous avons créés, nous passerons par le Hook Header de PrestaShop. Pour ceux qui souhaitent en savoir plus sur les hooks, référez-vous à la documentation de PrestaShop https://devdocs.prestashop.com/1.7/modules/concepts/hooks/. Il est même possible de créer ses propres hooks et de les placer où vous le souhaitez, dans votre thème par exemple.
Pour l’utiliser dans notre module, nous le définissons dans la fonction d’installation :
public function install()
{
return parent::install()
&& $this->registerHook('header');
}
Puis nous l’utilisons en ajoutant l’ensemble des fichiers nécessaires sur le front-office :
public function hookHeader()
{
$this->context->controller->addJS($this->_path.'/views/js/front.js');
$this->context->controller->addJS($this->_path.'views/js/slick.min.js');
$this->context->controller->addCSS($this->_path.'views/css/slick.css');
$this->context->controller->addCSS($this->_path.'views/css/slick-theme.css');
$this->context->controller->addCSS($this->_path.'/views/css/front.css');
}
Dans notre cas, nous avons créé un fichier front.js
, front.css
et nous avons également ajouté la librairie slick
dans notre module afin de nous aider dans la fabrication du diaporama https://kenwheeler.github.io/slick/. Nous ne rentrons pas dans le détail du CSS et du JS car là n’est pas le sujet. Libre à vous d’intégrer votre module comme vous le souhaitez 😉.
Astuce : Il existe un générateur de module vous permettant de télécharger le squelette de votre module avec quelques paramètres renseignés dans ce générateur -> https://validator.prestashop.com/generator. Vous pouvez vous y connecter avec votre compte sur PrestaShop Addons.
Nous souhaitons que notre module s’affiche sur la page d’accueil. Nous utilisons donc le hook pour l’affichage sur la page d’accueil sans oublier de le définir dans notre fonction d’installation :
&& $this->registerHook('displayHome')
public function hookDisplayHome()
{
if (!$this->isCached($this->templateFile, $this->getCacheId('cinsslider'))) {
$this->smarty->assign($this->getWidgetVariables());
}
return $this->fetch($this->templateFile, $this->getCacheId('cinsslider'));
}
La fonction getWidgetVariables()
retourne un tableau de variables à passer au moteur de template Smarty et qui seront des variables réutilisables dans cinsslider.tpl
. Libre à vous d’assigner à Smarty les variables souhaitées sous la forme que vous le souhaitez. Voici un exemple :
public function getWidgetVariables(
$hookName = null,
array $configuration = []
) {
$config = [];
$config['nb_slides_same_times'] = Configuration::get('CINS_SLIDER_NBSLIDES_DISPLAYED');
return array(
'config' => $config,
);
}
Si vous renseignez vos hooks de cette façon, votre module sera accessible uniquement sur les hooks que vous aurez définis. Depuis PrestaShop 1.7, il existe le système de WidgetInterface qui permet de rendre accessible votre module sur l’ensemble des hooks d’affichage front-office définis sur votre boutique en ligne. Voici comment le mettre en place :
use PrestaShop\PrestaShop\Core\Module\WidgetInterface;
Nous l’insérons dans notre classe :
class Cinsslider extends Module implements WidgetInterface {
Nous définissons la fonction renderWidget()
pour afficher ce que nous souhaitons. Cette fonction sera utilisée si le module est greffé sur un hook qui n’est pas défini dans le module. Ici, nous avons repris l’affichage que nous avons défini sur la page d’accueil.
public function renderWidget(
$hookName = null,
array $configuration = []
) {
$this->hookDisplayHome();
}
2 / Création de la page de configuration en back-office
Pour rappel, nous souhaitons créer une page de configuration générale pour notre diaporama. Ici, le formulaire sera plus ou moins complexe en fonction du besoin exprimé ou de la modularité souhaitée. Dans notre cas, il faut paramétrer le fait d’afficher plusieurs diapositives sur le même écran donc nous aurons un paramètre de configuration pour cet élément. Pour se faire, nous allons utiliser l’objet Configuration : https://devdocs.prestashop.com/1.7/modules/creation/#the-configuration-object
Astuce : Un paramètre défini via cet objet est accessible n’importe où sur votre boutique. La bonne pratique est donc de les définir en majuscule comme pourrait l’être une constante PHP.
Nous commençons par l’enregistrer à l’installation du module et lui donner une valeur par défaut :
public function install()
{
return parent::install()
&& Configuration::updateValue('CINS_SLIDER_NBSLIDES_DISPLAYED', 3)
Vu que c’est une information accessible sur l’ensemble de la boutique, il faut la supprimer dans le cas où le module est désinstallé :
public function uninstall()
{
return Configuration::deleteByName('CINS_SLIDER_NBSLIDES_DISPLAYED') &&
parent::uninstall();
}
Pour créer une page de configuration et voir le bouton « Configurer » sur notre module, c’est la méthode getContent()
qu’il faut utiliser. Cette dernière retourne le contenu de la page d’administration. Dans notre cas, nous allons utiliser les Helper Form qui vont nous permettre de générer un formulaire rapidement et facilement dans le style de l’interface d’administration de PrestaShop. Nous avons créé une fonction renderForm()
pour cette génération de formulaire :
public function renderForm()
{
$fields_form_general = array(
'form' => array(
'legend' => array(
'title' => $this->l('Cins Slider General Settings'),
'icon' => 'icon-cogs',
),
'input' => array(
array(
'type' => 'text',
'label' => $this->l('Number of slide to display in the same time'),
'name' => 'CINS_SLIDER_NBSLIDES_DISPLAYED',
'desc' => $this->l(
'Determine the number of slide to display in the same time. It will be 1 in mobile device.'
),
),
'submit' => array(
'title' => $this->trans('Save', array(), 'Admin.Actions'),
),
),
);
$lang = new Language((int)Configuration::get('PS_LANG_DEFAULT'));
$helper = new HelperForm();
$helper->show_toolbar = false;
$helper->table = $this->table;
$helper->default_form_language = $lang->id;
$helper->allow_employee_form_lang = Configuration::get('PS_BO_ALLOW_EMPLOYEE_FORM_LANG') ? Configuration::get(
'PS_BO_ALLOW_EMPLOYEE_FORM_LANG'
) : 0;
$helper->identifier = $this->identifier;
$helper->submit_action = 'submit'.$this->name;
$helper->currentIndex = $this->context->link->getAdminLink(
'AdminModules',
false
).'&configure='.$this->name.'&tab_module='.$this->tab.'&module_name='.$this->name;
$helper->token = Tools::getAdminTokenLite('AdminModules');
$helper->tpl_vars = array(
'fields_value' => $this->getConfigFieldsValues(),
'languages' => $this->context->controller->getLanguages(),
'id_language' => $this->context->language->id,
);
return $helper->generateForm(array($fields_form_general));
}
Toute la partie concernant l’objet HelperForm est à inclure pour nous faciliter la construction du formulaire dans le style de Prestashop en administration. Seule la méthode getConfigFieldsValues()
va nous intéresser car elle permet de récupérer les valeurs enregistrées pour les champs que nous avons à créer. Nous y reviendrons plus tard.
Pour les champs du formulaire, nous avons ajouté un simple champ texte mais libre à vous d’ ajouter autant de champs et de formulaires que nécessaire (si vous voulez découper votre configuration en plusieurs parties distinctes par exemple). Si vous créez plusieurs formulaires, pensez à bien l’ajouter dans la valeur retournée dans la fonction generateForm()
du Helper. Au niveau des types de champs, il est possible de faire des textarea, des switcher, des champs couleur, fichier, date… Chaque champ devra à minima comprendre les paramètres suivants : son type, son label (titre affiché en administration) et son nom (ici la référence à la valeur de configuration que l’on a créée)
Pour aller plus loin, nous pouvons gérer la validation des champs du formulaire que nous venons de créer dans la méthode getContent()
:
public function getContent()
{
$output = '';
$errors = array();
if (Tools::isSubmit('submitcinsslider')) {
$nb_slides_displayed = Tools::getValue('CINS_SLIDER_NBSLIDES_DISPLAYED');
if (!Validate::isInt($nb_slides_displayed) || $nb_slides_displayed <= 0) {
$errors[] = $this->l('The number of slides is invalid. Please enter a positive number.');
}
if (isset($errors) && count($errors)) {
$output = $this->displayError(implode('
', $errors));
} else {
Configuration::updateValue('CINS_SLIDER_NBSLIDES_DISPLAYED', (int)$nb_slides_displayed);
$output = $this->displayConfirmation(
$this->trans('The settings have been updated.', array(), 'Admin.Notifications.Success')
);
}
}
return $output.$this->renderForm();
}
Lors de la soumission de notre formulaire de configuration, nous effectuons la validation souhaitée en renvoyant une erreur personnalisée dans le cas où le test serait non concluant (ici, la valeur doit être un entier positif). Si le test est concluant, nous modifions la donnée de configuration pour enregistrer celle renseignée. Une fois la gestion de la validation des champs faite, nous retournons notre formulaire en ajoutant à notre variable $output
, le rendu du formulaire précédemment créé dans la méthode renderForm()
.
Astuce : Notez que vous pouvez personnaliser entièrement le rendu en back-office de votre page de configuration du module. Vous pouvez tout à fait inclure un fichier .tpl incluant votre HTML personnalisé.
Avant d’oublier, créons la méthode récupérant les données de configuration pour notre formulaire : getConfigFieldsValues()
.
public function getConfigFieldsValues()
{
$config_fields_values['CINS_SLIDER_NBSLIDES_DISPLAYED'] = Tools::getValue(
'CINS_SLIDER_NBSLIDES_DISPLAYED',
(int)Configuration::get('CINS_SLIDER_NBSLIDES_DISPLAYED')
);
return $config_fields_values;
}
Retournez un tableau avec en clé « le nom de la donnée de configuration » et en valeur « la valeur récupérée depuis la donnée de configuration ». De cette façon, si jamais vous faites une deuxième version de votre module, libre à vous de revenir sur votre formulaire pour rajouter des champs.
Une fois ces 3 méthodes mises en place, dès lors que vous aurez installé votre module, un bouton « Configurer » sera visible et vous pourrez alors accéder à la configuration de votre module depuis le back-office.
3 / Création d’un objet personnalisé et de son contrôleur
Si les données de configuration ne suffisent pas à faire ce que vous souhaitez, il va falloir passer par un objet personnalisé pour arriver à vos fins. Dans notre cas, nous allons créer une classe représentant une diapositive.
A la racine du module, créez un fichier .php qui étend de ObjectModel
:
'cinsslider',
'primary' => 'id_cinsslider',
'multilang' => true,
'fields' => array(
'id_shop' => array('type' => self::TYPE_INT, 'validate' => 'isunsignedInt', 'required' => true),
'position' => array('type' => self::TYPE_INT, 'validate' => 'isInt'),
'width' => array('type' => self::TYPE_INT, 'validate' => 'isInt'),
'file_name' => array('type' => self::TYPE_STRING, 'validate' => 'isFileName'),
'active' => array('type' => self::TYPE_STRING, 'validate' => 'isInt'),
// Lang fields
'title' => array('type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName'),
'text' => array('type' => self::TYPE_HTML, 'lang' => true, 'validate' => 'isCleanHtml'),
'link' => array('type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName'),
),
);
public function copyFromPost()
{
/* Classical fields */
foreach ($_POST as $key => $value) {
if (array_key_exists($key, $this) and $key != 'id_'.$this->table) {
$this->{$key} = $value;
}
}
/* Multilingual fields */
if (sizeof($this->fieldsValidateLang)) {
$languages = Language::getLanguages(false);
foreach ($languages as $language) {
foreach ($this->fieldsValidateLang as $field => $validation) {
if (isset($_POST[$field.'_'.(int)($language['id_lang'])])) {
$this->{$field}[(int)($language['id_lang'])] = $_POST[$field.'_'.(int)($language['id_lang'])];
}
}
}
}
}
}
Nous retrouvons ici un objet personnalisé cinssliderClass
; représentant une diapositive avec une définition d’un certain nombre de champs nécessaires au bon fonctionnement d’une diapositive ainsi que sa définition (type de champ, multilangue, méthode de validation). Une méthode copyFromPost()
; va venir parcourir l’ensemble des champs à la soumission (ajout/modification) d’une diapositive pour les charger et les sauvegarder.
Avant de créer le contrôleur associé, nous allons retourner dans le fichier .php principal de notre module pour venir y ajouter une fonction à l’installation et à la désinstallation pour gérer des tables en base de données afin de stocker les informations de notre classe. Créez une fonction installDB()
:
public function installDB()
{
$return = true;
$return &= Db::getInstance()->execute(
'
CREATE TABLE IF NOT EXISTS `'._DB_PREFIX_.'cinsslider` (
`id_cinsslider` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`id_shop` int(10) unsigned NOT NULL ,
`position` int(10),
`file_name` VARCHAR(255) NOT NULL,
`active` int(10),
PRIMARY KEY (`id_cinsslider`)
) ENGINE='._MYSQL_ENGINE_.' DEFAULT CHARSET=utf8 ;'
);
$return &= Db::getInstance()->execute(
'
CREATE TABLE IF NOT EXISTS `'._DB_PREFIX_.'cinsslider_lang` (
`id_cinsslider` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`id_lang` int(10) unsigned NOT NULL ,
`title` VARCHAR(300),
`text` VARCHAR(300),
`link` VARCHAR(300),
PRIMARY KEY (`id_cinsslider`, `id_lang`)
) ENGINE='._MYSQL_ENGINE_.' DEFAULT CHARSET=utf8 ;'
);
return $return;
}
Nous ajoutons ensuite cette fonction dans le hook install()
afin qu’elle soit lancée lors de l’installation du module :
public function install()
{
return parent::install()
&& $this->installDB()
...
Même principe mais cette fois-ci en prévision d’une désinstallation du module, nous insérons une méthode pour supprimer les tables créées à mettre en place dans le hook uninstall()
:
public function uninstallDB()
{
return Db::getInstance()->execute('DROP TABLE IF EXISTS `'._DB_PREFIX_.'cinsslider`') && Db::getInstance(
)->execute('DROP TABLE IF EXISTS `'._DB_PREFIX_.'cinsslider_lang`');
}
public function uninstall()
{
return
$this->uninstallDB() &&
...
Une fois votre classe et vos tables créées, nous ajoutons un contrôleur pour administrer ces diapositives. À la racine de votre module, créez le dossier /controllers
puis dans ce dossier, créez un dossier /admin
dans lequel vous ajouterez votre contrôleur .php. C’est dans ce fichier que nous gérerons la page listant l’ensemble des diapositives ainsi que le formulaire d’ajout/modification d’une diapositive :
bootstrap = true; //Gestion de l'affichage en mode bootstrap
$this->table = cinssliderClass::$definition['table']; //Table de l'objet
$this->identifier = cinssliderClass::$definition['primary']; //Clé primaire de l'objet
$this->className = cinssliderClass::class; //Classe de l'objet
$this->lang = true; //Flag pour dire si utilisation de langues ou non
$this->position_identifier = 'position';
$this->_orderBy = 'position';
$this->_orderWay = 'asc';
//Appel de la fonction parente pour pouvoir utiliser la traduction ensuite
parent::__construct();
$this->fields_list = array(
'id_cinsslider' => array(
'title' => $this->trans('ID', array(), 'Admin.Global'),
'width' => 80,
'type' => 'text',
'search' => false,
'orderby' => false,
),
'title' => array(
'title' => $this->trans('Title', array(), 'Modules.Cinsslider.Admin'),
'width' => 140,
'type' => 'text',
'search' => false,
'orderby' => false,
),
'position' => [
'title' => 'Position',
'position' => 'position',
'search' => false,
'orderby' => false,
],
'active' => [
'title' => $this->l('Status'),
'width' => 70,
'active' => 'status',
'align' => 'center',
'type' => 'bool',
'search' => false,
'orderby' => false,
],
);
//Ajout d'actions sur chaque ligne
$this->addRowAction('edit');
$this->addRowAction('duplicate');
$this->addRowAction('delete');
}
}
Nous commençons par inclure la classe précédemment créée. Nous créons ensuite notre contrôleur d’admin en héritant de ModuleAdminController
. Puis, nous définissons les paramètres de base dans son constructeur et nous renseignons les champs de notre classe que nous voulons représenter sous forme de liste (comme la liste des produits par exemple). Ici, nous avons choisi d’afficher l’identifient, le titre, la position (important pour changer l’ordre à la volée) et le statut. A partir du moment où le nom des champs correspond à ceux que vous avez définis dans votre classe, le lien va se faire. Enfin, nous ajoutons des actions (modifier, dupliquer et supprimer).
La liste des diapositives étant préparée, nous pouvons désormais nous attaquer au formulaire d’une diapositive. De la même façon que nous l’avons fait pour le formulaire de configuration, nous aurons une méthode permettant de rendre le formulaire et une autre permettant de valider et d’enregistrer les valeurs renseignées ou les actions à effectuer.
Pour afficher le formulaire, la méthode est renderForm()
:
public function renderForm()
{
$imageFile = 'cinsslider/img/cinsslider-'.(int)$this->object->id.'-'.(int)$this->object->id_shop.'.jpg';
$this->fields_form = array(
'legend' => array(
'title' => $this->trans('New cinsslider', array(), 'Modules.Cinsslider.Admin'),
),
'input' => array(
array(
'type' => 'file',
'label' => $this->trans('Image', array(), 'Admin.Global'),
'name' => 'image',
'value' => true,
'image' => (
file_exists(_PS_MODULE_DIR_.$imageFile) ?
'' :
'Pas d\'image’
),
'display_image' => false,
),
array(
'type' => 'text',
'label' => $this->trans('Titre', array(), 'Modules.Cinsslider.Admin'),
'lang' => true,
'name' => 'title',
),
array(
'type' => 'textarea',
'label' => $this->trans('Texte', array(), 'Modules.Cinsslider.Admin'),
'autoload_rte' => true,
'lang' => true,
'name' => 'text',
),
array(
'type' => 'text',
'label' => $this->trans('Lien', array(), 'Modules.Cinsslider.Admin'),
'lang' => true,
'name' => 'link',
),
),
'submit' => array(
'title' => $this->trans('Enregistrer', array(), 'Admin.Actions'),
),
);
return parent::renderForm();
}
Ici, il faut bien faire attention à faire correspondre le paramètre « name » de chaque champ par le nom du champ de la classe que vous utilisez.
Ensuite, nous créons une seconde méthode pour gérer la soumission de ce formulaire ou les actions proposées :
public function postProcess()
{
if (Tools::isSubmit('submitUpdatecinsslider') || Tools::isSubmit('submitAddcinsslider')) {
if ($id_cinsslider = Tools::getValue('id_cinsslider')) {
$cinsslider = new cinssliderClass((int)$id_cinsslider);
} else {
$cinsslider = new cinssliderClass();
}
$cinsslider->copyFromPost();
$cinsslider->id_shop = $this->context->shop->id;
if ($cinsslider->validateFields(false) && $cinsslider->validateFieldsLang(false)) {
$cinsslider->save();
Tools::clearSmartyCache();
if (
isset($_FILES['image']) &&
isset($_FILES['image']['tmp_name']) &&
!empty($_FILES['image']['tmp_name']) &&
false === ($error_validate = ImageManager::validateUpload($_FILES['image'])) &&
($tmpName = tempnam(_PS_TMP_IMG_DIR_, 'PS')) &&
($error_move = move_uploaded_file($_FILES['image']['tmp_name'], $tmpName))
) {
$filename = 'cinsslider-'.(int)$cinsslider->id.'-'.(int)$cinsslider->id_shop.'.jpg';
$complete_filename = _PS_MODULE_DIR_.'cinsslider/img/'.$filename;
if (file_exists($complete_filename)) {
unlink($complete_filename);
}
if (ImageManager::resize($tmpName, $complete_filename)) {
$cinsslider->file_name = $filename;
$cinsslider->save();
}
unlink($tmpName);
}
}
return false;
} elseif (Tools::isSubmit('deletecinsslider')) {
if ($id_cinsslider = Tools::getValue('id_cinsslider')) {
$cinsslider = new cinssliderClass((int)$id_cinsslider);
if (file_exists(dirname(__FILE__).'/img/'.$cinsslider->file_name)) {
unlink(dirname(__FILE__).'/img/'.$cinsslider->file_name);
}
if (file_exists(dirname(__FILE__).'/img/'.$cinsslider->file_name_mobile)) {
unlink(dirname(__FILE__).'/img/'.$cinsslider->file_name_mobile);
}
$cinsslider->delete();
Tools::clearSmartyCache();
}
return false;
} elseif (Tools::isSubmit('duplicatecinsslider')) {
if (Tools::getValue('id_cinsslider')) {
$id_old_cinsslider = Tools::getValue('id_cinsslider');
$duplicate_cinsslider = new cinssliderClass((int)$id_old_cinsslider);
$duplicate_cinsslider->active = 0;
unset($duplicate_cinsslider->id);
unset($duplicate_cinsslider->id_cinsslider);
$duplicate_cinsslider->copyFromPost();
$duplicate_cinsslider->id_shop = $this->context->shop->id;
$titles = $duplicate_cinsslider->title;
foreach ($titles as &$t) {
$t = 'Copy of '.$t;
}
$duplicate_cinsslider->title = $titles;
if ($duplicate_cinsslider->validateFields(false) && $duplicate_cinsslider->validateFieldsLang(false)) {
$duplicate_cinsslider->save();
Tools::clearSmartyCache();
}
} else {
$this->errors[] = $this->trans(
'An error occurred while duplicate this object.',
array(),
'Modules.Cinsslider.Admin'
);
}
return false;
}
return parent::postProcess();
}
Nous utilisons les méthodes copyFromPost()
et save()
que nous avons définies dans la classe, ou qui sont reprises depuis la classe ObjectModel
pour l’ajout et la modification d’une diapositive. Vu que nous ajoutons une image à notre objet, il y a une spécificité supplémentaire car il faut gérer son enregistrement en tant que fichier, raison pour laquelle il y a des vérifications sur le $_FILES
.
Pour la suppression, nous utilisons la méthode delete()
de l’objectModel.
Pour la duplication, nous récupérons notre diapositive et en créons une copie sur une nouvelle instance. Puis nous supprimons son identifiant, la désactivons (activée par défaut) et nous changeons son titre pour lui rajouter ‘Copie de …’ afin de bien l’identifier.
Enfin, vu que nous souhaitons gérer l’ordre des positions depuis la liste des diapositives, nous créons une méthode ajaxProcessUpdatePositions()
dédiée en venant modifier directement via une requête SQL la valeur des champs positions de nos diapositives :
public function ajaxProcessUpdatePositions()
{
$cinssliders = Tools::getValue('cinsslider');
$position = 0;
foreach ($cinssliders as $cinsslider) {
$ids = explode('_', $cinsslider);
Db::getInstance()->execute(
'UPDATE `'._DB_PREFIX_.'cinsslider` SET position='.$position++.'
WHERE id_cinsslider = '.$ids[2]
);
}
}
4 / Création d’un onglet dans la barre d’administration
Maintenant que nous avons créé le contrôleur de notre classe, il faut pouvoir y accéder depuis le back-office. Nous allons donc créer un onglet pour y accéder grâce à l’objet « Tab ». Rendez-vous dans le fichier .php principal de votre module pour procéder aux changements. Sur le même principe que les tables en bases de données, nous allons lancer ces méthodes à l’installation et à la désinstallation du module :
protected function installTab()
{
$idTab = (int)Tab::getIdFromClassName('AdminCins');
if (!$idTab) {
$tab = new Tab();
$tab->class_name = 'AdminCins';
$tab->id_parent = (int)Tab::getIdFromClassName('IMPROVE');
$tab->icon = 'settings_applications';
$languages = Language::getLanguages();
foreach ($languages as $lang) {
$tab->name[$lang['id_lang']] = $this->l('Modules CINS');
}
try {
$tab->save();
} catch (Exception $e) {
echo $e->getMessage();
return false;
}
}
$tab = new Tab();
$tab->class_name = 'AdminCinsSlider';
$tab->module = $this->name;
$tab->id_parent = (int)Tab::getIdFromClassName('AdminCins');
$languages = Language::getLanguages();
foreach ($languages as $lang) {
$tab->name[$lang['id_lang']] = $this->l('Slides');
}
try {
$tab->save();
} catch (Exception $e) {
echo $e->getMessage();
return false;
}
return true;
}
Nous faisons bien le lien vers notre contrôleur d’admin « AdminCinsSlider » donc faites bien attention à renseigner le nom du contrôleur que vous avez créé. Puis nous intégrons cette méthode lors de l’installation du module :
public function install()
{
return parent::install()
&& $this->installDB()
&& $this->installTab()
...
Pour la désinstallation même principe :
protected function uninstallTab()
{
$idTab = (int)Tab::getIdFromClassName('AdminCinsSlider');
if ($idTab) {
$tab = new Tab($idTab);
try {
$tab->delete();
} catch (Exception $e) {
echo $e->getMessage();
return false;
}
}
return true;
}
public function uninstall()
{
return
$this->uninstallDB() &&
$this->uninstallTab() &&
...
A partir de ce moment-là, si vous installez votre module, un onglet va s’afficher dans la partie « Personnaliser » avec le nom que vous avez donné à l’onglet et à son lien :
Le clic sur « Diapositives » va afficher le rendu du contrôleur d’Admin créé précédemment et donc la liste des diapositives :
Nous voyons que les colonnes créées sont bien en place, que nous pouvons modifier les positions à la volée ainsi que le fait d’activer ou non une diapositive. Le clic sur le bouton déroulant à droite montre les actions définies précédemment dans le constructeur de notre contrôleur à savoir modifier, dupliquer et supprimer.
Au clic sur modifier ou sur l’icône « + », nous accèdons au formulaire d’une diapositive comme nous l’avons défini dans la méthode renderForm()
du contrôleur.
Astuce : Rien ne vous empêche de lancer les méthodes de création de tables et d’onglets non pas depuis le hook d’installation et de désinstallation mais via d’autres hooks pour éviter de désinstaller/réinstaller votre module à chaque fois que vous modifier votre module.
5 / Mettre en place des données par défaut à l’installation
Vous avez vu dans les dernières captures qu’il y a déjà des diapositives alors que nous venons tout juste d’installer le module. Il est possible d’installer du contenu par défaut dès l’installation afin d’avoir un bloc déjà prêt et visible en front-office.
Pour ce faire, dans le fichier PHP principal de mon module, nous créons une méthode où allons créer des instances de notre classe que nous lançons lors de l’installation de notre module.
Nous incluons dans un premier temps notre objet diapositive :
include_once _PS_MODULE_DIR_.'cinsslider/cinssliderClass.php';
Notre méthode pour créer 3 diapositives par défaut :
public function installFixtures()
{
$return = true;
$tab_texts = array(
array(
'title' => $this->trans('Title 1', array(), 'Modules.Cinsslider.Shop'),
'text' => $this->trans('Description 1', array(), 'Modules.Cinsslider.Shop'),
'link' => $this->trans('/link-1', array(), 'Modules.Cinsslider.Shop'),
'position' => 1,
'active' => true,
'file_name' => '1920x860.png',
),
array(
'title' => $this->trans('Title 2', array(), 'Modules.Cinsslider.Shop'),
'text' => $this->trans('Description 2', array(), 'Modules.Cinsslider.Shop'),
'link' => $this->trans('/link-2', array(), 'Modules.Cinsslider.Shop'),
'position' => 2,
'active' => true,
'file_name' => '1920x860.png',
),
array(
'title' => $this->trans('Title 3', array(), 'Modules.Cinsslider.Shop'),
'text' => $this->trans('Description 3', array(), 'Modules.Cinsslider.Shop'),
'link' => $this->trans('/link-3', array(), 'Modules.Cinsslider.Shop'),
'position' => 2,
'active' => true,
'file_name' => '1920x860.png',
),
);
foreach ($tab_texts as $tab) {
$cinsslider = new cinssliderClass();
foreach (Language::getLanguages(false) as $lang) {
$cinsslider->title[$lang['id_lang']] = $tab['title'];
$cinsslider->text[$lang['id_lang']] = $tab['text'];
$cinsslider->link[$lang['id_lang']] = $tab['link'];
}
$cinsslider->position = $tab['position'];
$cinsslider->file_name = $tab['file_name'];
$cinsslider->active = $tab['active'];
$cinsslider->id_shop = $this->context->shop->id;
$return &= $cinsslider->save();
}
return $return;
}
Petite particularité ici, nous prenons bien soin d’insérer en FTP une image portant le même nom que nous avons défini dans cette méthode pour le nom du fichier afin qu’elle soit reprise et ce, à l’endroit défini dans les méthodes de votre contrôleur (à savoir dans un dossier /img
à la racine du module si vous avez repris notre exemple).
Enfin, nous insérons cette méthode dans la fonction d’installation de notre module :
public function install()
{
return parent::install()
&& $this->installDB()
&& $this->installTab()
&& $this->installFixtures()
...
6 / Mise en place des traductions
Vous avez sans doute pu remarquer des chaines de caractères en anglais dans les exemples de code fourni dans cet article. L’ensemble de ces chaines sont comprises dans les méthodes de traduction trans()
ou l()
du module. À partir du moment où ces chaines se trouvent dans une de ces méthodes, elles seront accessibles depuis le back-office dans « International > Traductions ». Donc pas besoin de vous embêter à passer par un éditeur de chaines traductibles comme PoEdit, passer par le back-office PrestaShop suffit !
Depuis l’onglet de traduction, sélectionnez votre module et traduisez-le dans la langue souhaitée :
Vous avez ensuite accès à un ensemble de chaînes de caractères avec leur traduction pour la langue sélectionnée :
Astuce : cliquez une première fois sur « Fermer tous les blocs » puis une nouvelle fois pour ouvrir et voir l’ensemble des chaînes traductibles.
À partir du moment ou vous allez renseigner vos traductions à cet endroit, PrestaShop va créer tout seul le dossier /fichier
de la langue dans votre module. Vous verrez alors un dossier « translations » à la racine de votre module avec à l’intérieur un fichier .php par langue traduite. Dans notre exemple pour la langue française, le fichier fr.php
s’y trouve et contient l’ensemble des chaines traductibles avec leur traduction pour la langue :
cinsslider_a40e87ead941181361b0dc28fd747ffb'] = 'CINS Slider v2';
$_MODULE['<{cinsslider}prestashop>cinsslider_1920d02115b6f72217b4c52d4d1c278e'] = 'Ajoute un diaporama configurable sur la page d\'accueil';
7/ Surcharges du fonctionnement de base de PrestaShop depuis votre module
Dans notre exemple, nous n’avons pas eu besoin de surcharger le fonctionnement natif de PrestaShop puisque nous avons créer notre propre classe. Dans certains cas, vous serez amené à surcharger le fonctionnement de PrestaShop et donc de modifier une classe ou un contrôleur natif par exemple. Si c’est le cas, c’est possible de le faire dans votre module.
Créez un dossier override à la racine de votre module. Dans celui-ci, créez un dossier /classes
si vous voulez modifier une classe, controller/ si vous modifiez un contrôleur…
Ajoutez ensuite votre override comme si vous le faisiez directement dans le dossier override de PrestaShop. A l’installation du module, PrestaShop va détecter qu’un dossier override est présent et il va venir créer l’override en question dans ses surcharges. A la désinstallation du module, il va venir supprimer la surcharge du dossier override de PrestaShop. De ce fait, une surcharge peut être décorrélée d’un site et liée à un module pour plus de flexibilité/modularité.
Astuce : si plusieurs modules surchargent la même méthode de la même classe, il va y avoir un conflit. Même si c’est une fonctionnalité intéressante, il faut rester vigilant sur ce point. Pour éviter ce problème, l’idéal est de regrouper les surcharges d’une même méthode dans un même module lorsque c’est possible.
8 / Mise à jour de vos modules
Nous n’avons jamais utilisé cette fonctionnalité de mises à jour au sein de CINS pour nos propres modules vu qu’ils sont réservés à un usage interne. Cela dit, le versionnement de vos modules est possible et est détaillé dans la documentation de PrestaShop 1.7 : https://devdocs.prestashop.com/1.7/modules/creation/enabling-auto-update/
9 / Bonnes pratiques
Une liste de bonnes pratiques dans le développement de modules PrestaShop 1.7 est disponible depuis leur documentation : https://devdocs.prestashop.com/1.7/modules/creation/good-practices/
N’hésitez pas à y jeter un œil 😊
Conclusion
- Réutilisation. Un module que l’on développe n’est pas forcément cloisonné qu’à un seul projet. Il est possible de les réutiliser d’un projet à un autre ou d’en réutiliser une partie pour l’adapter.
- Adaptabilité. Rien ne vous empêche d’intégrer des librairies propres au fonctionnement de votre module et ce indépendamment de votre thème (Par exemple :
slick
pour construire un diaporama comme vu dans notre exemple). - Une maîtrise totale du code. Que ce soit pour le rendu HTML pour se simplifier la mise en forme CSS, pour améliorer le SEO, pour l’administration des contenus utilisés dans le module… le fait de développer nos propres modules nous permet de nous adapter aux différentes contraintes du web et d’apporter l’expertise d’une agence web.