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:
2026-02-14 09:20:21 +00:00
commit 73ac44b3f5
8 changed files with 4652 additions and 0 deletions

961
src/UrlRouter.php Normal file
View 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;
}
}
}