
В данной статье я расскажу, как сделать "красивые" ссылки (или какие нужно) для вашего сайта на yii2 с помощью создания своего класса правил для компонента Yii2 urlManager. Стоит заметить, что создание класса и отдельной таблицы в БД для него - это довольно радикальный подход и возможно вам достаточно будет создать несколько полей для "слага" и настроить правила urlManager как тут.
Приведу несколько примеров того, что у нас получится в итоге:
- главная страница: http://site.com;
- страница отдельной статьи: http://site.com/post.html;
- пагинация (переход на следующую страницу): http://site.com/?page=3;
- страница поиска с переданным параметром: http://site.com/search?q=php;
- страница рубрики: http://site.com/category/php;
- страница метки: http://site.com/tag/hpu.
Большинство правил можно задать не создавая отдельный класс для этого, поэтому использовать его будем тогда, когда для этого понадобится брать данные из таблицы БД, остальные правила пропишем прямо в конфигурации. Для этого открываем файл frontend\config\main.php (если Yii2 Advanced) и в массиве components меняем (создаем) urlManager :
'urlManager' => [ //'class' => 'yii\web\UrlManager', 'showScriptName' => false, //отключаем r=routes //запретить стандартные URL если не соответствует правилам класса 'enableStrictParsing' => true, 'enablePrettyUrl' => true, //отключаем index.php 'rules' => array( '/' => 'post/index', //для главной страницы 'site/captcha' => 'site/captcha', //для капчи ничего не меняем //эти страницы будут открываться при указании только одного действия '<action:search|login|logout|signup|request-password-reset>' => 'site/<action>', //остальные правила в своем классе SefRule ['class' => 'common\components\SefRule', 'connectionID' => 'db', ], ), ],Тут мы прописываем несколько правил, при использовании которых yii2 не должен использовать базу данных. Их мы размещаем до подключения нашего класса SefRule в массие "rules", чтобы при соответствии, программа их применяла не доходя до выполнения следующих строк. А именно это:
- главная страница;
- вывод капчи (например в форме контактов и комментариев);
- для страницы поиска.
'search' => 'site/search',то в адресной строке будет написано просто search, без указания имени контроллера (site). Т.к. я хочу вызывать по одному только названию действия и несколько других страниц контроллера site, я это объединил в одну строку:
'<action:search|login|logout|signup|request-password-reset>' => 'site/<action>',Подробнее о составлении правил маршрутизации можно прочитать тут: github.com
Также в компоненте конфигурации urlManager мы отключаем вывод точки входа - страницу index.php, других служебных надписей (routes) и запрещаем фреймворку, при несоответствии нашим правилам, самовольно искать способ открыть запрашиваемую страницу (убираем дубли страниц).
Создание класса правил ЧПУ.
Как видно из правил urlManager, класс будет размещаться в common\components. Ниже приведу полный код файла SefRule.php:<?php namespace common\components; use common\models\Sef; use yii\web\UrlRuleInterface; use yii\base\Object; class SefRule extends Object implements UrlRuleInterface{ public $connectionID = 'db'; public $name; public function init(){ if ($this->name === null) { $this->name = __CLASS__; } } //Формирует ссылки в заданном виде (часть формируется в коде, остальное берется из БД) public function createUrl($manager, $route, $params){ //debug($route); //Определяем контроллеры, у которых к страницам нужно добавлять .html $controller = explode('/',$route)[0]; //Получаем контроллер if ($route == 'post/view' || $controller == 'site') $html = '.html'; else $html = ''; //Если передаются параметры (напрмиер ?id=3&page=2) сохраняем в $link по-очереди $link =''; $page =''; if(count($params)){ $link = "?"; $page = false; foreach ($params as $key => $value){ if($key == 'page'){ $page = $value; continue; } $link .= "$key=$value&"; } $link = substr($link, 0, -1); //удаляем последний символ (&) } //Из БД получаем строку со ссылкой на которую нужно будет поменять $sef = Sef::find()->where(['link' => $route.$link])->one(); if ($sef){ //Если есть - добавляем пагинацию в конец (?page=2) if ($page) return $sef->link_sef."$html?page=$page"; else return $sef->link_sef.$html; } return false; } //Разбирает входящий URL запрос, преобразует ссылки произвольного вида (из БД поле link_sef) в нужный для Yii2 public function parseRequest($manager, $request){ //Получаем URL $pathInfo = $request->getPathInfo(); //Получаем 1 часть, до слэша, если есть $alias = explode('/',$pathInfo)[0]; //Если на конце .html, то убираем для поиска в БД $alias_small = str_replace(".html","",$alias); //не выводить .html для указанных URL (первая часть алиаса) $not_html = [ 'category','tag','posts','notes' ]; /* * Проверяем наличие URL (до слэша) в $not_html * Если есть, то в URL не должно быть окончание .html * $exception = true разрешает поиск URL в БД */ $exception = false; if(array_search($alias_small, $not_html) !== FALSE){ if (preg_match("/^(.*)\.html$/",$pathInfo)) return false; $exception = true; } //Будем искать в БД если ссылка содержит .html или есть в $not_html if(preg_match('/^(.*)\.html$/', $pathInfo, $matches) || $exception){ $pathInfo = isset($matches[1]) ? $matches[1] : $pathInfo; //получаем из БД данные по строке содержащей заданный алиас $sef = Sef::find()->where(['link_sef' => $pathInfo])->one(); if($sef){ //Разбивает строку типа post/view?id=5 на массив по разделителю $link_data = explode('?',$sef->link); //берем только первую часть без параметров (контроллер/действие) $route = $link_data[0]; $params = array(); //если есть параметры - вставляем их if(isset($link_data[1])){ $temp = explode('&',$link_data[1]); foreach($temp as $t){ $t = explode('=', $t); $params[$t[0]] = $t[1]; } } //$route - контроллер/действие //$params - параметры return [$route, $params]; } } return false; } }
Наш класс SefRule наследует методы от класса Object и реализует интерфейс UrlRuleInterface. Он состоит из двух основных методов, один из которых createUrl() - выводит из БД URL в том виде в котором нам нужно, а второй parseRequest() преобразует нашу фантазию в вид стандартный для Yii2. То есть он ищет URL произвольного вида в БД, возвращая нормальный URL. Все указанные методы - стандартные, переименовывать их нельзя.
В методе createUrl() я указал контроллеры (у меня это PostController и SiteController), при выводе страниц которых будет добавляться префикс .html, указывающий что это статическая страница.
В коде много комментариев, если останутся вопросы - задавайте.

Теперь касательно БАЗЫ ДАННЫХ.
Для тех, кто использует yii миграции для работы с БД, выкладываю соответствующий код:public function safeUp() { $tableOptions = null; if ($this->db->driverName === 'mysql') { $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_general_ci ENGINE=InnoDB'; } $this->createTable('{{%sef}}', [ 'id' => $this->primaryKey(), 'link' => $this->string()->notNull(), 'link_sef' => $this->string()->notNull(), ], $tableOptions); } public function safeDown() { $this->dropTable('{{%sef}}'); }Можно создать вручную. А именно - таблицу я назвал sef, в ней только 3 поля:
- id (primaryKey, AUTO_INCREMENT);
- link (varchar(255));
- link_sef (varchar(255)).

В поле link указываем обычные URL, которые нужны Yii2. Они задаются в формате контроллер/действие/[параметры] и посмотреть их можно прямо в адресной строке. Как видно по изображению у меня записи формируются с помощью 4-х контроллеров. Основной post, который управляет записями из БД, вспомогательный site, который формирует несколько статических, служебных страниц, например страницу контактов. Так же отдельно контроллер для рубрик и меток.
После контроллера нужно указать действие которые определяет файл для вывода страницы. Для записей которые берутся из БД, у меня это view. Дальше указывается id конкретной записи/рубрики/метки по которой производится выборка из БД.
В поле link_sef указывается фраза-алиас, то есть то, как мы хотим чтобы было прописано в адресной строке и ссылках. Алиас может быть любой, но необходимо, чтобы он был уникальный, т.к. один URL может соответствовать только одной странице. Для рубрик (категорий) я сделал, чтобы он начинался с category, а для меток с tag, что логично и ЧПУ-доступно :)
Вот и все, теперь мы имеем короткие, понятные URL. Причем страницы наших постов будут называться не 1,2,3 а иметь свое URL-название. Для удобства можно подключить автоматическое задание URL-алиаса в админке сайта при создании/редактировании постов и прочего. Но это уже материал для другой статьи.
Наконец нашел что-то толковое по этой теме.
ответ на комментарий Олег от 17.01.2017
Неделю бьюсь! Ничего не понятно, куда что и зачем должно приходить и в каком виде. Только тут в явном, понятном нашёл, где url видимый, а где внутренний.
Буду делать.
Автору спасибо!
после того, как я задаю правила через класс на странице происходит более 30 запросов к БД (по 1 запросу к каждому пункту меню из метода createUrl)
при этом замена url работает нормально.
если я удаляю все из createUrl то все продолжает работать, но при этом кол-во запросов к БД снижается до 3 (как и должно быть)
Собственно вопрос - почему createUrl трогает мои пункты меню и как избежать этих 30 запросов?
ответ на комментарий Сергей от 11.07.2017
Не понятно, что имеется ввиду под "замена url работает нормально". Если вы "жестко" прописываете ссылки для пунктов меню, то, конечно, будет работать без запросов к БД, ну и если "удалить код класса", то, также, запросов к БД не будет.
То есть варианты - прописать ссылки на пункты меню без использования urlManager или указать их в массиве rules. Можно еще попробовать закешировать - http://klisl.com/cache_yii.html
Ребята здравствуйте. У меня есть такой правила для контроллера site , будет скрывать контроллер и останутся только action.Вопрос в том, что так работает и (www.site.ru/controller/action) и только (www.site.ru/action). Как можно сделать так , что при обращении www.site.ru/site/about дала 404 error и только работал так (www.site.ru/about) . Помогите пожалуйста.
ответ на комментарий Hovhannes от 04.09.2017
ответ на комментарий Денис от 20.02.2018