В Joomla есть встроенная система генерации SEF ссылок и обратно. По тем или иным причинам она может Вам не подойти. К примеру, когда этот блог переезжал с самописной системы на Joomla, то потребовалось восстановить старую систему SEF логики.

Для статей, нужна была такая ссылка

http://xdan.ru/deploying-an-update-server.html

Для категорий такая

http://xdan.ru/categories/cms/joomla/doc

Реализовать подобное в Joomle можно рядом способов. Есть очень мощные и известные расширения sh404sef или joomSEF. Эти расширения достаточно удобные и гибкие, для подобной задачи. Однако достаточно часто они работают не так как требуется и исправить ситуацию порой очень сложно. И так как у нас технический блог, то мы напишем собственное расширение, которое будет заниматься подобной задачей, тем более что сделать это довольно просто. 

Для начала, напишем заготовку плагина. Для его работы потребуется создать два файла <имя плагина>.xml и <имя плагина>.php К примеру, если мы назовем наш плагин sefmaster, то у нас будет два файла sefmaster.php и sefmaster.xml

Где XML файл это манифест расширения. В нем перечисляются данные о разработчике, версии и название плагина. Минимально, плагин может выглядеть так

<?xml version="1.0" encoding="utf-8"?>
<extension type="plugin" version="3.0" group="system" method="upgrade">
	<name>plg_system_sefmaster</name>
	<author></author>
	<creationDate>January 2015</creationDate>
	<copyright>Copyright (c) 2014 system. All rights reserved.</copyright>
	<license>GNU General Public License version 2 or later; see LICENSE.txt</license>
	<authorEmail></authorEmail>
	<authorUrl></authorUrl>
	<version>1.0.0</version>
	<description> <![CDATA[Manage SEF]]> </description>
	<files>
		<filename plugin="sefmaster">sefmaster.php</filename>
		<filename>index.html</filename>
	</files>
	<config>
		<fields name="params">
			<fieldset name="basic">
			</fieldset>
		</fields>
	</config>
</extension>

Тут важными атрибутом будет group и важным элементом будет version (именно тег а не атрибут вверху).

  • group - нужен для того, чтобы установить к каким событиям плагин имеет доступ. Список всех событий и групп вы можете посмотреть тут  
  • version - текущая версия вашего приложения, при каждом обновлении эту версию надо увеличивать, иначе Joomla не будет заменять старые файлы

Далее сам файл плагина, который и будет запускаться

<?php
defined('_JEXEC') or die;
jimport('joomla.plugin.plugin');
class plgsystemSEFMaster extends JPlugin {
	function onAfterInitialise() {
        $app = JFactory::getApplication();
        if ($app->isAdmin()) {
            return;
        }
        $router = $app->getRouter();
        $router->attachBuildRule(array($this, 'build'));
        $router->attachParseRule(array($this, 'parse'));
        return true;
    }
	public function build(&$siteRouter, &$uri) {}
	public function parse(&$siteRouter, &$uri) {}
}

Мы создали два метода build и parse

  • build - будет выполняться всегда, когда Joomla потребуется с генерировать SEF ссылку (на вход подается index.php?option=com_content&task=read&id=5 на выходе выдает /statyya.html)
  • parse - будет выполнятся, при заходе на сайт по SEF ссылке. Результатом должен быть обычный ?option=com_content&task=read&id=5

Говоря на входе и на выходе, нужно понимать, что сами метода не обязательно возвращают нужные данные. Они их заменяют своими. 

Таких плагинов в системе может быть сколько угодно. Порядок их работы определяется в при настройке плагина. 

Порядок работы плагинов в Joomla

Далее напишем метода build и посмотрим, что у нас получилось.

public function build(&$siteRouter, &$uri) {
	$path = $uri->getPath();
	$query = $uri->getQuery(true);
	switch ($query['option']) {
		case 'com_content':
			switch ($query['view']) {
				case 'article': 
					if (isset($query['id'])) {
						if (!is_numeric($query['id'])) {
							list($id) = explode(':', $query['id']);
						} else {
							$id = $query['id'];
						}
						$db = JFactory::getDBO();
						
						$query = $db->getQuery(true);
						$query
							->select('alias')
							->from($db->quoteName('#__content'))
							->where(array('id='.intval($id)));
							
						$>setQuery($query, 0, 1);
						
						$alias = $db->loadObject()->alias;
						$path = $alias.'.html';
						unset($query['catid'],$query['option'],
							$query['view'],$query['id'],$query['layout']);
					}
				break;
				case 'category': 
					if (isset($query['id'])) {
						$category = JCategories::getInstance('Content')->get($query['id']);
						$path = array();
						while ($category && $category->id > 1){
							array_unshift($path, $category->alias);
							$category = $category->getParent();
						}
						$path = implode('/', $path);
						unset($query['catid'],$query['option'],
							$query['view'],$query['id'],$query['layout']);
					}
				break;
			}
		break;
	} 
	if ($path != $uri->getPath() || $query != $uri->getQuery(true)) {
		unset($query['Itemid']);
		$uri->setPath($path);
		$uri->setQuery($query);
	}
	return $uri;
}

$query - будет содержать набор параметров, которые прописаны в URL которую вы конвертируете в SEF

  • $query['option'] - название компонента
  • $query['view'] - вид в этом компоненте. В примере рассматривается случай content компонента, и два его вида article и категория. Вам ничего не мешает дополнить switch/case своими компонентами и видами
  • $query['id'] - если в статье заполнен alias, то вероятно в этом параметре будет такое значение 12:aliaseeeee, если alias не был заполнен то такое 12 где 12 - это id статьи или категории а соответствующих таблицах

Для статей alias ищется прямым запросом к базе по id, для категорий используется специальное api, котрое возвращает весь путь. Если вам не нужен весь путь, то можно также воспользоваться прямым запросом к базе.

На этом этапе, если alias в статье или категории не заполнен, вы можете сами его сгенерировать и заполнить. А можете Создать свой тип sef ссылки. К примеру, на этом блоге, если alias не заполнен, то ссылка принемает вид http://xdan.ru/post-12.html где 12 - это id статьи. За то, чтобы этой строке соответствовала именно статья с id = 12, отвечает второй метод - parse

public function parse(&$siteRouter, &$uri) {
	$doc = JFactory::getDocument();
	$doc->setBase(JURI::base());
	
	$path = $uri->getPath();
	$path = str_replace(JURI::base() . '/', '', $path);
	$path = rtrim($path, '/');
	$newQuery = false;
	
	if ($path) {
		if (preg_match('#(.*)\.html#ui', $path, $matches)) {
			$db = JFactory::getDBO();
			$query = $db->getQuery(true);
			$query
				->select('id')
				->from($db->quoteName('#__content'))
				->where(array('alias='.$db->quote($matches[1])));
			
			$db->setQuery($query, 0, 1);
			$id = $db->loadObject()->id;
			$newQuery = 'option=com_content&view=article&id='.$id;
			$doc->setBase(JURI::base());
		} else {
			$_path = explode('/', $path);
			
			switch ($_path[0]) {
				// исключения, которые не следует обрабатывать
				case 'logout':break;
				case 'my-profile':break;
				case 'login':break;
				case 'register':break;
				
				case 'tag':
					$doc->setBase(JURI::base());
					$id = $this->getIdFromAlias($_path[1], 'tags');
					$newQuery = 'option=com_tags&view=tag&id='.$id.'-'.$_path[1];
				break;
				default:
					$doc->setBase(JURI::base());
					$alias = $path[count($path)-1];
		
					$db = JFactory::getDBO();
					$query = $db->getQuery(true);
					$query
						->select('id')
						->from('#__categories')
						->where(array(
							'alias='.$db->quote($alias),
							'extension="com_content"',
						));
					
					$db->setQuery($query, 0, 1);
					$id = $db->loadObject()->id;
					$newQuery = 'option=com_content&view=category&id='.$id.'&layout=blog';
			}
		}
	}
	
	if ($newQuery) {
		// добавляем те get параметры, 
		// которые шли после sef ссылки.
		$oldQuery = $uri->getQuery(false);
		if (!empty($oldQuery)) {
			$newQuery = $newQuery.'&'.$oldQuery;
		}
		
		// мы выполнили свою sef функцию
		$uri->setPath('');
		$uri->setQuery($newQuery);
		parse_str($newQuery,$output);
		// метод возвращает полученные данные, для других элментов системы
		return $output; 
	}
	return array();
}

Тут происходит обратный процесс. Сначала мы проверям подходит ли sef путь под такой шаблон <alias>.html - это бы означало, что пользователь открывает страницу со статьей, и нужно найти ее id. Делается это прямым запросом к БД. Для категорий делается идентичная операция, за исключением того, что нас интересует не весь путь, а только его частица - последняя частица. К примеру для пути /categories/cms/joomla/doc id категории нужно искать по alias'у doc

Код $doc->setBase(JURI::base()); нужен для того, чтобы Joomla корректно вспринимала корень сервера, в тот момент, когда ссылка будет такой - /categories/cms/joomla/doc. Если этого не делать, браузер будет пытаться грузить все относительные ссылки, исходя из этого корня.

Кроме этого, вы можете обозначить исключения, по которым плагин не будет обрабатывать такие пути. Такое обычно требуется в меню. Там вложения имеют место быть, и их плагин обрабатывать не должен. Кроме того, он не должен обрабатывать sef других компонентов.

Поэтому верным решением тут будет изменить эти две строки

$id = $db->loadObject()->id;
$newQuery = 'option=com_content&view=category&id='.$id.'&layout=blog';

таким образом

$id = $db->loadObject()->id;
if ($id) {
	$newQuery = 'option=com_content&view=category&id='.$id.'&layout=blog';
}

Тогда если искомый alias не был найден, то есть вероятность, что данный sef url принадлежит не нашему компоненту и его лучше не трогать. Его обработает встроенный sef плагин Joomla

Таких плагинов может быть сколько угодно, и каждый может выполнять лишь одну конкретную только для него роль.

Хорошим тоном в этой ситуации, будет добавление кеширования в плагин. Эти вещи вы сделайте сами. Дам вам лишь шаблон работы с кешем

$cache = new JCache(array('component'=>'sefmaster','lifetime'=>JFactory::getConfig()->get('cachetime')*60));
// смотрим есть ли элемент в кеше
if ($id = $cache->get($key = md5(serialize($path))) {
	return $id;
}
// если нет, находим его
/* тут код нахождения $id*/
//сохраняем найденный $id для следующего раза
$cache->store($id, $key, 'system');

В завершении добавьте к этим двум файлам, пустой index.html - это делается для безопасности, чтобы злоумышленних не увидел список файлов в папке расширения.

Далее, запакуйте файлы в zip архив и устанавливайте в менеджере расширеений, как обычное расширение Joomla. При экспериментах не забывайте менять версию

Исходные файлы к статье вы можете скачать тут

Всего доброго

Оставлять комментарии могут только зарегистрированные пользователи

Комментарии  

well-wisher
# well-wisher 30.01.2015 17:07
чувак у тебя не грузится страница
http://xdan.ru/kak-pomenjat-ssilku-vo-flash-bannere-ili-kak-dekompilirovat-flash.html
Valeriy Chupurnov
# Valeriy Chupurnov 30.01.2015 17:16
исправил
1-F7
# 1-F7 08.04.2017 03:30
Добрый день. Мне нужно переделать юрл в компоненте поиска Joomla 3.5 com_search.

В компоненте поиска ссылки выгладят так: search.html?searchword=Бог&searchphrase=all&start=20

А нужно сделать так:

1. Страница меню - /search.html (Здесь не чего не делаем).

2. Первая страница с результатами поиска - /search/Поисковой-запрос.html (Важен регистр и тире, а также, чтоб к примеру при переходе по ссылке /search/переход.html выдавало страницу с результатами поиска.

3. Пагинации - /search/Поисковой-запрос/page-2.html, page-3.html и так далее.

С помощью вашего плагина это возможно?