Add Smarty plugin, auto-detect theme icons, PS 1.6 compat, new icons

- Add {mpr_icon name='x'} Smarty plugin for .tpl templates
- Auto-detect theme icon set (FA vs Material) without config
- Self-host icon fonts automatically if theme doesn't include them
- PS 1.6 compatible (addCSS fallback for registerStylesheet)
- Add 16 new icons: camera, play, video, chart, pie-chart, trending-up/down,
  tune, build, swap, touch, inventory, payments, campaign, trophy,
  schedule, history, verified, bolt
- Simplify MprIconsAssets: automatic inclusion, no config toggle needed
- Multiple modules calling registerAssets = single CSS load (dedup)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-14 09:33:48 +00:00
parent 99a8f5bf9b
commit 5b3374e2d5
3 changed files with 219 additions and 240 deletions

View File

@@ -5,8 +5,13 @@
*
* Simple API: MprIcons::get('account') - that's it.
*
* - Admin: auto-detects and uses Material Icons
* - Front: reads icon set from ps_mpr_config table
* - Auto-detects theme icon set (Font Awesome or Material Icons)
* - Admin: always Material Icons
* - Front: detects from theme, falls back to Font Awesome
* - Self-hosts icon fonts if theme doesn't include them
* - Smarty: {mpr_icon name='account'} in .tpl templates
*
* Compatible with PrestaShop 1.6 through 9.1
*
* @package myprestarocks/prestashop-icons
*/
@@ -23,6 +28,7 @@ class MprIcons
const CONFIG_KEY = 'front_icon_set';
private static $iconSet = null;
private static $smartyRegistered = false;
/**
* Semantic name => [FA class, Material name]
@@ -42,6 +48,10 @@ class MprIcons
'gift' => ['fa fa-gift', 'card_giftcard'],
'tag' => ['fa fa-tag', 'local_offer'],
'percent' => ['fa fa-percent', 'percent'],
'inventory' => ['fa fa-cube', 'inventory_2'],
'payments' => ['fa fa-money', 'payments'],
'campaign' => ['fa fa-bullhorn', 'campaign'],
'trophy' => ['fa fa-trophy', 'emoji_events'],
// Status
'success' => ['fa fa-check', 'check'],
@@ -60,6 +70,16 @@ class MprIcons
'save' => ['fa fa-save', 'save'],
'print' => ['fa fa-print', 'print'],
'copy' => ['fa fa-copy', 'content_copy'],
'tune' => ['fa fa-sliders', 'tune'],
'build' => ['fa fa-wrench', 'build'],
'swap' => ['fa fa-exchange', 'swap_horiz'],
'touch' => ['fa fa-hand-pointer-o', 'touch_app'],
// Analytics & Charts
'chart' => ['fa fa-bar-chart', 'bar_chart'],
'pie-chart' => ['fa fa-pie-chart', 'pie_chart'],
'trending-up' => ['fa fa-line-chart', 'trending_up'],
'trending-down' => ['fa fa-line-chart', 'trending_down'],
// UI
'close' => ['fa fa-times', 'close'],
@@ -90,6 +110,18 @@ class MprIcons
'unlock' => ['fa fa-unlock', 'lock_open'],
'shield' => ['fa fa-shield', 'shield'],
'key' => ['fa fa-key', 'vpn_key'],
'verified' => ['fa fa-check-circle', 'verified_user'],
'bolt' => ['fa fa-bolt', 'bolt'],
// Media
'camera' => ['fa fa-camera', 'add_a_photo'],
'play' => ['fa fa-play-circle', 'play_circle'],
'image' => ['fa fa-image', 'image'],
'video' => ['fa fa-video-camera', 'videocam'],
// Time & History
'schedule' => ['fa fa-clock-o', 'schedule'],
'history' => ['fa fa-history', 'history'],
// Misc
'home' => ['fa fa-home', 'home'],
@@ -102,7 +134,6 @@ class MprIcons
'eye-off' => ['fa fa-eye-slash', 'visibility_off'],
'file' => ['fa fa-file', 'description'],
'folder' => ['fa fa-folder', 'folder'],
'image' => ['fa fa-image', 'image'],
'link' => ['fa fa-link', 'link'],
'external' => ['fa fa-external-link', 'open_in_new'],
'help' => ['fa fa-question-circle', 'help'],
@@ -151,6 +182,34 @@ class MprIcons
return $set === self::MATERIAL ? $material : $fa;
}
/**
* Smarty {mpr_icon} function handler
*
* Usage in .tpl:
* {mpr_icon name='account'}
* {mpr_icon name='cart' class='text-primary'}
*/
public static function smartyFunction($params, $smarty): string
{
return self::get($params['name'] ?? '', $params['class'] ?? '');
}
/**
* Register {mpr_icon} as a Smarty function plugin
* Safe to call multiple times — registers only once
*
* @param \Smarty|\SmartyBC $smarty Smarty instance
*/
public static function registerSmartyPlugin($smarty): void
{
if (self::$smartyRegistered) {
return;
}
$smarty->registerPlugin('function', 'mpr_icon', [self::class, 'smartyFunction']);
self::$smartyRegistered = true;
}
/**
* Detect and cache the icon set to use
*/
@@ -160,14 +219,21 @@ class MprIcons
return self::$iconSet;
}
// Admin context = Material Icons
// Admin context = Material Icons (PS 1.7+ admin always loads them)
if (defined('_PS_ADMIN_DIR_')) {
self::$iconSet = self::MATERIAL;
return self::$iconSet;
}
// Front office - read from config table
self::$iconSet = self::readConfigTable() ?: self::FA;
// Front office - check config table first, then auto-detect
$fromConfig = self::readConfigTable();
if ($fromConfig !== null) {
self::$iconSet = $fromConfig;
return self::$iconSet;
}
// No config stored — auto-detect from theme
self::$iconSet = self::detectThemeIcons();
return self::$iconSet;
}
@@ -192,7 +258,7 @@ class MprIcons
return $result;
}
} catch (\Exception $e) {
// Table doesn't exist or query failed - use default
// Table doesn't exist or query failed
}
return null;
@@ -222,7 +288,7 @@ class MprIcons
$result = \Db::getInstance()->execute($sql);
if ($result) {
self::$iconSet = null; // Clear cache
self::$iconSet = null;
}
return $result;
@@ -250,8 +316,8 @@ class MprIcons
}
/**
* Initialize icon system - call from module install
* Creates config table if needed, sets default icon set
* Initialize icon system call from module install
* Creates config table if needed, detects and saves icon set
*/
public static function init(): bool
{
@@ -259,7 +325,6 @@ class MprIcons
return false;
}
// If no config exists, detect and save default
$current = self::readConfigTable();
if ($current === null) {
$detected = self::detectThemeIcons();
@@ -281,7 +346,6 @@ class MprIcons
try {
$table = _DB_PREFIX_ . self::CONFIG_TABLE;
// Check if table exists
$exists = \Db::getInstance()->getValue(
"SELECT COUNT(*) FROM information_schema.tables
WHERE table_schema = DATABASE()
@@ -292,7 +356,8 @@ class MprIcons
return true;
}
// Create table
$engine = defined('_MYSQL_ENGINE_') ? _MYSQL_ENGINE_ : 'InnoDB';
$sql = "CREATE TABLE IF NOT EXISTS `{$table}` (
`id_config` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`module` VARCHAR(64) NOT NULL,
@@ -303,7 +368,7 @@ class MprIcons
PRIMARY KEY (`id_config`),
UNIQUE KEY `module_key` (`module`, `key`),
KEY `module` (`module`)
) ENGINE=" . _MYSQL_ENGINE_ . " DEFAULT CHARSET=utf8mb4;";
) ENGINE={$engine} DEFAULT CHARSET=utf8mb4;";
return \Db::getInstance()->execute($sql);
} catch (\Exception $e) {
@@ -312,44 +377,119 @@ class MprIcons
}
/**
* Detect which icon set the current theme likely uses
* Returns 'fontawesome' for most PrestaShop themes
* Detect which icon set the current theme uses
*
* Checks theme config and assets for Material Icons indicators.
* Returns 'fontawesome' for classic and most themes,
* 'material' for Hummingbird and Material-based themes.
*/
public static function detectThemeIcons(): string
{
// Most PrestaShop themes (including classic) use Font Awesome
// Only return Material if we can positively detect it
if (!defined('_PS_THEME_DIR_')) {
return self::FA;
}
try {
// Check theme.yml for icon hints
// PS 1.7+ — check theme.yml
$themeYml = _PS_THEME_DIR_ . 'config/theme.yml';
if (file_exists($themeYml)) {
$content = file_get_contents($themeYml);
if (strpos($content, 'material') !== false || strpos($content, 'Material') !== false) {
if (stripos($content, 'hummingbird') !== false) {
return self::MATERIAL;
}
if (stripos($content, 'material-icons') !== false || stripos($content, 'material_icons') !== false) {
return self::MATERIAL;
}
}
// Check if theme has material-icons in assets
$assetsDir = _PS_THEME_DIR_ . 'assets/css/';
if (is_dir($assetsDir)) {
$files = scandir($assetsDir);
// Check theme assets for material-icons CSS
$dirs = [
_PS_THEME_DIR_ . 'assets/css/',
_PS_THEME_DIR_ . 'css/',
];
foreach ($dirs as $dir) {
if (!is_dir($dir)) {
continue;
}
$files = scandir($dir);
foreach ($files as $file) {
if (strpos($file, 'material') !== false) {
if (stripos($file, 'material') !== false) {
return self::MATERIAL;
}
}
}
// PS 1.6 — check for parent theme name
if (defined('_THEME_NAME_')) {
$themeName = _THEME_NAME_;
if (stripos($themeName, 'hummingbird') !== false) {
return self::MATERIAL;
}
}
} catch (\Exception $e) {
// Detection failed — use safe default
}
return self::FA;
}
/**
* Check if the current theme already includes the needed icon font
* Used by MprIconsAssets to decide whether to self-host
*/
public static function themeHasIconFont(): bool
{
if (!defined('_PS_THEME_DIR_')) {
return false;
}
$iconSet = self::getIconSet();
try {
$dirs = [
_PS_THEME_DIR_ . 'assets/css/',
_PS_THEME_DIR_ . 'css/',
];
foreach ($dirs as $dir) {
if (!is_dir($dir)) {
continue;
}
$files = scandir($dir);
foreach ($files as $file) {
if ($file === '.' || $file === '..') {
continue;
}
if ($iconSet === self::MATERIAL) {
if (stripos($file, 'material') !== false) {
return true;
}
} else {
if (stripos($file, 'fontawesome') !== false || stripos($file, 'font-awesome') !== false) {
return true;
}
}
}
}
// For Font Awesome — most PS 1.6/1.7 themes include it as a dependency
// Check if theme's package.json or theme.yml lists it
if ($iconSet === self::FA) {
$themeYml = _PS_THEME_DIR_ . 'config/theme.yml';
if (file_exists($themeYml)) {
$content = file_get_contents($themeYml);
if (stripos($content, 'font-awesome') !== false || stripos($content, 'fontawesome') !== false) {
return true;
}
}
}
} catch (\Exception $e) {
// Ignore detection errors
// Detection failed
}
// Default to Font Awesome (safe for 95% of themes)
return self::FA;
return false;
}
/**
@@ -369,9 +509,8 @@ class MprIcons
}
/**
* Register icon font CSS assets
* Convenience method that delegates to MprIconsAssets
* Call from hookHeader or setMedia
* Register icon font CSS assets if theme doesn't provide them
* Delegates to MprIconsAssets. Safe to call from multiple modules.
*
* @param \Module $module The module instance
* @return bool Whether assets were registered
@@ -380,12 +519,4 @@ class MprIcons
{
return MprIconsAssets::registerAssets($module);
}
/**
* Check if self-hosted icon fonts are enabled
*/
public static function isSelfHostEnabled(): bool
{
return MprIconsConfig::isSelfHostEnabled();
}
}

View File

@@ -1,11 +1,13 @@
<?php
/**
* MprIconsAssets - CSS injection helper for self-hosted icon fonts
* MprIconsAssets - Automatic icon font CSS loader
*
* Usage in module:
* // In hookHeader or setMedia:
* MprIconsAssets::registerAssets($this);
* Detects if the theme provides icon fonts. If not, self-hosts them
* from the bundled assets. Safe to call from multiple modules —
* CSS is registered with a fixed ID so it loads only once.
*
* Compatible with PrestaShop 1.6 through 9.1
*
* @package myprestarocks/prestashop-icons
*/
@@ -14,9 +16,10 @@ namespace MyPrestaRocks\Icons;
class MprIconsAssets
{
private static $registered = false;
/**
* Get the base path to this package's assets
* Works regardless of which module includes the package
*/
public static function getAssetsPath(): string
{
@@ -25,39 +28,48 @@ class MprIconsAssets
/**
* Get the assets URL relative to module
* Requires module context to determine URL path
*
* @param \Module $module The module instance
* @return string URL path to assets
*/
public static function getAssetsUrl(\Module $module): string
{
// Find the vendor path relative to module
$modulePath = $module->getLocalPath();
$assetsPath = self::getAssetsPath();
// Get relative path from module to assets
$relativePath = str_replace($modulePath, '', $assetsPath);
return $module->getPathUri() . ltrim($relativePath, '/');
}
/**
* Register icon font CSS if self-hosting is enabled
* Call this from your module's hookHeader or setMedia
* Register icon font CSS — automatic detection
*
* 1. Skips in admin context (admin always has Material Icons)
* 2. Checks if theme already provides the needed icon font
* 3. If not, registers bundled CSS from this package
* 4. Uses fixed registration ID — multiple modules calling = one load
*
* Call from hookActionFrontControllerSetMedia or hookHeader.
*
* @param \Module $module The module instance
* @return bool Whether assets were registered
* @return bool Whether assets were registered (false if skipped or already loaded)
*/
public static function registerAssets(\Module $module): bool
{
// Skip in admin context (admin always has Material Icons)
// Skip in admin context
if (defined('_PS_ADMIN_DIR_')) {
return false;
}
// Check if self-hosting is enabled
if (!MprIconsConfig::isSelfHostEnabled()) {
// Already registered by another module in this request
if (self::$registered) {
return false;
}
// Theme already includes the needed icon font — skip
if (MprIcons::themeHasIconFont()) {
self::$registered = true;
return false;
}
@@ -70,14 +82,23 @@ class MprIconsAssets
$cssFile = $assetsUrl . '/fontawesome/fontawesome.min.css';
}
// Register CSS
$controller = \Context::getContext()->controller;
if ($controller instanceof \FrontController) {
// PS 1.7+ — use registerStylesheet (deduplicates by ID)
if (method_exists($controller, 'registerStylesheet')) {
$controller->registerStylesheet(
'mpr-icons-' . $iconSet,
$cssFile,
['media' => 'all', 'priority' => 50]
['media' => 'all', 'priority' => 50, 'server' => 'remote']
);
self::$registered = true;
return true;
}
// PS 1.6 — use addCSS (deduplicates by URI)
if (method_exists($controller, 'addCSS')) {
$controller->addCSS($cssFile, 'all');
self::$registered = true;
return true;
}
@@ -85,17 +106,14 @@ class MprIconsAssets
}
/**
* Get CSS link tag HTML (for manual inclusion)
* Get CSS link tag HTML for manual inclusion
* Useful for email templates or non-standard contexts
*
* @param \Module $module The module instance
* @return string HTML link tag or empty string
* @return string HTML link tag
*/
public static function getCssTag(\Module $module): string
{
if (!MprIconsConfig::isSelfHostEnabled()) {
return '';
}
$iconSet = MprIcons::getIconSet();
$assetsUrl = self::getAssetsUrl($module);
@@ -111,7 +129,7 @@ class MprIconsAssets
/**
* Check if bundled assets exist
*
* @param string $iconSet The icon set to check ('fontawesome' or 'material')
* @param string $iconSet The icon set to check
* @return bool
*/
public static function assetsExist(string $iconSet): bool
@@ -124,70 +142,4 @@ class MprIconsAssets
return file_exists($assetsPath . '/fontawesome/fontawesome.min.css');
}
/**
* Copy assets to module's public directory
* Use this if your module needs assets in a specific location
*
* @param string $targetDir Target directory
* @param string $iconSet Which icon set to copy
* @return bool Success
*/
public static function copyAssets(string $targetDir, string $iconSet): bool
{
$assetsPath = self::getAssetsPath();
if ($iconSet === MprIcons::MATERIAL) {
$sourceDir = $assetsPath . '/material-icons';
} else {
$sourceDir = $assetsPath . '/fontawesome';
}
if (!is_dir($sourceDir)) {
return false;
}
return self::recursiveCopy($sourceDir, $targetDir);
}
/**
* Recursively copy directory
*/
private static function recursiveCopy(string $source, string $dest): bool
{
if (!is_dir($dest)) {
if (!mkdir($dest, 0755, true)) {
return false;
}
}
$dir = opendir($source);
if (!$dir) {
return false;
}
while (($file = readdir($dir)) !== false) {
if ($file === '.' || $file === '..') {
continue;
}
$srcPath = $source . '/' . $file;
$destPath = $dest . '/' . $file;
if (is_dir($srcPath)) {
if (!self::recursiveCopy($srcPath, $destPath)) {
closedir($dir);
return false;
}
} else {
if (!copy($srcPath, $destPath)) {
closedir($dir);
return false;
}
}
}
closedir($dir);
return true;
}
}

View File

@@ -1,11 +1,11 @@
<?php
/**
* MprIconsConfig - Form field generator for icon set configuration
* MprIconsConfig - Admin form fields for icon set configuration
*
* Usage in module admin:
* $fields = array_merge($fields, MprIconsConfig::getFormFields());
* // Then in postProcess:
* // In postProcess:
* MprIconsConfig::processForm();
*
* @package myprestarocks/prestashop-icons
@@ -15,12 +15,10 @@ namespace MyPrestaRocks\Icons;
class MprIconsConfig
{
const CONFIG_KEY_SELFHOST = 'front_icon_selfhost';
/**
* Get form fields for HelperForm integration
*
* @param string $formName Form name prefix (default: 'MPR_ICONS')
* @param string $formName Form name prefix
* @return array HelperForm-compatible field definitions
*/
public static function getFormFields(string $formName = 'MPR_ICONS'): array
@@ -30,27 +28,16 @@ class MprIconsConfig
'type' => 'select',
'label' => 'Front Office Icon Set',
'name' => $formName . '_ICON_SET',
'desc' => 'Choose which icon font to use on the front office. Most themes include Font Awesome.',
'desc' => 'Choose which icon font to use on the front office. Auto-detected from theme on install. Override here if needed.',
'options' => [
'query' => [
['id' => MprIcons::FA, 'name' => 'Font Awesome (default)'],
['id' => MprIcons::MATERIAL, 'name' => 'Material Icons'],
['id' => MprIcons::FA, 'name' => 'Font Awesome (classic theme)'],
['id' => MprIcons::MATERIAL, 'name' => 'Material Icons (hummingbird theme)'],
],
'id' => 'id',
'name' => 'name',
],
],
[
'type' => 'switch',
'label' => 'Include Icon Fonts from Module',
'name' => $formName . '_SELFHOST',
'desc' => 'Enable this if your theme does not load icon fonts. The module will include the necessary CSS.',
'is_bool' => true,
'values' => [
['id' => 'active_on', 'value' => 1, 'label' => 'Yes'],
['id' => 'active_off', 'value' => 0, 'label' => 'No'],
],
],
];
}
@@ -64,7 +51,6 @@ class MprIconsConfig
{
return [
$formName . '_ICON_SET' => MprIcons::getIconSet(),
$formName . '_SELFHOST' => self::isSelfHostEnabled() ? 1 : 0,
];
}
@@ -77,96 +63,6 @@ class MprIconsConfig
public static function processForm(string $formName = 'MPR_ICONS'): bool
{
$iconSet = \Tools::getValue($formName . '_ICON_SET', MprIcons::FA);
$selfHost = (int) \Tools::getValue($formName . '_SELFHOST', 0);
$success = MprIcons::saveIconSet($iconSet);
$success = self::saveSelfHostOption($selfHost) && $success;
return $success;
}
/**
* Check if self-hosting is enabled
*/
public static function isSelfHostEnabled(): bool
{
$value = self::readConfig(self::CONFIG_KEY_SELFHOST);
return $value === '1';
}
/**
* Save self-host option
*/
public static function saveSelfHostOption(int $enabled): bool
{
return self::saveConfig(self::CONFIG_KEY_SELFHOST, $enabled ? '1' : '0');
}
/**
* Read config from mpr_config table
*/
private static function readConfig(string $key): ?string
{
if (!defined('_DB_PREFIX_')) {
return null;
}
try {
$table = _DB_PREFIX_ . MprIcons::CONFIG_TABLE;
$sql = "SELECT `value` FROM `{$table}`
WHERE `module` = '" . pSQL(MprIcons::CONFIG_MODULE) . "'
AND `key` = '" . pSQL($key) . "'";
$result = \Db::getInstance()->getValue($sql);
return $result !== false ? $result : null;
} catch (\Exception $e) {
return null;
}
}
/**
* Save config to mpr_config table
*/
private static function saveConfig(string $key, string $value): bool
{
if (!defined('_DB_PREFIX_')) {
return false;
}
// Ensure table exists
MprIcons::ensureConfigTable();
try {
$table = _DB_PREFIX_ . MprIcons::CONFIG_TABLE;
$now = date('Y-m-d H:i:s');
$sql = "INSERT INTO `{$table}` (`module`, `key`, `value`, `date_add`, `date_upd`)
VALUES ('" . pSQL(MprIcons::CONFIG_MODULE) . "', '" . pSQL($key) . "', '" . pSQL($value) . "', '{$now}', '{$now}')
ON DUPLICATE KEY UPDATE `value` = '" . pSQL($value) . "', `date_upd` = '{$now}'";
return \Db::getInstance()->execute($sql);
} catch (\Exception $e) {
return false;
}
}
/**
* Get complete form input array for a HelperForm
* Includes form wrapper with legend
*
* @param string $formName Form name prefix
* @return array Complete form input structure
*/
public static function getFormInput(string $formName = 'MPR_ICONS'): array
{
return [
'form' => [
'legend' => [
'title' => 'Icon Settings',
'icon' => 'icon-picture',
],
'input' => self::getFormFields($formName),
],
];
return MprIcons::saveIconSet($iconSet);
}
}