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:
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