La reescritura de URLs de Magento es una característica del core. Principalmente se utiliza para generar URLs amigables para buscadores (SEO friendly) en productos, categorías, páginas CMS, etc. La reescritura es posible además a través del archivo etc/config.xml cuando creamos un módulo de Magento. Vamos a ver cómo funciona en detalle.
La tabla core_url_rewrite
Magento guarda las reescrituras de URLs en la tabla «core_url_rewrite»:
- url_rewrite_id: Clave primaria
- store_id: Id de la tienda para la que se hace la reescritura
- id_path: Identificador único interno. Normalmente es "product" + id de producto, o "category" + id de categoría, etc.
- request_path: La url solicitada en el navegador
- target_path: La url que debería abrirse si la url solicitada corresponde con la anterior
- is_system: Si la reescritura de url ha sido generada por Magento o ha sido creada de forma manual
- options: Diferentes opciones de redirección, como "RP" (redirección permanente 301), "R" (redirección temporal 302), o null/empty (nulo/vacío)
- description: Para agregar cualquier descripción
- category_id: El id de la categoría si la redirección es para una categoría
- product_id: El id del producto si la redirección es para un producto
Implementación de reescrituras de URLs
Vamos a ver en detalle cómo Magento implementa estas reescrituras cuando abrimos una URL en el navegador. Cuando se abre una URL de Magento en el navegador, el control principal pasa a
Mage_Core_Controller_Varien_Front::dispatch()
donde está este código:
$this->_getRequestRewriteController()->rewrite();
Esto básicamente crea un objeto de la clase 'core/url_rewrite_request' y llama a la función rewrite que hace lo siguiente:
public function rewrite()
{
if (!$this->_request->isStraight()) {
$this->_rewriteDb();
}
$this->_rewriteConfig();
return true;
}
Como podemos ver aquí, se llama a las funciones _rewriteDB()
y _rewriteConfig()
, las cuales controlan las reescrituras de base de datos o de config.xml respectivamente.
Reescrituras de URLs por base de datos
Vamos a ver la reescritura de base de datos primero:
protected function _rewriteDb()
{
if (null === $this->_rewrite->getStoreId() || false === $this->_rewrite->getStoreId()) {
$this->_rewrite->setStoreId($this->_app->getStore()->getId());
}
$requestCases = $this->_getRequestCases();
$this->_rewrite->loadByRequestPath($requestCases);
$fromStore = $this->_request->getQuery('___from_store');
if (!$this->_rewrite->getId() && $fromStore) {
$stores = $this->_app->getStores(false, true);
if (!empty($stores[$fromStore])) {
/** @var $store Mage_Core_Model_Store */
$store = $stores[$fromStore];
$fromStoreId = $store->getId();
} else {
return false;
}
$this->_rewrite->setStoreId($fromStoreId)->loadByRequestPath($requestCases);
if (!$this->_rewrite->getId()) {
return false;
}
// Load rewrite by id_path
$currentStore = $this->_app->getStore();
$this->_rewrite->setStoreId($currentStore->getId())->loadByIdPath($this->_rewrite->getIdPath());
$this->_setStoreCodeCookie($currentStore->getCode());
$targetUrl = $currentStore->getBaseUrl() . $this->_rewrite->getRequestPath();
$this->_sendRedirectHeaders($targetUrl, true);
}
if (!$this->_rewrite->getId()) {
return false;
}
$this->_request->setAlias(Mage_Core_Model_Url_Rewrite::REWRITE_REQUEST_PATH_ALIAS,
$this->_rewrite->getRequestPath());
$this->_processRedirectOptions();
return true;
}
La primera cosa que se hace aquí es crear los "Casos de solicitud" ($requestCases). Los casos de solicitud son básicamente un array que contiene las diferentes combinaciones de la solicitud por orden de prioridad. Por ejemplo, si la URL es "http://tumagento.com/pagina.html?param=1" estos serían los casos de solicitud generados:
- pagina.html?param=1
- pagina.html/?param=1
- pagina.html
- pagina.html/
De igual forma, si la URL es "http://tumagento.com/pagina.html/?param=1" estos serían los casos generados:
- pagina.html/?param=1
- pagina.html?param=1
- pagina.html/
- pagina.html
Véase la diferencia en el orden de la barra "/" basada en la URL solicitada. Aquí está el código que utiliza Magento para esto:
protected function _getRequestCases()
{
$pathInfo = $this->_request->getPathInfo();
$requestPath = trim($pathInfo, '/');
$origSlash = (substr($pathInfo, -1) == '/') ? '/' : '';
// If there were final slash - add nothing to less priority paths. And vice versa.
$altSlash = $origSlash ? '' : '/';
$requestCases = array();
// Query params in request, matching "path + query" has more priority
$queryString = $this->_getQueryString();
if ($queryString) {
$requestCases[] = $requestPath . $origSlash . '?' . $queryString;
$requestCases[] = $requestPath . $altSlash . '?' . $queryString;
}
$requestCases[] = $requestPath . $origSlash;
$requestCases[] = $requestPath . $altSlash;
return $requestCases;
}
Con cada caso de solicitud generado, Magento consulta la base de datos para ver si hay coincidencias con la función:
$this->_rewrite->loadByRequestPath($requestCases);
Esta función está definida en el recurso modelo "Mage_Core_Model_Resource_Url_Rewrite".
public function loadByRequestPath(Mage_Core_Model_Url_Rewrite $object, $path)
{
if (!is_array($path)) {
$path = array($path);
}
$pathBind = array();
foreach ($path as $key => $url) {
$pathBind['path' . $key] = $url;
}
// Form select
$adapter = $this->_getReadAdapter();
$select = $adapter->select()
->from($this->getMainTable())
->where('request_path IN (:' . implode(', :', array_flip($pathBind)) . ')')
->where('store_id IN(?)', array(Mage_Core_Model_App::ADMIN_STORE_ID, (int)$object->getStoreId()));
$items = $adapter->fetchAll($select, $pathBind);
// Go through all found records and choose one with lowest penalty - earlier path in array, concrete store
$mapPenalty = array_flip(array_values($path)); // we got mapping array(path => index), lower index - better
$currentPenalty = null;
$foundItem = null;
foreach ($items as $item) {
if (!array_key_exists($item['request_path'], $mapPenalty)) {
continue;
}
$penalty = $mapPenalty[$item['request_path']] << 1 + ($item['store_id'] ? 0 : 1);
if (!$foundItem || $currentPenalty > $penalty) {
$foundItem = $item;
$currentPenalty = $penalty;
if (!$currentPenalty) {
break; // Found best matching item with zero penalty, no reason to continue
}
}
}
// Set data and finish loading
if ($foundItem) {
$object->setData($foundItem);
}
// Finish
$this->unserializeFields($object);
$this->_afterLoad($object);
return $this;
}
Lo que este código hace es crear una query SQL como esta:
SELECT `core_url_rewrite`.* FROM `core_url_rewrite` WHERE (request_path IN (:path0, :path1)) AND (store_id IN(0, 1))
y lo devuelve en un array como este:
Array
(
[path0] =>
[path1] =>
)
Lo siguiente que hace el código es calcular la penalización mínima usando operadores bit a bit, pero lo que hace en resumen es devolver la preferencia de rutas en un array y de una tienda concreta. Así que una vez que tengamos la ruta final, Magento hace la redirección final utilizando:
$this->_processRedirectOptions();
Entre medias hay también otro código que se explica por sí solo. Así que esta es la forma de funcionar de las redirecciones de base de datos. Una cosa a tener en cuenta es cuándo Magento crea estas reescrituras de URL, esto ocurre en "Mage_Catalog_Model_Indexer_Url" usando la función "reindexAll()".
Reescrituras de URLs por config.xml
Vamos a ver ahora cómo funcionan las redirecciones de fichero etc/config.xml, aquí está el código para las reescrituras basadas en config.xml
protected function _rewriteConfig()
{
$config = $this->_config->getNode('global/rewrite');
if (!$config) {
return false;
}
foreach ($config->children() as $rewrite) {
$from = (string)$rewrite->from;
$to = (string)$rewrite->to;
if (empty($from) || empty($to)) {
continue;
}
$from = $this->_processRewriteUrl($from);
$to = $this->_processRewriteUrl($to);
$pathInfo = preg_replace($from, $to, $this->_request->getPathInfo());
if (isset($rewrite->complete)) {
$this->_request->setPathInfo($pathInfo);
} else {
$this->_request->rewritePathInfo($pathInfo);
}
}
return true;
}
protected function _processRewriteUrl($url)
{
$startPos = strpos($url, '{');
if ($startPos !== false) {
$endPos = strpos($url, '}');
$routeName = substr($url, $startPos + 1, $endPos - $startPos - 1);
$router = $this->_getRouterByRoute($routeName);
if ($router) {
$frontName = $router->getFrontNameByRoute($routeName);
$url = str_replace('{' . $routeName . '}', $frontName, $url);
}
}
return $url;
}
Este sería el xml que escribiríamos en el archivo etc/config.xml de nuestro módulo de Magento:
<global>
<rewrite>
<designer_url>
<from><![CDATA[#^/author/id/#]]></from>
<to><![CDATA[/designer/index/index/id/]]></to>
<complete>1</complete>
</designer_url>
</rewrite>
</global>
Como podemos ver este código explica bastante por sí solo cómo funciona. Una cosa a tener en cuenta es que Magento usa "preg_replace" para buscar urls coincidentes, así que podemos usar tambíen como ruta expresiones regulares sin ningún problema.