ЧПУ (SEF URLs) в Symfony 3 — автогенерация slug, настройка и маршрутизация
Третьего дня мне понадобилось провести блиц вебинар на тему ЧПУ в Symfony. Вообще время вебинара у меня ограничено двумя часами, при этом я должен был рассказать еще и про автогенерацию CRUD функционала (scaffolding) в той же Symfony, и про простейший способ создать постраничность. Это создало проблему, так как я знаю как сделать ЧПУ «ручками», не прибегая к автоматизированным под эту задачу инструментам, но рассказ получился бы долгий и оказались бы затянутыми в обсуждение лишние темы. Поэтому я пошел спрашивать у Интернета как сделать все проще. И вот я оказался в той редкой ситуации, когда такая популярная платформа как Symfony не имеет банального обучающего материала на тему «ЧПУ в три клика». Смотрел так же и на английском языке, но там тоже пусто (может плохо искал — время было ограничено). В общем я справился с поиском разрозненного материала по данной теме, а так же со сбором его в единое повествование, так что почему бы не поделиться со всеми?
Терминология Я не знаю кто будет читать мою статью, так что для начала разберемся в терминологии.
ЧПУ — аббревиатура от «Человекопонятные URL». На английский переводится как Friendly URL или Semantic URL. Однако чаще используется как аналогичная аббревиатура: SEF URLs — Search Engine Friendly URLs.
Что дает вам ЧПУ?
Самое очевидное — это то, что URL-ы вашего сайта будут понятны пользователю. Зачем, только, ему их читать? Большинство клиентов моих заказчиков даже не подозревают о наличии адресной строки браузера. Если есть сомнения, то посмотрите сколько результатов выведет запрос в Гугл «Где находится адресная строка браузера».
Однако есть неоспоримые плюс — правильно составленные ЧПУ являются одним из важных элементов SEO оптимизации, благодаря которой странички вашего сайта будут появляться в поисковике на первой странице. Для этого URL на вашем сайте должны содержать релевантную для поисковика информацию о страничках, на которые они ведут и иметь продуманную вложенность. Все это замечательно, но речь в этой статье не о SEO оптимизации. Предполагается, что вы уже решили получить ЧПУ на своем сайте и дополнительная мотивация вам уже не требуется.
ЧПУ URL-ы — это адреса страниц описывающие всю необходимую информацию о запрашиваемой у сервера странице в виде сегментов пути, то есть GET параметры в таком URL-е большая редкость.
Обычно можно найти шаблоны пути подобные таким: http(s)://Домен/slug-категории/slug-подкатегории/slug-товара-или-статьи http(s)://Домен/Профайл/slug-владельца-профайла
Тут появляется еще один термин — Slug, который важен для дальнейшего понимания статьи:
Slug — (из Викисловаря) альтернативная дружественная к восприятию человеком — буквенно-цифровая часть универсального адреса интернет-ссылки (URL) к рубрицируемому содержимому. То есть, если по простому, то slug заменяет всяческие признаки и id-шники ресурсов нашего сайта в URL-е на человекопонятный текст.
Разберем пример
Кому и так ясно что-такое ЧПУ — мотаем дальше.
На примере сайта магазина rozetka.com.ua (первый сайт, который попался под руку). ЧПУ тут в зачаточном состоянии. Давайте попробуем их ссылки довести до ума вручную:
Я зашел на страницу «Мячи для настольного тенниса» и адрес в ее оказался: rozetka.com.ua/t_balls/c81265
Явно, что «c81265» первым символом указывает на то, что запрашиваемый объект — категория товаров, а число после него — id категории в базе данных.
Переделав под ЧПУ у на получилось бы просто: rozetka.com.ua/t_balls
Просто удалили id-шник? Как же так? А как же контентные страницы (http://rozetka.com.ua/contacts/)? Да никаких проблем. Просто поставьте все контентные страницы, так чтобы текущий путь в запросе сверялся в первую очередь с ними. В Symfony это делается всего лишь тем, что маршруты для этих путей объявлены первыми.
Если и так не получается или у вас на сайте есть еще что-то важное кроме контентных страниц и категорий товаров, то делаем более однозначный путь: rozetka.com.ua/category/t_balls
Далее я перешел на сам продукт «Мячи для настольного тенниса Donic Elit 1* 6 шт белый (618016): rozetka.com.ua/198578/p198578
Вот тут просто беда. ЧПУ даже перестало пахнуть.
Как должен был выглядеть такой URL? В зависимости от того как у вас устроен сайт может быть несколько вариантов. По степени загруженности URL сегментов, уменьшающих неоднозначность пути:
t_balls — slug категории donic-elit-1-6-beliy — slug продукта
Думаю с наглядностью мы закончили.
Как получить ЧПУ в Symfony
Объяснять буду на примере свежей установки Symfony. На момент написания была взята версия Symfony 3.3.0. Предполагается, что вы установили Symfony и сконфигурировали доступ к базе данных.
Прежде чем начнется суть да дело нужно подружить нашу Symfony 3.3.0 с phpunit, чтобы она не валилась после автогенерации контроллеров. Дополните composer.json проекта двумя строчками:
composer.json
И произведите обновление зависимостей:
Или так, если у вас композер архивом лежит в проекте:
Генерируем внутри бандла AppBundle сущность продукта консольной командой:
Наверняка вы заметили, что помимо остальных полей имеется интересное поле slug. Я сделал его уникальным, и без возможности быть равным null. Дело в том, что в нашем новом проекте мы должны будем иметь возможность выбирать товары из базы данных как по id-шникам, так и по slug-ам. Slug теперь — наш второй после id уникальный идентификатор записи.
Для удобства изложения и для вашего удобства тестирования мною изложенного материала сгенерируем CRUD контроллер на основе сущности AppBundle:Product, созданой на предыдущем шаге. Для этого выполним консольные команды:
Теперь после запуска сервера
Мы можем посетить страницу http://localhost:2020/products/ и увидеть пустой список продуктов да ссылку на страницу создания нового продукта:
Повременим с созданием новых продуктов. Ведь нас ждет подключение расширений Doctrine.
Подключение поведенческих расширений Doctrine
Почему нам нужны расширения Doctrine? Разве мы сами не можем генерировать slug для продукта? В целом да. Все это можно сделать собственными руками: генерировать slug на основе поля или набора полей, заботиться об уникальности slug-а, иметь всегда в виду необходимость его заполнения, иначе сайт рухнет. Но мы не ради этого здесь собрались. Так что читаем официальную документацию по тому как пользоваться расширениями Doctrine:
Тут нам советуют использовать бандл StofDoctrineExtensionsBundle, который обеспечит корректное подключение расширений Doctrine. Читаем документацию по нему:
Устанавливаем бандл StofDoctrineExtensionsBundle:
Подключаем скачанный бандл:
app/AppKernel.php
Из всего богатства вовлеченных нами в проект расширений Doctrine нам нужно всего одно — Sluggable behavior extension. Так что сконфигурируем StofDoctrineExtensionsBundle таким образом, чтобы это расширение было включено:
app/config/config.yml
Расширение Sluggable behavior extension подключено. Надо теперь указать ему, что именно от него требуется. Для этого почитаем по нему документацию:
Оказывается, нам не так уж и много нужно сделать. Всего-то нужно в сущности продукта подключить класс аннотаций, которые предоставляет нам расширение, да указать этими аннотациями полю Product:slug, что оно должно автоматически заполняться как slug на основе полей, которые мы выберем:
src/AppBundle/Entity/Product.php
Здесь я указал аннотацией @Gedmo\Slug(fields=) , что я хочу, чтобы slug генерировался на основании поля name. Можно указать несколько полей, чтобы они конкантинировались при генерации. Например, часто вместо с именем сущности указывают дату создания: @Gedmo\Slug(fields=) .
Пора создавать продукты. Но перед этим нужно убрать лишнее поле из формы, ведь поле slug Doctrine будет заполнять самостоятельно:
src/AppBundle/Form/ProductType.php
Сохраняем и видим, что slug сгенерирован. Он годен для использования в маршрутах вашего приложения:
Остается проверить ЧПУ на деле.
Первый ЧПУ маршрут
Сделаем все по простому. А именно — переделаем маршруты products_show и products_edit:
таким образом, чтобы они показывали нам продукт не по id-нику, а по slug-у. Маршрут products_delete менять не будем, так как он не виден ни пользователю, ни поисковику.
src/AppBundle/Controller/ProductController.php
app/Resources/views/product/index.html.twig
app/Resources/views/product/show.html.twig
Теперь маршрут на детальный просмотр продукта выглядит так: @Route("/", name="products_show")
Маршрут на редактирование продукта: @Route("//edit", name="products_edit")
Уникальность slug-ов Вопрос заданный мне в комментариях пользователем psycho-coder сподвиг меня дополнить статью. А что, если я захочу создать несколько продуктов с одинаковым наименованием? Ведь Symfony позволяет это сделать. Что будет тогда со slug-ами, которые пишутся в поле с уникальным ключом в базе данных?
Как я говорил выше, Doctrine Sluggable behavior extension берет на себя ответственность по построению уникальных slug-ов.
Для примера я создал три раза подряд продукт с одним и тем же именем: „Что-то осмысленное“. Автоматически сгенерированные slug-и получились такими:
- chto-to-osmyslennoe
- chto-to-osmyslennoe-1
- chto-to-osmyslennoe-2
Если этот вариант не нравится, то можно для поля slug указать генерацию на основе не одного поля, а двух. Пример подобной аннотации для поля slug:
Трижды создаем продукт с промежутком в одну минуту и получаем slug-и:
- chto-to-osmyslennoe-2015-05-05-04-04
- chto-to-osmyslennoe-2015-05-05-04-05
- chto-to-osmyslennoe-2015-05-05-04-06
Если и это не нравится, то придумываем свой вариант и делимся в комментариях
Мы добились нашей цели:
- slug генерируется автоматически при сохранении сущности
- маршруты работают с учетом slug вместо id-шника
- Поле slug в базе данных обладает уникальным ключом, что позволяет нивелировать тормоза при выборке продуктов по этому полю
Архив с Symfony проектом, созданным в процессе написания статьи прикладываю тут.
Кстати, 3d картинку рендерил сам специально для этой статьи. Мне она понравилась, да и сил много не отняла.