Всем доброго времени суток!
Третьего дня мне понадобилось провести блиц вебинар на тему ЧПУ в 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-е на человекопонятный текст.
Разберем пример
Кому и так ясно что-такое ЧПУ — мотаем дальше.
Как получить ЧПУ в 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:
→ How to use Doctrine Extensions
Тут нам советуют использовать бандл StofDoctrineExtensionsBundle, который обеспечит корректное подключение расширений Doctrine. Читаем документацию по нему:
→ StofDoctrineExtensionsBundle
Устанавливаем бандл StofDoctrineExtensionsBundle:
Подключаем скачанный бандл:
app/AppKernel.php
Из всего богатства вовлеченных нами в проект расширений Doctrine нам нужно всего одно — Sluggable behavior extension. Так что сконфигурируем StofDoctrineExtensionsBundle таким образом, чтобы это расширение было включено:
app/config/config.yml
Расширение Sluggable behavior extension подключено. Надо теперь указать ему, что именно от него требуется. Для этого почитаем по нему документацию:
→ Sluggable behavior extension for Doctrine 2
Оказывается, нам не так уж и много нужно сделать. Всего-то нужно в сущности продукта подключить класс аннотаций, которые предоставляет нам расширение, да указать этими аннотациями полю Product:slug, что оно должно автоматически заполняться как slug на основе полей, которые мы выберем:
src/AppBundle/Entity/Product.php
Здесь я указал аннотацией
Пора создавать продукты. Но перед этим нужно убрать лишнее поле из формы, ведь поле slug Doctrine будет заполнять самостоятельно:
src/AppBundle/Form/ProductType.php
Заходим на форму создания продукта (http://localhost:2020/products/new)

Сохраняем и видим, что 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
Получилось так:

Теперь маршрут на детальный просмотр продукта выглядит так:
Маршрут на редактирование продукта:
Уникальность slug-ов
Вопрос заданный мне в комментариях пользователем psycho-coder сподвиг меня дополнить статью. А что, если я захочу создать несколько продуктов с одинаковым наименованием? Ведь Symfony позволяет это сделать. Что будет тогда со slug-ами, которые пишутся в поле с уникальным ключом в базе данных?
Как я говорил выше, Doctrine Sluggable behavior extension берет на себя ответственность по построению уникальных slug-ов.
Для примера я создал три раза подряд продукт с одним и тем же именем: „Что-то осмысленное“. Автоматически сгенерированные slug-и получились такими:
Если этот вариант не нравится, то можно для поля slug указать генерацию на основе не одного поля, а двух. Пример подобной аннотации для поля slug:
Трижды создаем продукт с промежутком в одну минуту и получаем slug-и:
Если и это не нравится, то придумываем свой вариант и делимся в комментариях
Напоследок
Мы добились нашей цели:
Пора идти закупаться пивом и думать как это все воплотить в большом проекте. Если я был вам полезен, значит я рад быть вам полезен.
Архив с Symfony проектом, созданным в процессе написания статьи прикладываю тут.
Кстати, 3d картинку рендерил сам специально для этой статьи. Мне она понравилась, да и сил много не отняла.
Всем хороших маршрутов!
Третьего дня мне понадобилось провести блиц вебинар на тему ЧПУ в 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-е на человекопонятный текст.
Разберем пример
Кому и так ясно что-такое ЧПУ — мотаем дальше.
Разбор примера как могли бы выглядеть 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 продукта
Думаю с наглядностью мы закончили.
Я зашел на страницу «Мячи для настольного тенниса» и адрес в ее оказался:
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 сегментов, уменьшающих неоднозначность пути:
- rozetka.com.ua/t_balls/donic-elit-1-6-beliy
- rozetka.com.ua/category/t_balls/donic-elit-1-6-beliy
- rozetka.com.ua/category/t_balls/product/donic-elit-1-6-beliy
Здесь:
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
... "require-dev": { ... "phpunit/phpunit": "^6.2.1" ... }, ... "config": { "platform": { "php": "7.0.15" }, ... }, ...
И произведите обновление зависимостей:
composer update
Или так, если у вас композер архивом лежит в проекте:
php composer.phar update
Генерируем внутри бандла AppBundle сущность продукта консольной командой:
php bin/console doctrine:generate:entity --entity=AppBundle:Product --fields="name:string description:text seller:string publishDate:datetime slug:string(length=128 nullable=false unique=true)" -q
Наверняка вы заметили, что помимо остальных полей имеется интересное поле slug. Я сделал его уникальным, и без возможн��сти быть равным null. Дело в том, что в нашем новом проекте мы должны будем иметь возможность выбирать товары из базы данных как по id-шникам, так и по slug-ам. Slug теперь — наш второй после id уникальный идентификатор записи.
Для удобства изложения и для вашего удобства тестирования мною изложенного материала сгенерируем CRUD контроллер на основе сущности AppBundle:Product, созданой на предыдущем шаге. Для этого выполним консольные команды:
php bin/console doctrine:database:create #создаем базу данных php bin/console doctrine:schema:create #создаем структуру данных в базе данных php bin/console doctrine:generate:crud --entity="AppBundle:Product" --route-prefix=products --with-write -n #генерируем CRUD контроллер
Теперь после запуска сервера
php bin/console server:run localhost:2020
Мы можем посетить страницу http://localhost:2020/products/ и увидеть пустой список продуктов да ссылку на страницу создания нового продукта:

Повременим с созданием новых продуктов. Ведь нас ждет подключение расширений Doctrine.
Подключение поведенческих расширений Doctrine
Почему нам нужны расширения Doctrine? Разве мы сами не можем генерировать slug для продукта? В целом да. Все это можно сделать собственными руками: генерировать slug на основе поля или набора полей, заботиться об уникальности slug-а, иметь всегда в виду необходимость его заполнения, иначе сайт рухнет. Но мы не ради этого здесь собрались. Так что читаем официальную документацию по тому как пользоваться расширениями Doctrine:
→ How to use Doctrine Extensions
Тут нам советуют использовать бандл StofDoctrineExtensionsBundle, который обеспечит корректное подключение расширений Doctrine. Читаем документацию по нему:
→ StofDoctrineExtensionsBundle
Устанавливаем бандл StofDoctrineExtensionsBundle:
composer require stof/doctrine-extensions-bundle
Подключаем скачанный бандл:
app/AppKernel.php
class AppKernel extends Kernel { public function registerBundles() { $bundles = array( // ... new Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle(), ); // ... } // ... }
Из всего богатства вовлеченных нами в проект расширений Doctrine нам нужно всего одно — Sluggable behavior extension. Так что сконфигурируем StofDoctrineExtensionsBundle таким образом, чтобы это расширение было включено:
app/config/config.yml
... stof_doctrine_extensions: default_locale: en_US orm: default: sluggable : true ...
Расширение Sluggable behavior extension подключено. Надо теперь указать ему, что именно от него требуется. Для этого почитаем по нему документацию:
→ Sluggable behavior extension for Doctrine 2
Оказывается, нам не так уж и много нужно сделать. Всего-то нужно в сущности продукта подключить класс аннотаций, которые предоставляет нам расширение, да указать этими аннотациями полю Product:slug, что оно должно автоматически заполняться как slug на основе полей, которые мы выберем:
src/AppBundle/Entity/Product.php
... use Gedmo\Mapping\Annotation as Gedmo; ... /** * Product * * @ORM\Table(name="product") * @ORM\Entity(repositoryClass="AppBundle\Repository\ProductRepository") */ class Product { ... /** * @var string * * @Gedmo\Slug(fields={"name"}) * @ORM\Column(name="slug", type="string", length=128, nullable=false, unique=true) */ private $slug; ... }
Здесь я указал аннотацией
@Gedmo\Slug(fields={"name"}), что я хочу, чтобы slug генерировался на основании поля name. Можно указать несколько полей, чтобы они конкантинировались при генерации. Например, часто вместо с именем сущности указывают дату создания: @Gedmo\Slug(fields={"publishDate", "name"}).Пора создавать продукты. Но перед этим нужно убрать лишнее поле из формы, ведь поле slug Doctrine будет заполнять самостоятельно:
src/AppBundle/Form/ProductType.php
... class ProductType extends AbstractType { /** * {@inheritdoc} */ public function buildForm(FormBuilderInterface $builder, array $options) { $builder->add('name')->add('description')->add('seller')->add('publishDate'); //Удалили ->add('slug') } ... }
Заходим на форму создания продукта (http://localhost:2020/products/new)

Сохраняем и видим, что slug сгенерирован. Он годен для использования в маршрутах вашего приложения:

Остается проверить ЧПУ на деле.
Первый ЧПУ маршрут
Сделаем все по простому. А именно — переделаем маршруты products_show и products_edit:

таким образом, чтобы они показывали нам продукт не по id-нику, а по slug-у. Маршрут products_delete менять не будем, так как он не виден ни пользователю, ни поисковику.
src/AppBundle/Controller/ProductController.php
... class ProductController extends Controller { ... /** * Finds and displays a product entity. * * @Route("/{slug}", name="products_show") * @Method("GET") * @param string $slug * @return \Symfony\Component\HttpFoundation\Response */ public function showAction(string $slug) { $product = $this->getDoctrine() ->getRepository('AppBundle:Product') ->findOneBySlug($slug); $deleteForm = $this->createDeleteForm($product); return $this->render('product/show.html.twig', array( 'product' => $product, 'delete_form' => $deleteForm->createView(), )); } /** * Displays a form to edit an existing product entity. * * @Route("/{slug}/edit", name="products_edit") * @Method({"GET", "POST"}) * @param Request $request * @param string $slug * @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response */ public function editAction(Request $request, string $slug) { $product = $this->getDoctrine() ->getRepository('AppBundle:Product') ->findOneBySlug($slug); $deleteForm = $this->createDeleteForm($product); $editForm = $this->createForm('AppBundle\Form\ProductType', $product); $editForm->handleRequest($request); if ($editForm->isSubmitted() && $editForm->isValid()) { $this->getDoctrine()->getManager()->flush(); return $this->redirectToRoute('products_edit', array('slug' => $product->getSlug())); } return $this->render('product/edit.html.twig', array( 'product' => $product, 'edit_form' => $editForm->createView(), 'delete_form' => $deleteForm->createView(), )); } ... }
app/Resources/views/product/index.html.twig
{% extends 'base.html.twig' %} {% block body %} <h1>Products list</h1> <table> <thead> <tr> <th>Id</th> <th>Name</th> <th>Description</th> <th>Seller</th> <th>Publishdate</th> <th>Actions</th> </tr> </thead> <tbody> {% for product in products %} <tr> <td><a href="{{ path('products_show', { 'slug': product.slug }) }}">{{ product.id }}</a></td> <td>{{ product.name }}</td> <td>{{ product.description }}</td> <td>{{ product.seller }}</td> <td>{% if product.publishDate %}{{ product.publishDate|date('Y-m-d H:i:s') }}{% endif %}</td> <td> <ul> <li> <a href="{{ path('products_show', { 'slug': product.slug }) }}">show</a> </li> <li> <a href="{{ path('products_edit', { 'slug': product.slug }) }}">edit</a> </li> </ul> </td> </tr> {% endfor %} </tbody> </table> <ul> <li> <a href="{{ path('products_new') }}">Create a new product</a> </li> </ul> {% endblock %}
app/Resources/views/product/show.html.twig
{% extends 'base.html.twig' %} {% block body %} <h1>Product</h1> <table> <tbody> <tr> <th>Id</th> <td>{{ product.id }}</td> </tr> <tr> <th>Name</th> <td>{{ product.name }}</td> </tr> <tr> <th>Description</th> <td>{{ product.description }}</td> </tr> <tr> <th>Seller</th> <td>{{ product.seller }}</td> </tr> <tr> <th>Publishdate</th> <td>{% if product.publishDate %}{{ product.publishDate|date('Y-m-d H:i:s') }}{% endif %}</td> </tr> <tr> <th>Slug</th> <td>{{ product.slug }}</td> </tr> </tbody> </table> <ul> <li> <a href="{{ path('products_index') }}">Back to the list</a> </li> <li> <a href="{{ path('products_edit', { 'slug': product.slug }) }}">Edit</a> </li> <li> {{ form_start(delete_form) }} <input type="submit" value="Delete"> {{ form_end(delete_form) }} </li> </ul> {% endblock %}
Получилось так:

Теперь маршрут на детальный просмотр продукта выглядит так:
@Route("/{slug}", name="products_show")Маршрут на редактирование продукта:
@Route("/{slug}/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:
@Gedmo\Slug(fields={"name", "publishDate"})Трижды создаем продукт с промежутком в одну минуту и получаем 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 картинку рендерил сам специально для этой статьи. Мне она понравилась, да и сил много не отняла.
Всем хороших маршрутов!
