Initial shared URL engine package
Extract URL pattern management, routing, entity lifecycle handling, and schema installation from mprfriendlyurl into a shared Composer package. Both mprfriendlyurl and mprseorevolution will consume this via configurable table prefix and pattern storage abstraction. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
27
composer.json
Normal file
27
composer.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "myprestarocks/prestashop-url",
|
||||
"description": "Shared URL engine for PrestaShop modules - pattern management, routing, entity lifecycle",
|
||||
"keywords": ["prestashop", "url", "seo", "routing", "rewrite"],
|
||||
"type": "library",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "mypresta.rocks",
|
||||
"email": "info@mypresta.rocks"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=7.1.3"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"MyPrestaRocks\\Url\\": "src/"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "1.0.x-dev"
|
||||
}
|
||||
},
|
||||
"minimum-stability": "stable"
|
||||
}
|
||||
1061
src/EntityLifecycleHandler.php
Normal file
1061
src/EntityLifecycleHandler.php
Normal file
File diff suppressed because it is too large
Load Diff
338
src/Schema/UrlSchemaInstaller.php
Normal file
338
src/Schema/UrlSchemaInstaller.php
Normal file
@@ -0,0 +1,338 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* URL Schema Installer
|
||||
*
|
||||
* Creates and migrates URL tables (cache, history, override, pattern).
|
||||
* Table prefix is configurable per module (e.g., 'mprfurl_' or 'mprseo_').
|
||||
*
|
||||
* @author mypresta.rocks
|
||||
* @copyright mypresta.rocks
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace MyPrestaRocks\Url\Schema;
|
||||
|
||||
class UrlSchemaInstaller
|
||||
{
|
||||
/** @var string Table prefix (e.g., 'mprfurl_' or 'mprseo_') */
|
||||
protected $tablePrefix;
|
||||
|
||||
/**
|
||||
* @param string $tablePrefix
|
||||
*/
|
||||
public function __construct($tablePrefix)
|
||||
{
|
||||
$this->tablePrefix = $tablePrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install all URL tables
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function install()
|
||||
{
|
||||
$engine = _MYSQL_ENGINE_;
|
||||
|
||||
return $this->createUrlCacheTable($engine)
|
||||
&& $this->createUrlHistoryTable($engine)
|
||||
&& $this->createUrlOverrideTable($engine)
|
||||
&& $this->createUrlPatternTable($engine);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstall all URL tables
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function uninstall()
|
||||
{
|
||||
$prefix = _DB_PREFIX_ . pSQL($this->tablePrefix);
|
||||
|
||||
return \Db::getInstance()->execute('DROP TABLE IF EXISTS `' . $prefix . 'url_cache`')
|
||||
&& \Db::getInstance()->execute('DROP TABLE IF EXISTS `' . $prefix . 'url_history`')
|
||||
&& \Db::getInstance()->execute('DROP TABLE IF EXISTS `' . $prefix . 'url_override`')
|
||||
&& \Db::getInstance()->execute('DROP TABLE IF EXISTS `' . $prefix . 'url_pattern`');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create url_cache table
|
||||
*
|
||||
* @param string $engine MySQL engine
|
||||
* @return bool
|
||||
*/
|
||||
protected function createUrlCacheTable($engine)
|
||||
{
|
||||
$prefix = _DB_PREFIX_ . pSQL($this->tablePrefix);
|
||||
|
||||
return \Db::getInstance()->execute('
|
||||
CREATE TABLE IF NOT EXISTS `' . $prefix . 'url_cache` (
|
||||
`id_cache` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`entity_type` VARCHAR(50) NOT NULL,
|
||||
`id_entity` INT NOT NULL,
|
||||
`id_lang` INT NOT NULL,
|
||||
`id_shop` INT NOT NULL,
|
||||
`url` VARCHAR(500) NOT NULL,
|
||||
`url_hash` CHAR(32) NOT NULL,
|
||||
`full_url` VARCHAR(600) DEFAULT NULL,
|
||||
`source` ENUM(\'pattern\', \'override\') DEFAULT \'pattern\',
|
||||
`pattern_version` INT DEFAULT 1,
|
||||
`date_add` DATETIME NOT NULL,
|
||||
`date_upd` DATETIME NOT NULL,
|
||||
UNIQUE KEY `unique_entity` (`entity_type`, `id_entity`, `id_lang`, `id_shop`),
|
||||
INDEX `idx_url_hash` (`url_hash`),
|
||||
INDEX `idx_shop_lang` (`id_shop`, `id_lang`)
|
||||
) ENGINE=' . $engine . ' DEFAULT CHARSET=utf8mb4
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create url_history table
|
||||
*
|
||||
* @param string $engine MySQL engine
|
||||
* @return bool
|
||||
*/
|
||||
protected function createUrlHistoryTable($engine)
|
||||
{
|
||||
$prefix = _DB_PREFIX_ . pSQL($this->tablePrefix);
|
||||
|
||||
return \Db::getInstance()->execute('
|
||||
CREATE TABLE IF NOT EXISTS `' . $prefix . 'url_history` (
|
||||
`id_history` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`entity_type` VARCHAR(50) NOT NULL,
|
||||
`id_entity` INT NOT NULL,
|
||||
`id_lang` INT NOT NULL,
|
||||
`id_shop` INT NOT NULL,
|
||||
`old_url` VARCHAR(500) NOT NULL,
|
||||
`old_url_hash` CHAR(32) NOT NULL,
|
||||
`new_url` VARCHAR(500) DEFAULT NULL,
|
||||
`redirect_type` ENUM(\'301\', \'302\', \'307\', \'410\') DEFAULT \'301\',
|
||||
`redirect_enabled` TINYINT(1) DEFAULT 1,
|
||||
`change_reason` ENUM(\'pattern_change\', \'manual_edit\', \'entity_update\', \'entity_delete\', \'bulk_regenerate\', \'entity_disabled\') DEFAULT \'entity_update\',
|
||||
`hits` INT DEFAULT 0,
|
||||
`last_hit` DATETIME DEFAULT NULL,
|
||||
`date_add` DATETIME NOT NULL,
|
||||
INDEX `idx_old_url_hash` (`old_url_hash`),
|
||||
INDEX `idx_entity` (`entity_type`, `id_entity`, `id_lang`),
|
||||
INDEX `idx_shop_enabled` (`id_shop`, `redirect_enabled`)
|
||||
) ENGINE=' . $engine . ' DEFAULT CHARSET=utf8mb4
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create url_override table (full version with redirect target fields)
|
||||
*
|
||||
* @param string $engine MySQL engine
|
||||
* @return bool
|
||||
*/
|
||||
protected function createUrlOverrideTable($engine)
|
||||
{
|
||||
$prefix = _DB_PREFIX_ . pSQL($this->tablePrefix);
|
||||
|
||||
return \Db::getInstance()->execute('
|
||||
CREATE TABLE IF NOT EXISTS `' . $prefix . 'url_override` (
|
||||
`id_override` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`entity_type` VARCHAR(50) NOT NULL,
|
||||
`id_entity` INT NOT NULL,
|
||||
`id_lang` INT NOT NULL,
|
||||
`id_shop` INT NOT NULL,
|
||||
`id_override_group` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`custom_url` VARCHAR(500) NOT NULL DEFAULT \'\',
|
||||
`custom_url_hash` CHAR(32) NOT NULL DEFAULT \'\',
|
||||
`target_entity_type` VARCHAR(50) DEFAULT NULL,
|
||||
`target_id_entity` INT DEFAULT 0,
|
||||
`redirect_type` ENUM(\'301\', \'302\', \'307\') DEFAULT \'301\',
|
||||
`target_custom_url` VARCHAR(500) DEFAULT NULL,
|
||||
`locked` TINYINT(1) DEFAULT 1,
|
||||
`date_add` DATETIME NOT NULL,
|
||||
`date_upd` DATETIME NOT NULL,
|
||||
UNIQUE KEY `unique_entity` (`entity_type`, `id_entity`, `id_lang`, `id_shop`),
|
||||
UNIQUE KEY `unique_url` (`custom_url_hash`, `id_lang`, `id_shop`),
|
||||
INDEX `idx_shop_lang` (`id_shop`, `id_lang`),
|
||||
INDEX `idx_override_group` (`id_override_group`)
|
||||
) ENGINE=' . $engine . ' DEFAULT CHARSET=utf8mb4
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create url_pattern table (for SqlPatternStorage)
|
||||
*
|
||||
* @param string $engine MySQL engine
|
||||
* @return bool
|
||||
*/
|
||||
protected function createUrlPatternTable($engine)
|
||||
{
|
||||
$prefix = _DB_PREFIX_ . pSQL($this->tablePrefix);
|
||||
|
||||
return \Db::getInstance()->execute('
|
||||
CREATE TABLE IF NOT EXISTS `' . $prefix . 'url_pattern` (
|
||||
`id_pattern` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`entity_type` VARCHAR(50) NOT NULL,
|
||||
`id_shop` INT NOT NULL,
|
||||
`enabled` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`pattern` VARCHAR(500) NOT NULL,
|
||||
`suffix` VARCHAR(10) DEFAULT \'\',
|
||||
`include_parent_categories` TINYINT(1) DEFAULT 0,
|
||||
`category_separator` VARCHAR(10) DEFAULT \'/\',
|
||||
`max_category_depth` INT DEFAULT 0,
|
||||
`include_cms_category` TINYINT(1) DEFAULT 0,
|
||||
`lowercase` TINYINT(1) DEFAULT 1,
|
||||
`replace_spaces` VARCHAR(10) DEFAULT \'-\',
|
||||
`remove_accents` TINYINT(1) DEFAULT 1,
|
||||
`remove_special_chars` TINYINT(1) DEFAULT 1,
|
||||
`collision_suffix` ENUM(\'id\', \'increment\', \'sku\', \'ean13\') DEFAULT \'id\',
|
||||
`cache_enabled` TINYINT(1) DEFAULT 1,
|
||||
`cache_ttl` INT DEFAULT 86400,
|
||||
`redirect_on_delete` VARCHAR(20) DEFAULT \'parent\',
|
||||
`redirect_on_disable` VARCHAR(20) DEFAULT \'parent\',
|
||||
`redirect_on_url_change` VARCHAR(20) DEFAULT \'301\',
|
||||
`product_redirect_target` VARCHAR(50) DEFAULT \'default_category\',
|
||||
`disable_attribute_urls` TINYINT(1) DEFAULT 0,
|
||||
`attribute_url_style` VARCHAR(50) DEFAULT \'group-value\',
|
||||
`date_add` DATETIME NOT NULL,
|
||||
`date_upd` DATETIME NOT NULL,
|
||||
UNIQUE KEY `unique_type_shop` (`entity_type`, `id_shop`),
|
||||
INDEX `idx_enabled` (`enabled`)
|
||||
) ENGINE=' . $engine . ' DEFAULT CHARSET=utf8mb4
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate mprseorevolution's url_override table to add missing columns
|
||||
* Safe to call multiple times — uses IF NOT EXISTS / column existence checks
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function migrateOverrideTable()
|
||||
{
|
||||
$prefix = _DB_PREFIX_ . pSQL($this->tablePrefix);
|
||||
$db = \Db::getInstance();
|
||||
|
||||
$columns = $this->getTableColumns($prefix . 'url_override');
|
||||
|
||||
$success = true;
|
||||
|
||||
if (!in_array('id_override_group', $columns)) {
|
||||
$success = $success && $db->execute('
|
||||
ALTER TABLE `' . $prefix . 'url_override`
|
||||
ADD COLUMN `id_override_group` INT UNSIGNED NOT NULL DEFAULT 0 AFTER `id_shop`,
|
||||
ADD INDEX `idx_override_group` (`id_override_group`)
|
||||
');
|
||||
}
|
||||
|
||||
if (!in_array('target_entity_type', $columns)) {
|
||||
$success = $success && $db->execute('
|
||||
ALTER TABLE `' . $prefix . 'url_override`
|
||||
ADD COLUMN `target_entity_type` VARCHAR(50) DEFAULT NULL AFTER `locked`
|
||||
');
|
||||
}
|
||||
|
||||
if (!in_array('target_id_entity', $columns)) {
|
||||
$success = $success && $db->execute('
|
||||
ALTER TABLE `' . $prefix . 'url_override`
|
||||
ADD COLUMN `target_id_entity` INT DEFAULT 0 AFTER `target_entity_type`
|
||||
');
|
||||
}
|
||||
|
||||
if (!in_array('redirect_type', $columns)) {
|
||||
$success = $success && $db->execute('
|
||||
ALTER TABLE `' . $prefix . 'url_override`
|
||||
ADD COLUMN `redirect_type` ENUM(\'301\', \'302\', \'307\') DEFAULT \'301\' AFTER `target_id_entity`
|
||||
');
|
||||
}
|
||||
|
||||
if (!in_array('target_custom_url', $columns)) {
|
||||
$success = $success && $db->execute('
|
||||
ALTER TABLE `' . $prefix . 'url_override`
|
||||
ADD COLUMN `target_custom_url` VARCHAR(500) DEFAULT NULL AFTER `redirect_type`
|
||||
');
|
||||
}
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate url_history table to add entity_disabled change_reason
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function migrateHistoryTable()
|
||||
{
|
||||
$prefix = _DB_PREFIX_ . pSQL($this->tablePrefix);
|
||||
$db = \Db::getInstance();
|
||||
|
||||
// Check if entity_disabled is already in the ENUM
|
||||
$columnInfo = $db->getRow('
|
||||
SELECT COLUMN_TYPE FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = "' . pSQL($prefix) . 'url_history"
|
||||
AND COLUMN_NAME = "change_reason"
|
||||
');
|
||||
|
||||
if ($columnInfo && strpos($columnInfo['COLUMN_TYPE'], 'entity_disabled') === false) {
|
||||
return $db->execute('
|
||||
ALTER TABLE `' . $prefix . 'url_history`
|
||||
MODIFY COLUMN `change_reason`
|
||||
ENUM(\'pattern_change\', \'manual_edit\', \'entity_update\', \'entity_delete\', \'bulk_regenerate\', \'entity_disabled\')
|
||||
DEFAULT \'entity_update\'
|
||||
');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run all migrations (safe to call multiple times)
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function migrate()
|
||||
{
|
||||
return $this->migrateOverrideTable()
|
||||
&& $this->migrateHistoryTable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get column names for a table
|
||||
*
|
||||
* @param string $tableName Full table name (with ps_ prefix)
|
||||
* @return array
|
||||
*/
|
||||
protected function getTableColumns($tableName)
|
||||
{
|
||||
$columns = [];
|
||||
|
||||
$rows = \Db::getInstance()->executeS('
|
||||
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = "' . pSQL($tableName) . '"
|
||||
');
|
||||
|
||||
foreach ($rows ?: [] as $row) {
|
||||
$columns[] = $row['COLUMN_NAME'];
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a specific URL table exists
|
||||
*
|
||||
* @param string $tableSuffix e.g., 'url_cache', 'url_history'
|
||||
* @return bool
|
||||
*/
|
||||
public function tableExists($tableSuffix)
|
||||
{
|
||||
$prefix = _DB_PREFIX_ . pSQL($this->tablePrefix);
|
||||
$tableName = $prefix . pSQL($tableSuffix);
|
||||
|
||||
$result = \Db::getInstance()->getValue('
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = "' . pSQL($tableName) . '"
|
||||
');
|
||||
|
||||
return (int) $result > 0;
|
||||
}
|
||||
}
|
||||
82
src/Storage/ConfigPatternStorage.php
Normal file
82
src/Storage/ConfigPatternStorage.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Config-based Pattern Storage
|
||||
*
|
||||
* Stores URL patterns as JSON in the mpr_config table
|
||||
* via MyPrestaRocks\Admin\Config\ConfigTable.
|
||||
* Used by mprfriendlyurl.
|
||||
*
|
||||
* @author mypresta.rocks
|
||||
* @copyright mypresta.rocks
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace MyPrestaRocks\Url\Storage;
|
||||
|
||||
use MyPrestaRocks\Admin\Config\ConfigTable;
|
||||
|
||||
class ConfigPatternStorage implements PatternStorageInterface
|
||||
{
|
||||
/** @var string Module name for ConfigTable scope */
|
||||
protected $moduleName;
|
||||
|
||||
/** @var array Default patterns per entity type */
|
||||
protected $defaults;
|
||||
|
||||
/**
|
||||
* @param string $moduleName Module name used as ConfigTable namespace
|
||||
* @param array $defaults Default pattern configs keyed by entity type
|
||||
*/
|
||||
public function __construct($moduleName, array $defaults = [])
|
||||
{
|
||||
$this->moduleName = $moduleName;
|
||||
$this->defaults = $defaults;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getPattern($entityType, $idShop)
|
||||
{
|
||||
$configKey = 'pattern.' . $entityType;
|
||||
$stored = ConfigTable::get($this->moduleName, $configKey, $idShop);
|
||||
|
||||
if ($stored) {
|
||||
$pattern = json_decode($stored, true);
|
||||
if (is_array($pattern)) {
|
||||
return $pattern;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->defaults[$entityType] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setPattern($entityType, $idShop, array $data)
|
||||
{
|
||||
$configKey = 'pattern.' . $entityType;
|
||||
|
||||
return ConfigTable::set($this->moduleName, $configKey, json_encode($data), $idShop);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getAllPatterns($idShop)
|
||||
{
|
||||
$entityTypes = ['product', 'category', 'cms', 'cms_category', 'manufacturer', 'supplier'];
|
||||
$patterns = [];
|
||||
|
||||
foreach ($entityTypes as $type) {
|
||||
$pattern = $this->getPattern($type, $idShop);
|
||||
if ($pattern) {
|
||||
$patterns[$type] = $pattern;
|
||||
}
|
||||
}
|
||||
|
||||
return $patterns;
|
||||
}
|
||||
}
|
||||
45
src/Storage/PatternStorageInterface.php
Normal file
45
src/Storage/PatternStorageInterface.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Pattern Storage Interface
|
||||
*
|
||||
* Abstraction for URL pattern configuration storage.
|
||||
* Modules choose between SQL table storage (mprseorevolution)
|
||||
* or JSON config storage via ConfigTable (mprfriendlyurl).
|
||||
*
|
||||
* @author mypresta.rocks
|
||||
* @copyright mypresta.rocks
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace MyPrestaRocks\Url\Storage;
|
||||
|
||||
interface PatternStorageInterface
|
||||
{
|
||||
/**
|
||||
* Get the URL pattern configuration for an entity type
|
||||
*
|
||||
* @param string $entityType One of: product, category, cms, cms_category, manufacturer, supplier
|
||||
* @param int $idShop
|
||||
* @return array|null Pattern config array or null if not set
|
||||
*/
|
||||
public function getPattern($entityType, $idShop);
|
||||
|
||||
/**
|
||||
* Save URL pattern configuration for an entity type
|
||||
*
|
||||
* @param string $entityType
|
||||
* @param int $idShop
|
||||
* @param array $data Pattern config array
|
||||
* @return bool
|
||||
*/
|
||||
public function setPattern($entityType, $idShop, array $data);
|
||||
|
||||
/**
|
||||
* Get all pattern configurations for a shop
|
||||
*
|
||||
* @param int $idShop
|
||||
* @return array Keyed by entity type
|
||||
*/
|
||||
public function getAllPatterns($idShop);
|
||||
}
|
||||
133
src/Storage/SqlPatternStorage.php
Normal file
133
src/Storage/SqlPatternStorage.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* SQL-based Pattern Storage
|
||||
*
|
||||
* Stores URL patterns in a dedicated SQL table with individual columns.
|
||||
* Used by mprseorevolution via {prefix}_url_pattern table.
|
||||
*
|
||||
* @author mypresta.rocks
|
||||
* @copyright mypresta.rocks
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace MyPrestaRocks\Url\Storage;
|
||||
|
||||
class SqlPatternStorage implements PatternStorageInterface
|
||||
{
|
||||
/** @var string Table prefix (e.g., 'mprseo_') */
|
||||
protected $tablePrefix;
|
||||
|
||||
/** @var array Default patterns per entity type */
|
||||
protected $defaults;
|
||||
|
||||
/** @var array Column list for the pattern table */
|
||||
protected static $columns = [
|
||||
'enabled', 'pattern', 'suffix',
|
||||
'include_parent_categories', 'category_separator', 'max_category_depth',
|
||||
'include_cms_category',
|
||||
'lowercase', 'replace_spaces', 'remove_accents', 'remove_special_chars',
|
||||
'collision_suffix',
|
||||
'cache_enabled', 'cache_ttl',
|
||||
'redirect_on_delete', 'redirect_on_disable', 'redirect_on_url_change',
|
||||
'product_redirect_target',
|
||||
'disable_attribute_urls', 'attribute_url_style',
|
||||
];
|
||||
|
||||
/**
|
||||
* @param string $tablePrefix Table prefix (e.g., 'mprseo_')
|
||||
* @param array $defaults Default pattern configs keyed by entity type
|
||||
*/
|
||||
public function __construct($tablePrefix, array $defaults = [])
|
||||
{
|
||||
$this->tablePrefix = $tablePrefix;
|
||||
$this->defaults = $defaults;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getPattern($entityType, $idShop)
|
||||
{
|
||||
$row = \Db::getInstance()->getRow('
|
||||
SELECT * FROM `' . _DB_PREFIX_ . pSQL($this->tablePrefix) . 'url_pattern`
|
||||
WHERE entity_type = "' . pSQL($entityType) . '"
|
||||
AND id_shop = ' . (int) $idShop
|
||||
);
|
||||
|
||||
if ($row) {
|
||||
// Convert DB row to config array (same keys as ConfigPatternStorage)
|
||||
$pattern = [];
|
||||
foreach (self::$columns as $col) {
|
||||
if (array_key_exists($col, $row)) {
|
||||
$pattern[$col] = $row[$col];
|
||||
}
|
||||
}
|
||||
|
||||
return $pattern;
|
||||
}
|
||||
|
||||
return $this->defaults[$entityType] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setPattern($entityType, $idShop, array $data)
|
||||
{
|
||||
$existing = \Db::getInstance()->getRow('
|
||||
SELECT id_pattern FROM `' . _DB_PREFIX_ . pSQL($this->tablePrefix) . 'url_pattern`
|
||||
WHERE entity_type = "' . pSQL($entityType) . '"
|
||||
AND id_shop = ' . (int) $idShop
|
||||
);
|
||||
|
||||
$dbData = [];
|
||||
foreach (self::$columns as $col) {
|
||||
if (array_key_exists($col, $data)) {
|
||||
$dbData[$col] = pSQL($data[$col]);
|
||||
}
|
||||
}
|
||||
$dbData['date_upd'] = date('Y-m-d H:i:s');
|
||||
|
||||
if ($existing) {
|
||||
return \Db::getInstance()->update(
|
||||
pSQL($this->tablePrefix) . 'url_pattern',
|
||||
$dbData,
|
||||
'id_pattern = ' . (int) $existing['id_pattern']
|
||||
);
|
||||
}
|
||||
|
||||
$dbData['entity_type'] = pSQL($entityType);
|
||||
$dbData['id_shop'] = (int) $idShop;
|
||||
$dbData['date_add'] = date('Y-m-d H:i:s');
|
||||
|
||||
return \Db::getInstance()->insert(
|
||||
pSQL($this->tablePrefix) . 'url_pattern',
|
||||
$dbData
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getAllPatterns($idShop)
|
||||
{
|
||||
$rows = \Db::getInstance()->executeS('
|
||||
SELECT * FROM `' . _DB_PREFIX_ . pSQL($this->tablePrefix) . 'url_pattern`
|
||||
WHERE id_shop = ' . (int) $idShop
|
||||
);
|
||||
|
||||
$patterns = [];
|
||||
foreach ($rows ?: [] as $row) {
|
||||
$pattern = [];
|
||||
foreach (self::$columns as $col) {
|
||||
if (array_key_exists($col, $row)) {
|
||||
$pattern[$col] = $row[$col];
|
||||
}
|
||||
}
|
||||
$patterns[$row['entity_type']] = $pattern;
|
||||
}
|
||||
|
||||
return $patterns;
|
||||
}
|
||||
}
|
||||
2005
src/UrlPatternManager.php
Normal file
2005
src/UrlPatternManager.php
Normal file
File diff suppressed because it is too large
Load Diff
961
src/UrlRouter.php
Normal file
961
src/UrlRouter.php
Normal file
@@ -0,0 +1,961 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* URL Router
|
||||
*
|
||||
* Handles incoming URL resolution - parses custom URLs and routes to the correct entity.
|
||||
* Counterpart to UrlPatternManager (which generates URLs).
|
||||
* Table prefix and module name are configurable per module.
|
||||
*
|
||||
* @author mypresta.rocks
|
||||
* @copyright mypresta.rocks
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace MyPrestaRocks\Url;
|
||||
|
||||
class UrlRouter
|
||||
{
|
||||
/** @var UrlPatternManager */
|
||||
protected $patternManager;
|
||||
|
||||
/** @var string Table prefix for URL tables */
|
||||
protected $tablePrefix;
|
||||
|
||||
/** @var string Module name for route generation */
|
||||
protected $moduleName;
|
||||
|
||||
/** @var int */
|
||||
protected $idShop;
|
||||
|
||||
/** @var int */
|
||||
protected $idLang;
|
||||
|
||||
/** @var array Cached route patterns */
|
||||
protected static $routePatterns = [];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param UrlPatternManager $patternManager
|
||||
* @param string $tablePrefix Table prefix (e.g., 'mprfurl_' or 'mprseo_')
|
||||
* @param string $moduleName Module name for route params (e.g., 'mprfriendlyurl')
|
||||
* @param int|null $idShop
|
||||
* @param int|null $idLang
|
||||
*/
|
||||
public function __construct(UrlPatternManager $patternManager, $tablePrefix, $moduleName, $idShop = null, $idLang = null)
|
||||
{
|
||||
$this->patternManager = $patternManager;
|
||||
$this->tablePrefix = $tablePrefix;
|
||||
$this->moduleName = $moduleName;
|
||||
$this->idShop = $idShop ?: (int) \Context::getContext()->shop->id;
|
||||
$this->idLang = $idLang ?: (int) \Context::getContext()->language->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the pattern manager instance
|
||||
*
|
||||
* @return UrlPatternManager
|
||||
*/
|
||||
public function getPatternManager()
|
||||
{
|
||||
return $this->patternManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a URL to an entity
|
||||
*
|
||||
* @param string $url The request URI (without domain, with or without language prefix)
|
||||
* @return array|null [entity_type, id_entity, id_lang, id_product_attribute, controller] or redirect info
|
||||
*/
|
||||
public function resolve($url)
|
||||
{
|
||||
$url = $this->normalizeUrl($url);
|
||||
|
||||
if (empty($url)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 1. Check URL cache for exact match (fastest)
|
||||
$cached = $this->lookupCachedUrl($url);
|
||||
if ($cached) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
// 2. Check custom overrides
|
||||
$override = $this->lookupOverride($url);
|
||||
if ($override) {
|
||||
return $override;
|
||||
}
|
||||
|
||||
// 3. Check URL history for redirects
|
||||
$redirect = $this->patternManager->findRedirect($url, $this->idLang);
|
||||
if ($redirect) {
|
||||
return [
|
||||
'redirect' => true,
|
||||
'redirect_url' => $redirect['new_url'],
|
||||
'redirect_type' => $redirect['redirect_type'],
|
||||
];
|
||||
}
|
||||
|
||||
// 4. Try pattern matching (slowest, but handles dynamic URLs)
|
||||
$matched = $this->matchPattern($url);
|
||||
if ($matched) {
|
||||
return $matched;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize URL for matching
|
||||
*
|
||||
* @param string $url
|
||||
* @return string
|
||||
*/
|
||||
protected function normalizeUrl($url)
|
||||
{
|
||||
// Remove query string
|
||||
if (($pos = strpos($url, '?')) !== false) {
|
||||
$url = substr($url, 0, $pos);
|
||||
}
|
||||
|
||||
$url = ltrim($url, '/');
|
||||
|
||||
// Remove language prefix if present
|
||||
$languages = \Language::getLanguages(true, $this->idShop);
|
||||
foreach ($languages as $lang) {
|
||||
$prefix = $lang['iso_code'] . '/';
|
||||
if (strpos($url, $prefix) === 0) {
|
||||
$url = substr($url, strlen($prefix));
|
||||
$this->idLang = (int) $lang['id_lang'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$url = rtrim($url, '/');
|
||||
|
||||
// Remove common suffixes for matching
|
||||
$suffixes = ['.html', '.htm', '.php'];
|
||||
foreach ($suffixes as $suffix) {
|
||||
if (substr($url, -strlen($suffix)) === $suffix) {
|
||||
$url = substr($url, 0, -strlen($suffix));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return strtolower($url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup URL in cache table
|
||||
*
|
||||
* @param string $url
|
||||
* @return array|null
|
||||
*/
|
||||
protected function lookupCachedUrl($url)
|
||||
{
|
||||
$urlVariants = [$url, $url . '.html', $url . '/'];
|
||||
|
||||
foreach ($urlVariants as $variant) {
|
||||
$hash = md5($variant);
|
||||
|
||||
$result = \Db::getInstance()->getRow('
|
||||
SELECT entity_type, id_entity, id_lang
|
||||
FROM `' . _DB_PREFIX_ . $this->tablePrefix . 'url_cache`
|
||||
WHERE url_hash = "' . pSQL($hash) . '"
|
||||
AND id_shop = ' . (int) $this->idShop . '
|
||||
');
|
||||
|
||||
if ($result) {
|
||||
return [
|
||||
'entity_type' => $result['entity_type'],
|
||||
'id_entity' => (int) $result['id_entity'],
|
||||
'id_lang' => (int) $result['id_lang'],
|
||||
'controller' => $this->getControllerForEntityType($result['entity_type']),
|
||||
'id_product_attribute' => 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup URL in overrides table
|
||||
*
|
||||
* @param string $url
|
||||
* @return array|null
|
||||
*/
|
||||
protected function lookupOverride($url)
|
||||
{
|
||||
$urlVariants = [$url, $url . '.html', $url . '/'];
|
||||
|
||||
foreach ($urlVariants as $variant) {
|
||||
$hash = md5($variant);
|
||||
|
||||
$result = \Db::getInstance()->getRow('
|
||||
SELECT entity_type, id_entity, id_lang,
|
||||
target_entity_type, target_id_entity, redirect_type, target_custom_url
|
||||
FROM `' . _DB_PREFIX_ . $this->tablePrefix . 'url_override`
|
||||
WHERE custom_url_hash = "' . pSQL($hash) . '"
|
||||
AND id_shop = ' . (int) $this->idShop . '
|
||||
');
|
||||
|
||||
if ($result) {
|
||||
// Redirect to target entity
|
||||
if (!empty($result['target_entity_type']) && !empty($result['target_id_entity'])) {
|
||||
$targetUrl = $this->resolveEntityUrl(
|
||||
$result['target_entity_type'],
|
||||
(int) $result['target_id_entity'],
|
||||
(int) $result['id_lang']
|
||||
);
|
||||
if ($targetUrl) {
|
||||
return [
|
||||
'redirect' => true,
|
||||
'redirect_url' => $targetUrl,
|
||||
'redirect_type' => $result['redirect_type'] ?: '301',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect to custom URL
|
||||
if (!empty($result['target_custom_url'])) {
|
||||
return [
|
||||
'redirect' => true,
|
||||
'redirect_url' => $result['target_custom_url'],
|
||||
'redirect_type' => $result['redirect_type'] ?: '301',
|
||||
];
|
||||
}
|
||||
|
||||
// Legacy override: serve entity at custom URL
|
||||
return [
|
||||
'entity_type' => $result['entity_type'],
|
||||
'id_entity' => (int) $result['id_entity'],
|
||||
'id_lang' => (int) $result['id_lang'],
|
||||
'controller' => $this->getControllerForEntityType($result['entity_type']),
|
||||
'id_product_attribute' => 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a target entity to its front-office URL (relative path)
|
||||
*
|
||||
* @param string $entityType
|
||||
* @param int $idEntity
|
||||
* @param int $idLang
|
||||
* @return string|null
|
||||
*/
|
||||
protected function resolveEntityUrl($entityType, $idEntity, $idLang)
|
||||
{
|
||||
$context = \Context::getContext();
|
||||
$link = $context->link;
|
||||
|
||||
try {
|
||||
switch ($entityType) {
|
||||
case 'product':
|
||||
$url = $link->getProductLink($idEntity, null, null, null, $idLang);
|
||||
break;
|
||||
case 'category':
|
||||
$url = $link->getCategoryLink($idEntity, null, $idLang);
|
||||
break;
|
||||
case 'cms':
|
||||
$cmsObj = new \CMS($idEntity, $idLang);
|
||||
$url = $link->getCMSLink($cmsObj, null, null, $idLang);
|
||||
break;
|
||||
case 'manufacturer':
|
||||
$url = $link->getManufacturerLink($idEntity, null, $idLang);
|
||||
break;
|
||||
case 'supplier':
|
||||
$url = $link->getSupplierLink($idEntity, null, $idLang);
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
$baseUrl = $context->shop->getBaseURL(true);
|
||||
if (strpos($url, $baseUrl) === 0) {
|
||||
return ltrim(substr($url, strlen($baseUrl)), '/');
|
||||
}
|
||||
|
||||
return ltrim(parse_url($url, PHP_URL_PATH), '/');
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to match URL against patterns
|
||||
*
|
||||
* @param string $url
|
||||
* @return array|null
|
||||
*/
|
||||
protected function matchPattern($url)
|
||||
{
|
||||
$entityTypes = UrlPatternManager::getSupportedEntityTypes();
|
||||
|
||||
foreach ($entityTypes as $entityType) {
|
||||
if (!$this->patternManager->isEnabled($entityType)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$pattern = $this->patternManager->getPattern($entityType);
|
||||
$regex = $this->patternToRegex($pattern['pattern'], $entityType);
|
||||
|
||||
if (preg_match($regex, $url, $matches)) {
|
||||
$entity = $this->findEntityByPatternMatch($entityType, $matches);
|
||||
if ($entity) {
|
||||
return $entity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert pattern to regex for matching
|
||||
*
|
||||
* @param string $pattern
|
||||
* @param string $entityType
|
||||
* @return string
|
||||
*/
|
||||
protected function patternToRegex($pattern, $entityType)
|
||||
{
|
||||
$cacheKey = $this->tablePrefix . $entityType . '_' . md5($pattern);
|
||||
|
||||
if (isset(self::$routePatterns[$cacheKey])) {
|
||||
return self::$routePatterns[$cacheKey];
|
||||
}
|
||||
|
||||
$regex = preg_quote($pattern, '#');
|
||||
|
||||
$placeholders = [
|
||||
'\\{id\\}' => '(?P<id>[0-9]+)',
|
||||
'\\{name\\}' => '(?P<name>[a-z0-9\-]+)',
|
||||
'\\{rewrite\\}' => '(?P<name>[a-z0-9\-]+)',
|
||||
'\\{reference\\}' => '(?P<reference>[a-z0-9\-]+)',
|
||||
'\\{ean13\\}' => '(?P<ean13>[0-9]{13})?',
|
||||
'\\{upc\\}' => '(?P<upc>[0-9]{12})?',
|
||||
'\\{category\\}' => '(?P<category>[a-z0-9\-]+)',
|
||||
'\\{categories\\}' => '(?P<categories>[a-z0-9\-\/]+)',
|
||||
'\\{brand\\}' => '(?P<brand>[a-z0-9\-]+)',
|
||||
'\\{supplier\\}' => '(?P<supplier>[a-z0-9\-]+)',
|
||||
'\\{parent\\}' => '(?P<parent>[a-z0-9\-]+)',
|
||||
'\\{parents\\}' => '(?P<parents>[a-z0-9\-\/]+)',
|
||||
'\\{id_product_attribute\\}' => '(?P<id_product_attribute>[0-9]+)?',
|
||||
'\\{attributes\\}' => '(?P<attributes>[a-z0-9\-]+)?',
|
||||
'\\{tags\\}' => '(?P<tags>[a-z0-9\-]+)?',
|
||||
'\\{meta_keywords\\}' => '(?P<meta_keywords>[a-z0-9\-]+)?',
|
||||
'\\{meta_title\\}' => '(?P<meta_title>[a-z0-9\-]+)?',
|
||||
];
|
||||
|
||||
$regex = preg_replace('#\\\\\\{attribute\\:[^}]+\\\\\\}#', '(?P<attribute_value>[a-z0-9\-]+)?', $regex);
|
||||
|
||||
foreach ($placeholders as $placeholder => $replacement) {
|
||||
$regex = preg_replace('#' . $placeholder . '#', $replacement, $regex);
|
||||
}
|
||||
|
||||
$regex = preg_replace('#\\\\\\{[^}]+\\\\\\}#', '[a-z0-9\-]*', $regex);
|
||||
|
||||
$regex = '#^' . $regex . '$#i';
|
||||
|
||||
self::$routePatterns[$cacheKey] = $regex;
|
||||
|
||||
return $regex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find entity by pattern match data
|
||||
*
|
||||
* @param string $entityType
|
||||
* @param array $matches
|
||||
* @return array|null
|
||||
*/
|
||||
protected function findEntityByPatternMatch($entityType, array $matches)
|
||||
{
|
||||
switch ($entityType) {
|
||||
case 'product':
|
||||
return $this->findProductByMatch($matches);
|
||||
case 'category':
|
||||
return $this->findCategoryByMatch($matches);
|
||||
case 'cms':
|
||||
return $this->findCmsByMatch($matches);
|
||||
case 'cms_category':
|
||||
return $this->findCmsCategoryByMatch($matches);
|
||||
case 'manufacturer':
|
||||
return $this->findManufacturerByMatch($matches);
|
||||
case 'supplier':
|
||||
return $this->findSupplierByMatch($matches);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find product by pattern match
|
||||
*
|
||||
* @param array $matches
|
||||
* @return array|null
|
||||
*/
|
||||
protected function findProductByMatch(array $matches)
|
||||
{
|
||||
if (!empty($matches['id'])) {
|
||||
$idProduct = (int) $matches['id'];
|
||||
if ($this->productExists($idProduct)) {
|
||||
return [
|
||||
'entity_type' => 'product',
|
||||
'id_entity' => $idProduct,
|
||||
'id_lang' => $this->idLang,
|
||||
'controller' => 'product',
|
||||
'id_product_attribute' => !empty($matches['id_product_attribute']) ? (int) $matches['id_product_attribute'] : 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($matches['name'])) {
|
||||
$sql = '
|
||||
SELECT p.id_product
|
||||
FROM `' . _DB_PREFIX_ . 'product` p
|
||||
INNER JOIN `' . _DB_PREFIX_ . 'product_shop` ps ON ps.id_product = p.id_product AND ps.id_shop = ' . (int) $this->idShop . '
|
||||
INNER JOIN `' . _DB_PREFIX_ . 'product_lang` pl ON pl.id_product = p.id_product AND pl.id_lang = ' . (int) $this->idLang . ' AND pl.id_shop = ' . (int) $this->idShop . '
|
||||
WHERE pl.link_rewrite = "' . pSQL($matches['name']) . '"
|
||||
AND ps.active = 1
|
||||
';
|
||||
|
||||
if (!empty($matches['category'])) {
|
||||
$sql .= ' AND EXISTS (
|
||||
SELECT 1 FROM `' . _DB_PREFIX_ . 'category_lang` cl
|
||||
WHERE cl.id_category = p.id_category_default
|
||||
AND cl.id_lang = ' . (int) $this->idLang . '
|
||||
AND cl.link_rewrite = "' . pSQL($matches['category']) . '"
|
||||
)';
|
||||
}
|
||||
|
||||
if (!empty($matches['brand'])) {
|
||||
$sql .= ' AND EXISTS (
|
||||
SELECT 1 FROM `' . _DB_PREFIX_ . 'manufacturer` m
|
||||
WHERE m.id_manufacturer = p.id_manufacturer
|
||||
AND LOWER(REPLACE(m.name, " ", "-")) = "' . pSQL($matches['brand']) . '"
|
||||
)';
|
||||
}
|
||||
|
||||
$sql .= ' LIMIT 1';
|
||||
|
||||
$idProduct = (int) \Db::getInstance()->getValue($sql);
|
||||
|
||||
if ($idProduct) {
|
||||
return [
|
||||
'entity_type' => 'product',
|
||||
'id_entity' => $idProduct,
|
||||
'id_lang' => $this->idLang,
|
||||
'controller' => 'product',
|
||||
'id_product_attribute' => !empty($matches['id_product_attribute']) ? (int) $matches['id_product_attribute'] : 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($matches['ean13'])) {
|
||||
$idProduct = (int) \Db::getInstance()->getValue('
|
||||
SELECT p.id_product
|
||||
FROM `' . _DB_PREFIX_ . 'product` p
|
||||
INNER JOIN `' . _DB_PREFIX_ . 'product_shop` ps ON ps.id_product = p.id_product AND ps.id_shop = ' . (int) $this->idShop . '
|
||||
WHERE p.ean13 = "' . pSQL($matches['ean13']) . '"
|
||||
AND ps.active = 1
|
||||
LIMIT 1
|
||||
');
|
||||
|
||||
if ($idProduct) {
|
||||
return [
|
||||
'entity_type' => 'product',
|
||||
'id_entity' => $idProduct,
|
||||
'id_lang' => $this->idLang,
|
||||
'controller' => 'product',
|
||||
'id_product_attribute' => 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($matches['reference'])) {
|
||||
$idProduct = (int) \Db::getInstance()->getValue('
|
||||
SELECT p.id_product
|
||||
FROM `' . _DB_PREFIX_ . 'product` p
|
||||
INNER JOIN `' . _DB_PREFIX_ . 'product_shop` ps ON ps.id_product = p.id_product AND ps.id_shop = ' . (int) $this->idShop . '
|
||||
WHERE LOWER(p.reference) = "' . pSQL(strtolower($matches['reference'])) . '"
|
||||
AND ps.active = 1
|
||||
LIMIT 1
|
||||
');
|
||||
|
||||
if ($idProduct) {
|
||||
return [
|
||||
'entity_type' => 'product',
|
||||
'id_entity' => $idProduct,
|
||||
'id_lang' => $this->idLang,
|
||||
'controller' => 'product',
|
||||
'id_product_attribute' => 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find category by pattern match
|
||||
*
|
||||
* @param array $matches
|
||||
* @return array|null
|
||||
*/
|
||||
protected function findCategoryByMatch(array $matches)
|
||||
{
|
||||
if (!empty($matches['id'])) {
|
||||
$idCategory = (int) $matches['id'];
|
||||
if ($this->categoryExists($idCategory)) {
|
||||
return [
|
||||
'entity_type' => 'category',
|
||||
'id_entity' => $idCategory,
|
||||
'id_lang' => $this->idLang,
|
||||
'controller' => 'category',
|
||||
'id_product_attribute' => 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($matches['name'])) {
|
||||
$sql = '
|
||||
SELECT c.id_category
|
||||
FROM `' . _DB_PREFIX_ . 'category` c
|
||||
INNER JOIN `' . _DB_PREFIX_ . 'category_shop` cs ON cs.id_category = c.id_category AND cs.id_shop = ' . (int) $this->idShop . '
|
||||
INNER JOIN `' . _DB_PREFIX_ . 'category_lang` cl ON cl.id_category = c.id_category AND cl.id_lang = ' . (int) $this->idLang . ' AND cl.id_shop = ' . (int) $this->idShop . '
|
||||
WHERE cl.link_rewrite = "' . pSQL($matches['name']) . '"
|
||||
AND c.active = 1
|
||||
';
|
||||
|
||||
if (!empty($matches['parent'])) {
|
||||
$sql .= ' AND EXISTS (
|
||||
SELECT 1 FROM `' . _DB_PREFIX_ . 'category_lang` pcl
|
||||
WHERE pcl.id_category = c.id_parent
|
||||
AND pcl.id_lang = ' . (int) $this->idLang . '
|
||||
AND pcl.link_rewrite = "' . pSQL($matches['parent']) . '"
|
||||
)';
|
||||
}
|
||||
|
||||
$sql .= ' LIMIT 1';
|
||||
|
||||
$idCategory = (int) \Db::getInstance()->getValue($sql);
|
||||
|
||||
if ($idCategory) {
|
||||
return [
|
||||
'entity_type' => 'category',
|
||||
'id_entity' => $idCategory,
|
||||
'id_lang' => $this->idLang,
|
||||
'controller' => 'category',
|
||||
'id_product_attribute' => 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find CMS page by pattern match
|
||||
*
|
||||
* @param array $matches
|
||||
* @return array|null
|
||||
*/
|
||||
protected function findCmsByMatch(array $matches)
|
||||
{
|
||||
if (!empty($matches['id'])) {
|
||||
$idCms = (int) $matches['id'];
|
||||
if ($this->cmsExists($idCms)) {
|
||||
return [
|
||||
'entity_type' => 'cms',
|
||||
'id_entity' => $idCms,
|
||||
'id_lang' => $this->idLang,
|
||||
'controller' => 'cms',
|
||||
'id_product_attribute' => 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($matches['name'])) {
|
||||
$idCms = (int) \Db::getInstance()->getValue('
|
||||
SELECT c.id_cms
|
||||
FROM `' . _DB_PREFIX_ . 'cms` c
|
||||
INNER JOIN `' . _DB_PREFIX_ . 'cms_shop` cs ON cs.id_cms = c.id_cms AND cs.id_shop = ' . (int) $this->idShop . '
|
||||
INNER JOIN `' . _DB_PREFIX_ . 'cms_lang` cl ON cl.id_cms = c.id_cms AND cl.id_lang = ' . (int) $this->idLang . ' AND cl.id_shop = ' . (int) $this->idShop . '
|
||||
WHERE cl.link_rewrite = "' . pSQL($matches['name']) . '"
|
||||
AND c.active = 1
|
||||
LIMIT 1
|
||||
');
|
||||
|
||||
if ($idCms) {
|
||||
return [
|
||||
'entity_type' => 'cms',
|
||||
'id_entity' => $idCms,
|
||||
'id_lang' => $this->idLang,
|
||||
'controller' => 'cms',
|
||||
'id_product_attribute' => 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find CMS category by pattern match
|
||||
*
|
||||
* @param array $matches
|
||||
* @return array|null
|
||||
*/
|
||||
protected function findCmsCategoryByMatch(array $matches)
|
||||
{
|
||||
if (!empty($matches['id'])) {
|
||||
return [
|
||||
'entity_type' => 'cms_category',
|
||||
'id_entity' => (int) $matches['id'],
|
||||
'id_lang' => $this->idLang,
|
||||
'controller' => 'cms',
|
||||
'id_product_attribute' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
if (!empty($matches['name'])) {
|
||||
$idCmsCategory = (int) \Db::getInstance()->getValue('
|
||||
SELECT cc.id_cms_category
|
||||
FROM `' . _DB_PREFIX_ . 'cms_category` cc
|
||||
INNER JOIN `' . _DB_PREFIX_ . 'cms_category_shop` ccs ON ccs.id_cms_category = cc.id_cms_category AND ccs.id_shop = ' . (int) $this->idShop . '
|
||||
INNER JOIN `' . _DB_PREFIX_ . 'cms_category_lang` ccl ON ccl.id_cms_category = cc.id_cms_category AND ccl.id_lang = ' . (int) $this->idLang . ' AND ccl.id_shop = ' . (int) $this->idShop . '
|
||||
WHERE ccl.link_rewrite = "' . pSQL($matches['name']) . '"
|
||||
AND cc.active = 1
|
||||
LIMIT 1
|
||||
');
|
||||
|
||||
if ($idCmsCategory) {
|
||||
return [
|
||||
'entity_type' => 'cms_category',
|
||||
'id_entity' => $idCmsCategory,
|
||||
'id_lang' => $this->idLang,
|
||||
'controller' => 'cms',
|
||||
'id_product_attribute' => 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find manufacturer by pattern match
|
||||
*
|
||||
* @param array $matches
|
||||
* @return array|null
|
||||
*/
|
||||
protected function findManufacturerByMatch(array $matches)
|
||||
{
|
||||
if (!empty($matches['id'])) {
|
||||
return [
|
||||
'entity_type' => 'manufacturer',
|
||||
'id_entity' => (int) $matches['id'],
|
||||
'id_lang' => $this->idLang,
|
||||
'controller' => 'manufacturer',
|
||||
'id_product_attribute' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
if (!empty($matches['name'])) {
|
||||
$idManufacturer = (int) \Db::getInstance()->getValue('
|
||||
SELECT m.id_manufacturer
|
||||
FROM `' . _DB_PREFIX_ . 'manufacturer` m
|
||||
INNER JOIN `' . _DB_PREFIX_ . 'manufacturer_shop` ms ON ms.id_manufacturer = m.id_manufacturer AND ms.id_shop = ' . (int) $this->idShop . '
|
||||
WHERE LOWER(REPLACE(REPLACE(m.name, " ", "-"), "\'", "")) = "' . pSQL($matches['name']) . '"
|
||||
AND m.active = 1
|
||||
LIMIT 1
|
||||
');
|
||||
|
||||
if ($idManufacturer) {
|
||||
return [
|
||||
'entity_type' => 'manufacturer',
|
||||
'id_entity' => $idManufacturer,
|
||||
'id_lang' => $this->idLang,
|
||||
'controller' => 'manufacturer',
|
||||
'id_product_attribute' => 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find supplier by pattern match
|
||||
*
|
||||
* @param array $matches
|
||||
* @return array|null
|
||||
*/
|
||||
protected function findSupplierByMatch(array $matches)
|
||||
{
|
||||
if (!empty($matches['id'])) {
|
||||
return [
|
||||
'entity_type' => 'supplier',
|
||||
'id_entity' => (int) $matches['id'],
|
||||
'id_lang' => $this->idLang,
|
||||
'controller' => 'supplier',
|
||||
'id_product_attribute' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
if (!empty($matches['name'])) {
|
||||
$idSupplier = (int) \Db::getInstance()->getValue('
|
||||
SELECT s.id_supplier
|
||||
FROM `' . _DB_PREFIX_ . 'supplier` s
|
||||
INNER JOIN `' . _DB_PREFIX_ . 'supplier_shop` ss ON ss.id_supplier = s.id_supplier AND ss.id_shop = ' . (int) $this->idShop . '
|
||||
WHERE LOWER(REPLACE(REPLACE(s.name, " ", "-"), "\'", "")) = "' . pSQL($matches['name']) . '"
|
||||
AND s.active = 1
|
||||
LIMIT 1
|
||||
');
|
||||
|
||||
if ($idSupplier) {
|
||||
return [
|
||||
'entity_type' => 'supplier',
|
||||
'id_entity' => $idSupplier,
|
||||
'id_lang' => $this->idLang,
|
||||
'controller' => 'supplier',
|
||||
'id_product_attribute' => 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if product exists
|
||||
*
|
||||
* @param int $idProduct
|
||||
* @return bool
|
||||
*/
|
||||
protected function productExists($idProduct)
|
||||
{
|
||||
return (bool) \Db::getInstance()->getValue(
|
||||
'SELECT 1 FROM `' . _DB_PREFIX_ . 'product_shop`
|
||||
WHERE id_product = ' . (int) $idProduct . '
|
||||
AND id_shop = ' . (int) $this->idShop . '
|
||||
AND active = 1'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if category exists
|
||||
*
|
||||
* @param int $idCategory
|
||||
* @return bool
|
||||
*/
|
||||
protected function categoryExists($idCategory)
|
||||
{
|
||||
return (bool) \Db::getInstance()->getValue(
|
||||
'SELECT 1 FROM `' . _DB_PREFIX_ . 'category_shop`
|
||||
WHERE id_category = ' . (int) $idCategory . '
|
||||
AND id_shop = ' . (int) $this->idShop
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if CMS page exists
|
||||
*
|
||||
* @param int $idCms
|
||||
* @return bool
|
||||
*/
|
||||
protected function cmsExists($idCms)
|
||||
{
|
||||
return (bool) \Db::getInstance()->getValue(
|
||||
'SELECT 1 FROM `' . _DB_PREFIX_ . 'cms_shop`
|
||||
WHERE id_cms = ' . (int) $idCms . '
|
||||
AND id_shop = ' . (int) $this->idShop
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get controller name for entity type
|
||||
*
|
||||
* @param string $entityType
|
||||
* @return string
|
||||
*/
|
||||
public function getControllerForEntityType($entityType)
|
||||
{
|
||||
$controllers = [
|
||||
'product' => 'product',
|
||||
'category' => 'category',
|
||||
'cms' => 'cms',
|
||||
'cms_category' => 'cms',
|
||||
'manufacturer' => 'manufacturer',
|
||||
'supplier' => 'supplier',
|
||||
];
|
||||
|
||||
return $controllers[$entityType] ?? 'index';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate PrestaShop-compatible route definitions for moduleRoutes hook
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function generateModuleRoutes()
|
||||
{
|
||||
$routes = [];
|
||||
$entityTypes = UrlPatternManager::getSupportedEntityTypes();
|
||||
$routePrefix = str_replace('_', '', $this->tablePrefix);
|
||||
|
||||
foreach ($entityTypes as $entityType) {
|
||||
if (!$this->patternManager->isEnabled($entityType)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$pattern = $this->patternManager->getPattern($entityType);
|
||||
$routeRule = $this->patternToRouteRule($pattern['pattern'], $pattern);
|
||||
|
||||
$controller = $this->getControllerForEntityType($entityType);
|
||||
|
||||
$languages = \Language::getLanguages(true, $this->idShop);
|
||||
foreach ($languages as $lang) {
|
||||
$routeKey = $routePrefix . '_' . $entityType . '_' . $lang['iso_code'];
|
||||
|
||||
$routes[$routeKey] = [
|
||||
'controller' => $controller,
|
||||
'rule' => $lang['iso_code'] . '/' . $routeRule,
|
||||
'keywords' => $this->getRouteKeywords($entityType),
|
||||
'params' => [
|
||||
'fc' => 'module',
|
||||
'module' => $this->moduleName,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $routes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert pattern to PrestaShop route rule
|
||||
*
|
||||
* @param string $pattern
|
||||
* @param array $config
|
||||
* @return string
|
||||
*/
|
||||
protected function patternToRouteRule($pattern, array $config)
|
||||
{
|
||||
$conversions = [
|
||||
'{id}' => '{id}',
|
||||
'{name}' => '{rewrite}',
|
||||
'{rewrite}' => '{rewrite}',
|
||||
'{category}' => '{category}',
|
||||
'{categories}' => '{categories}',
|
||||
'{brand}' => '{manufacturer}',
|
||||
'{supplier}' => '{supplier}',
|
||||
'{ean13}' => '{ean13}',
|
||||
'{reference}' => '{reference}',
|
||||
'{parent}' => '{parent}',
|
||||
'{parents}' => '{parents}',
|
||||
'{id_product_attribute}' => '{id_product_attribute}',
|
||||
'{attributes}' => '{attributes}',
|
||||
];
|
||||
|
||||
$rule = $pattern;
|
||||
foreach ($conversions as $from => $to) {
|
||||
$rule = str_replace($from, $to, $rule);
|
||||
}
|
||||
|
||||
$rule = preg_replace('/\{attribute:[^}]+\}/', '', $rule);
|
||||
$rule = preg_replace('/\{[^}]+\}/', '', $rule);
|
||||
|
||||
if (!empty($config['suffix'])) {
|
||||
$rule .= $config['suffix'];
|
||||
}
|
||||
|
||||
$rule = preg_replace('#/+#', '/', $rule);
|
||||
$rule = trim($rule, '/');
|
||||
|
||||
return $rule;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get route keywords for entity type
|
||||
*
|
||||
* @param string $entityType
|
||||
* @return array
|
||||
*/
|
||||
public function getRouteKeywords($entityType)
|
||||
{
|
||||
$baseKeywords = [
|
||||
'id' => ['regexp' => '[0-9]+', 'param' => 'id_' . $entityType],
|
||||
'rewrite' => ['regexp' => '[_a-zA-Z0-9\pL\pS-]*', 'param' => 'rewrite'],
|
||||
];
|
||||
|
||||
switch ($entityType) {
|
||||
case 'product':
|
||||
return array_merge($baseKeywords, [
|
||||
'id' => ['regexp' => '[0-9]+', 'param' => 'id_product'],
|
||||
'category' => ['regexp' => '[_a-zA-Z0-9\pL\pS-]*'],
|
||||
'categories' => ['regexp' => '[_a-zA-Z0-9\pL\pS\/-]*'],
|
||||
'ean13' => ['regexp' => '[0-9]*'],
|
||||
'reference' => ['regexp' => '[_a-zA-Z0-9\pL\pS-]*'],
|
||||
'manufacturer' => ['regexp' => '[_a-zA-Z0-9\pL\pS-]*'],
|
||||
'supplier' => ['regexp' => '[_a-zA-Z0-9\pL\pS-]*'],
|
||||
'id_product_attribute' => ['regexp' => '[0-9]*', 'param' => 'id_product_attribute'],
|
||||
'attributes' => ['regexp' => '[_a-zA-Z0-9\pL\pS-]*'],
|
||||
'price' => ['regexp' => '[0-9\-\.]*'],
|
||||
'meta_title' => ['regexp' => '[_a-zA-Z0-9\pL\pS-]*'],
|
||||
'meta_keywords' => ['regexp' => '[_a-zA-Z0-9\pL\pS-]*'],
|
||||
'tags' => ['regexp' => '[_a-zA-Z0-9\pL\pS-]*'],
|
||||
]);
|
||||
|
||||
case 'category':
|
||||
return array_merge($baseKeywords, [
|
||||
'id' => ['regexp' => '[0-9]+', 'param' => 'id_category'],
|
||||
'parent' => ['regexp' => '[_a-zA-Z0-9\pL\pS-]*'],
|
||||
'parents' => ['regexp' => '[_a-zA-Z0-9\pL\pS\/-]*'],
|
||||
'meta_title' => ['regexp' => '[_a-zA-Z0-9\pL\pS-]*'],
|
||||
'meta_keywords' => ['regexp' => '[_a-zA-Z0-9\pL\pS-]*'],
|
||||
]);
|
||||
|
||||
case 'cms':
|
||||
return array_merge($baseKeywords, [
|
||||
'id' => ['regexp' => '[0-9]+', 'param' => 'id_cms'],
|
||||
'category' => ['regexp' => '[_a-zA-Z0-9\pL\pS-]*'],
|
||||
'categories' => ['regexp' => '[_a-zA-Z0-9\pL\pS\/-]*'],
|
||||
'meta_title' => ['regexp' => '[_a-zA-Z0-9\pL\pS-]*'],
|
||||
'meta_keywords' => ['regexp' => '[_a-zA-Z0-9\pL\pS-]*'],
|
||||
]);
|
||||
|
||||
case 'cms_category':
|
||||
return array_merge($baseKeywords, [
|
||||
'id' => ['regexp' => '[0-9]+', 'param' => 'id_cms_category'],
|
||||
'meta_title' => ['regexp' => '[_a-zA-Z0-9\pL\pS-]*'],
|
||||
'meta_keywords' => ['regexp' => '[_a-zA-Z0-9\pL\pS-]*'],
|
||||
]);
|
||||
|
||||
case 'manufacturer':
|
||||
return array_merge($baseKeywords, [
|
||||
'id' => ['regexp' => '[0-9]+', 'param' => 'id_manufacturer'],
|
||||
'meta_title' => ['regexp' => '[_a-zA-Z0-9\pL\pS-]*'],
|
||||
'meta_keywords' => ['regexp' => '[_a-zA-Z0-9\pL\pS-]*'],
|
||||
]);
|
||||
|
||||
case 'supplier':
|
||||
return array_merge($baseKeywords, [
|
||||
'id' => ['regexp' => '[0-9]+', 'param' => 'id_supplier'],
|
||||
'meta_title' => ['regexp' => '[_a-zA-Z0-9\pL\pS-]*'],
|
||||
'meta_keywords' => ['regexp' => '[_a-zA-Z0-9\pL\pS-]*'],
|
||||
]);
|
||||
|
||||
default:
|
||||
return $baseKeywords;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user