Le module Image Filter permet d'afficher des images cliquables sous forme de filtres sur les pages de catégories de la boutique PrestaShop. Il offre la possibilité de :
Définir des filtres par image.
Associer ces filtres à des catégories ou sous-catégories spécifiques.
Rediriger les utilisateurs vers des pages filtrées ou des URL spécifiques en cliquant sur les images.
Ce module est utile pour améliorer la navigation des utilisateurs et leur permettre d'appliquer des filtres visuels plus intuitifs.
2. Localisation des fichiers
Le module est situé dans le répertoire suivant de PrestaShop :
/modules/myimagefilter/
Fichiers principaux
Fichier
Rôle
myimagefilter.php
Fichier principal du module. Contient toute la logique métier et les hooks.
views/templates/hook/myimagefilter.tpl
Fichier .tpl responsable de l'affichage des filtres sur le front-office.
views/css/styles.css
Fichier CSS permettant de personnaliser l'apparence des images et des filtres.
img/filters/
Répertoire où les images des filtres téléversées via le back-office sont stockées.
3. Fonctionnalités principales
3.1 Affichage des filtres
Le module affiche des images sous forme de filtres sur les pages des catégories et sous-catégories.
Page principale : Les filtres sont directement associés aux pages de catégories par leur ID.
Sous-catégories : Grâce à une méthode récursive, le module récupère les sous-catégories pour inclure leurs filtres.
3.2 Gestion des filtres dans le back-office
Le module propose une interface dans le back-office pour :
Ajouter un filtre :
Définir l'ID de la catégorie.
Télécharger une image.
Définir un titre et une URL cible.
Modifier un filtre existant.
Supprimer un filtre existant.
Accès au module :
Depuis le panneau d'administration de PrestaShop :
Modules > Image Filter Module
3.3 Table de base de données
Le module utilise une table dédiée pour stocker les informations des filtres :
Nom de la table :pss_image_filter
Structure :
Colonne
Type
Description
id_image_filter
INT (PK)
ID unique de l'image filtre.
id_page
INT
ID de la catégorie associée.
image_url
VARCHAR(255)
URL de l'image utilisée pour le filtre.
target_url
VARCHAR(255)
URL de destination lorsque l'image est cliquée.
title
VARCHAR(255)
Titre affiché sous l'image (optionnel).
4. Architecture et Code
4.1 Hooks utilisés
Le module utilise deux hooks principaux :
displayBeforeAmazingFilter :
Permet d'afficher les filtres avant les filtres classiques de PrestaShop sur les pages de catégories.
header :
Utilisé pour ajouter le fichier CSS à la page.
4.2 Méthodes importantes
Méthode
Description
getAllSubCategories()
Récupère récursivement toutes les sous-catégories associées à une catégorie donnée.
hookDisplayBeforeAmazingFilter()
Récupère les filtres pour une catégorie et ses sous-catégories, et les passe au fichier .tpl.
renderForm()
Génère le formulaire pour ajouter ou modifier un filtre dans le back-office.
renderList()
Affiche la liste des filtres existants dans le back-office. Inclut les sous-catégories associées.
<?php
if (!defined('_PS_VERSION_')) {
exit;
}
class MyImageFilter extends Module
{
public function __construct()
{
$this->name = 'myimagefilter';
$this->tab = 'front_office_features';
$this->version = '1.0.0';
$this->author = 'Aleksandre';
$this->need_instance = 0;
parent::__construct();
$this->bootstrap = true;
$this->displayName = $this->l('Image Filter Module');
$this->description = $this->l('Gérez des images cliquables pour appliquer des filtres spécifiques à des pages.');
$this->ps_versions_compliancy = array('min' => '1.6.0.0', 'max' => '1.6.999.999');
}
public function install()
{
return parent::install()
&& $this->registerHook('displayBeforeAmazingFilter')
&& $this->registerHook('header')
&& $this->installDb();
}
public function uninstall()
{
return parent::uninstall() && $this->uninstallDb();
}
private function installDb()
{
$sql = 'CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . 'image_filter` (
`id_image_filter` INT(11) NOT NULL AUTO_INCREMENT,
`id_page` INT(11) NOT NULL,
`image_url` VARCHAR(255) NOT NULL,
`target_url` VARCHAR(255) NOT NULL,
`title` VARCHAR(255) NOT NULL,
PRIMARY KEY (`id_image_filter`)
) ENGINE=' . _MYSQL_ENGINE_ . ' DEFAULT CHARSET=utf8;';
return Db::getInstance()->execute($sql);
}
private function uninstallDb()
{
// $sql = 'DROP TABLE IF EXISTS `' . _DB_PREFIX_ . 'image_filter`;';
// return Db::getInstance()->execute($sql);
}
public function getContent()
{
$output = '';
if (Tools::isSubmit('submitImageFilter')) {
$id_image_filter = (int)Tools::getValue('id_image_filter', 0);
$id_page = (int)Tools::getValue('id_page');
$title = Tools::getValue('title');
$target_url = Tools::getValue('target_url');
$image_url = Tools::getValue('current_image_url', '');
if (isset($_FILES['image_file']) && !empty($_FILES['image_file']['name'])) {
$image_name = Tools::str2url($_FILES['image_file']['name']);
$upload_dir = _PS_IMG_DIR_ . 'filters/';
if (!is_dir($upload_dir)) {
mkdir($upload_dir, 0755, true);
}
$image_path = $upload_dir . $image_name;
if (move_uploaded_file($_FILES['image_file']['tmp_name'], $image_path)) {
$image_url = _PS_BASE_URL_ . '/img/filters/' . $image_name;
} else {
$output .= $this->displayError($this->l('Erreur lors du téléversement de l\'image.'));
}
}
$data = [
'id_page' => $id_page,
'title' => $title,
'target_url' => $target_url,
'image_url' => $image_url,
];
if ($id_image_filter) {
Db::getInstance()->update('image_filter', $data, 'id_image_filter = ' . (int)$id_image_filter);
$output .= $this->displayConfirmation($this->l('Configuration modifiée avec succès.'));
} else {
Db::getInstance()->insert('image_filter', $data);
$output .= $this->displayConfirmation($this->l('Configuration ajoutée avec succès.'));
}
}
if (Tools::isSubmit('deleteImageFilter')) {
$id_image_filter = (int)Tools::getValue('id_image_filter');
if ($id_image_filter && Db::getInstance()->delete('image_filter', 'id_image_filter = ' . $id_image_filter)) {
$output .= $this->displayConfirmation($this->l('Configuration supprimée avec succès.'));
} else {
$output .= $this->displayError($this->l('Erreur lors de la suppression.'));
}
}
$output .= $this->renderForm();
$output .= $this->renderList();
return $output;
}
public function renderForm()
{
$id_image_filter = (int)Tools::getValue('id_image_filter', 0);
$filter = $id_image_filter ? Db::getInstance()->getRow('SELECT * FROM `' . _DB_PREFIX_ . 'image_filter` WHERE id_image_filter = ' . $id_image_filter) : [];
$helper = new HelperForm();
$helper->module = $this;
$helper->name_controller = $this->name;
$helper->token = Tools::getAdminTokenLite('AdminModules');
$helper->currentIndex = AdminController::$currentIndex.'&configure='.$this->name;
$helper->title = $this->displayName;
$helper->show_toolbar = true;
$helper->toolbar_scroll = false;
$helper->bootstrap = true; // S'assurer que le helper utilise bootstrap
$helper->submit_action = 'submitImageFilter';
$helper->fields_value = [
'id_image_filter' => isset($filter['id_image_filter']) ? $filter['id_image_filter'] : '',
'id_page' => isset($filter['id_page']) ? $filter['id_page'] : '',
'title' => isset($filter['title']) ? $filter['title'] : '',
'target_url' => isset($filter['target_url']) ? $filter['target_url'] : '',
'current_image_url' => isset($filter['image_url']) ? $filter['image_url'] : '',
];
$helper->tpl_vars = [
'fields_value' => $helper->fields_value,
'languages' => $this->context->controller->getLanguages(),
'id_language' => $this->context->language->id
];
$fields_form = [
'form' => [
'legend' => [
'title' => $this->l('Ajouter ou modifier une image pour le filtre'),
'icon' => 'icon-picture',
],
'input' => [
['type' => 'hidden', 'name' => 'id_image_filter'],
['type' => 'text', 'label' => $this->l('ID de la page'), 'name' => 'id_page', 'required' => true],
['type' => 'file', 'label' => $this->l('Image'), 'name' => 'image_file'],
['type' => 'hidden', 'name' => 'current_image_url'],
['type' => 'text', 'label' => $this->l('Titre'), 'name' => 'title'],
['type' => 'text', 'label' => $this->l('URL cible'), 'name' => 'target_url', 'required' => true],
],
'submit' => [
'name' => 'submitImageFilter',
'title' => $this->l('Enregistrer'),
'class' => 'btn btn-primary pull-right',
],
],
];
return $helper->generateForm([$fields_form]);
}
public function renderList()
{
// 1. Récupération des images et des pages associées (tableau principal)
$fields_list = [
'id_image_filter' => ['title' => $this->l('ID'), 'type' => 'text', 'align' => 'center'],
'id_page' => ['title' => $this->l('ID de la Page'), 'type' => 'text'],
'title' => ['title' => $this->l('Titre'), 'type' => 'text'],
'target_url' => ['title' => $this->l('URL cible'), 'type' => 'text'],
'image_url' => ['title' => $this->l('Image'), 'type' => 'image'],
'delete_link' => [
'title' => $this->l('Action'),
'callback' => 'renderDeleteLink',
'callback_object' => $this
],
];
$helper = new HelperList();
$helper->actions = ['edit'];
$helper->identifier = 'id_image_filter';
$helper->currentIndex = AdminController::$currentIndex . '&configure=' . $this->name;
$helper->token = Tools::getAdminTokenLite('AdminModules');
$filters = Db::getInstance()->executeS('SELECT * FROM `' . _DB_PREFIX_ . 'image_filter`');
// On ajoute le lien de suppression à chaque ligne
foreach ($filters as &$f) {
$f['delete_link'] = AdminController::$currentIndex.'&configure='.$this->name.'&deleteImageFilter&id_image_filter='.$f['id_image_filter'].'&token='.Tools::getAdminTokenLite('AdminModules');
}
// 2. Affichage des sous-catégories associées
$subCategoryFields = [
'id_page' => ['title' => $this->l('ID de la Page'), 'type' => 'text'],
'subcategories' => ['title' => $this->l('Sous-catégories associées'), 'type' => 'text'],
];
$subCategoryData = [];
foreach ($filters as $filter) {
$subCategories = $this->getAllSubCategories($filter['id_page']);
$subCategoryData[] = [
'id_page' => $filter['id_page'],
'subcategories' => !empty($subCategories) ? implode(', ', $subCategories) : $this->l('Aucune sous-catégorie'),
];
}
$helperSubCategories = new HelperList();
$helperSubCategories->identifier = 'id_page';
$helperSubCategories->currentIndex = AdminController::$currentIndex . '&configure=' . $this->name;
$helperSubCategories->token = Tools::getAdminTokenLite('AdminModules');
// 3. Génération des deux tableaux distincts
$output = '<h3>' . $this->l('Liste des filtres d\'images') . '</h3>';
$output .= $helper->generateList($filters, $fields_list);
$output .= '<h3>' . $this->l('Sous-catégories associées aux pages') . '</h3>';
$output .= $helperSubCategories->generateList($subCategoryData, $subCategoryFields);
return $output;
}
public function hookDisplayBeforeAmazingFilter($params)
{
$id_category = (int)Tools::getValue('id_category');
if (!$id_category) {
return ''; // Sortir si aucune catégorie n'est spécifiée
}
// Récupérer l'ID parent (si nécessaire)
$parentCategory = Db::getInstance()->getValue('SELECT id_parent FROM `' . _DB_PREFIX_ . 'category` WHERE id_category = ' . $id_category);
// Récupération des sous-catégories
$categoryIds = $this->getAllSubCategories($id_category);
$categoryIds[] = $id_category; // Inclure la catégorie elle-même
// Ajouter l'ID parent si nécessaire
if ($parentCategory && $parentCategory != 0) {
$categoryIds[] = $parentCategory;
}
// Requête SQL
$sql = 'SELECT * FROM `' . _DB_PREFIX_ . 'image_filter` WHERE `id_page` IN (' . implode(',', array_map('intval', $categoryIds)) . ')';
$images = Db::getInstance()->executeS($sql);
if (empty($images)) {
return ''; // Sortir si aucun filtre n'est trouvé
}
// Assignation des images à Smarty
$this->context->smarty->assign(['images' => $images]);
return $this->display(__FILE__, 'views/templates/hook/myimagefilter.tpl');
}
private function getAllSubCategories($id_category)
{
$sql = 'SELECT id_category FROM `' . _DB_PREFIX_ . 'category` WHERE `id_parent` = ' . (int)$id_category;
$subCategories = Db::getInstance()->executeS($sql);
$categoryIds = [];
foreach ($subCategories as $category) {
$categoryIds[] = $category['id_category'];
// Appel récursif pour récupérer les sous-catégories enfants
$categoryIds = array_merge($categoryIds, $this->getAllSubCategories($category['id_category']));
}
return $categoryIds;
}
public function hookHeader($params)
{
$this->context->controller->addCSS($this->_path . 'views/css/styles.css');
}
public function renderDeleteLink($echo, $tr)
{
return '<a href="'.$tr['delete_link'].'" class="btn btn-danger" onclick="return confirm(\''.$this->l('Êtes-vous sûr ?').'\');">'.$this->l('Supprimer').'</a>';
}
}
<div class="image-filter-container">
{foreach from=$images item=image}
<div class="image-filter-item">
<a href="{$image.target_url}">
<img src="{$image.image_url}" alt="{$image.title}" class="img-responsive" />
</a>
<p class="image-filter-title">{$image.title}</p>
</div>
{/foreach}
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
// Sélectionne toutes les images cliquables
const filterImages = document.querySelectorAll('.image-filter-item a');
// Récupère l'URL actuelle de la page
const currentUrl = window.location.href;
filterImages.forEach((link) => {
const targetUrl = new URL(link.href); // Création de l'URL cible
const targetParams = targetUrl.searchParams;
// **Vérification de la correspondance avec l'URL actuelle**
let isSelected = false;
// Si l'URL du filtre est une URL absolue, on compare l'URL complète
if (link.href.startsWith('http')) {
if (link.href === currentUrl) {
isSelected = true;
}
} else {
// Si c'est une URL relative, on compare les paramètres de l'URL
const currentParams = new URL(window.location.href).searchParams;
let allParamsMatch = true;
targetParams.forEach((value, key) => {
if (currentParams.get(key) !== value) {
allParamsMatch = false;
}
});
isSelected = allParamsMatch;
}
// **Ajout de la classe "selected" au hover actif**
const imgElement = link.querySelector('img');
if (isSelected) {
imgElement.classList.add('selected');
}
// **Gère le clic sur les liens**
link.addEventListener('click', function (event) {
event.preventDefault();
// Si le lien est une URL complète (ex: http:// ou https://), on redirige vers la page
if (link.href.startsWith('http')) {
window.location.href = link.href;
return; // On arrête l'exécution du reste de la fonction
}
// Sinon, on applique les filtres en utilisant les paramètres de l'URL cible
const newUrl = new URL(window.location.href);
targetParams.forEach((value, key) => {
newUrl.searchParams.set(key, value);
});
// Met à jour l'URL et recharge la page
window.history.replaceState({}, '', newUrl);
window.location.reload();
});
});
// **Gestion du bouton "Effacer les filtres"**
const clearButton = document.querySelector('.clearAll .all');
if (clearButton) {
clearButton.addEventListener('click', function (event) {
event.preventDefault();
// Retire la classe "selected"
document.querySelectorAll('.image-filter-item img.selected').forEach((img) => {
img.classList.remove('selected');
});
// Supprime tous les paramètres de l'URL
const clearUrl = new URL(window.location.href);
clearUrl.search = '';
// Met à jour l'URL et recharge la page
window.location.href = clearUrl.href;
});
}
});
</script>