Хлебные крошки в Symfony2
Начиная проект на Symfony2, в отличие от Yii, у разработчика наверняка возникает чувство недостатка встроенной реализации де-факто стандартных обыденных вещей вроде паджинатора и хлебных крошек. Но это сложно назвать проблемой, так как за много лет существования фреймворка написано множество готовых компонентов.
Сегодня рассмотрим, как дополнить свой проект навигационной цепочкой вроде имеющейся здесь:
Дмитрий Елисеев » Блог » Программирование » Хлебные крошки в Symfony2
Кроме повышения удобства навигации для пользователей, хлебные крошки полезны и для SEO. Именно по ним поисковые системы определяют ссылочную структуру сайта.
После минуты поиска в Гугле для простой реализации хлебных крошек мной был выбран одноимённый компонент от некого коллектива White October из Оксфорда. Никаких премудростей в нём замечено не было. Главное, что работает и вполне совместим со стабильным релизом фреймворка.
Первым делом загружаем бандл:
php composer.phar require whiteoctober/breadcrumbs-bundle:dev-master
В composer.json
добавится строчка:
"require": { ... "whiteoctober/breadcrumbs-bundle": "dev-master", },
и выполнится загрузка в vendor
.
После успешного завершения подключаем к приложению WhiteOctoberBreadcrumbsBundle
в app/AppKernel.php
:
public function registerBundles() { return array( ... new WhiteOctober\BreadcrumbsBundle\WhiteOctoberBreadcrumbsBundle(), ... ); }
Бандл регистрирует сервис для работы с коллекцией крошек и Twig-функцию для непосредственного вывода их в шаблоне.
Для добавления пунктов нужно обратиться к сервису white_october_breadcrumbs
и добавить элементы методом addItem
. Метод принимает текст элемента, URL и массив подстановок для перевода (подробнее в README расширения по ссылке выше). Переводами мы особенно заниматься не будем и впишем надписи элементов на русском. При необходимости можно заменить их на соответствующие ключи.
Итак, по инструкции наш контроллер может выглядеть примерно так:
namespace App\AdminBundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; class UserController extends Controller { public function indexShow($id) { $entity = $this->loadEntity($id); $router = $this->get('router'); $breadcrumbs = $this->get('white_october_breadcrumbs'); $breadcrumbs->addItem('Главная', $router->generate('homepage')); $breadcrumbs->addItem('Панель управления', $router->generate('admin_homepage')); $breadcrumbs->addItem('Пользователи', $router->generate('admin_user')); $breadcrumbs->addItem($entity->getUsername()); return $this->render('AdminBundle:User:show.html.twig', array( 'entity' => $entity, )); } /** * @param $id * @return \App\UserBundle\Entity\User; * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException */ private function loadEntity($id) { $em = $this->getDoctrine()->getManager(); $entity = $em->getRepository('UserBundle:User')->find($id); if (!$entity) { throw $this->createNotFoundException('Unable to find User entity.'); } return $entity; } }
Теперь открываем главный шаблон app/Resources/views/base.html.twig
и перед блоком body
добавляем отображение крошек:
<section class="main"> {{ wo_render_breadcrumbs() }} {% block body %}{% endblock %} </section>
К слову, для удобства можно все служебные блоки выносить в отдельно импортируемые файлы:
<section class="main"> {% include '::breadcrumbs.html.twig' %} {% block body %}{% endblock %} </section>
Перед заголовком отобразится навигационная цепочка:
Главная / Панель управления / Пользователи / Вася
Со ссылками на всех элементах кроме последнего.
Контроллер или представление
По опыту разработки на Yii мне больше импонирует не засорять контроллеры, а указывать пункты хлебных крошек прямо в представлениях. Это и удобнее при наследовании бандлов. Например, для внедрения хлебных крошек на страницы FOSUserBundle
нам достаточно создать свой бандл UserBundle
, сделать его дочерним и переопределить в нём все стандартные представления. Это удобнее и менее трудозатратно, чем переписать все его контроллеры.
В компоненте от White October возможности задать пункты в представлениях не имеется. Исправим этот недостаток.
В своём бандле создадим папку Twig/Extension
и добавим класс BreadcrumbExtension
. В нём объявим новую Twig-функцию breadcrumb
и в её реализацию перенесём код по добавлению пункта из контроллера. Ещё сделаем автоматическое добавление ссылки на главную страницу:
namespace App\MainBundle\Twig\Extension; use Symfony\Component\Routing\Router; use WhiteOctober\BreadcrumbsBundle\Model\Breadcrumbs; class BreadcrumbExtension extends \Twig_Extension { /** * @var Breadcrumbs */ private $breadcrumbs; /** * @var Router */ private $router; /** * @var string */ private $homeRoute; /** * @var string */ private $homeLabel; /** * @param Breadcrumbs $breadcrumbs * @param Router $router * @param string $homeRoute * @param string $homeLabel */ public function __construct(Breadcrumbs $breadcrumbs, Router $router, $homeRoute = 'homepage', $homeLabel = 'Home') { $this->breadcrumbs = $breadcrumbs; $this->router = $router; $this->homeRoute = $homeRoute; $this->homeLabel = $homeLabel; } /** * @inheritdoc */ public function getFunctions() { return array( new \Twig_SimpleFunction('breadcrumb', array($this, 'addBreadcrumb')) ); } public function addBreadcrumb($label, $url = '', array $translationParameters = array()) { if (!$this->breadcrumbs->count()) { $this->breadcrumbs->addItem($this->homeLabel, $this->router->generate($this->homeRoute)); } $this->breadcrumbs->addItem($label, $url, $translationParameters); } /** * @inheritdoc */ public function getName() { return 'breadcrumb_extension'; } }
Теперь регистрируем расширение в файле Resources/config/services.yml
нашего бандла с указанием параметров и подгрузкой зависимостей:
parameters: main.twig.breadcrumb_extension.home.route: homepage main.twig.breadcrumb_extension.home.label: Главная services: main.twig.breadcrumb_extension: class: App\MainBundle\Twig\Extension\BreadcrumbExtension arguments: - @white_october_breadcrumbs - @router - %main.twig.breadcrumb_extension.home.route% - %main.twig.breadcrumb_extension.home.label% tags: - { name: twig.extension }
Теперь у нас должна работать функция breadcrumb
. Попробуем добавить пункты и отобразить крошки в любом представлении:
{# src/App/AdminBundle/Resources/views/User/show.html.twig #} {% extends '::base.html.twig' %} {% block body %} {{ breadcrumb('Панель управления', path('admin_homepage')) }} {{ breadcrumb('Пользователи', path('admin_user')) }} {{ breadcrumb(entity.username) }} {% include '::breadcrumbs.html.twig' %} <h1>Пользователь {{ entity.username }}</h1> {% endblock %}
Перед заголовком отобразится цепочка:
Главная / Панель управления / Пользователи / Вася
Теперь перенесём include
из этого представления в наш шаблон:
{# app/Resources/views/base.html.twig #} ... <section class="main"> {% include '::breadcrumbs.html.twig' %} {% block body %}{% endblock %} </section> ...
и отнаследуемся от него:
{# src/App/AdminBundle/Resources/views/User/show.html.twig #} {% extends '::base.html.twig' %} {% block body %} {{ breadcrumb('Панель управления', path('admin_homepage')) }} {{ breadcrumb('Пользователи', path('admin_user')) }} {{ breadcrumb(entity.username) }} <h1>Пользователь {{ entity.username }}</h1> {% endblock %}
Но теперь при запуске мы хлебные крошки не увидим.
При использовании нативной шаблонизации в Yii Framework весь PHP-код исполняется сразу. То есть код исполняется по цепочке:
- Исполнение контроллера;
- Рендер представления и запись результата в буфер;
- Рендер шаблона с передачей результата из буфера в переменной
$content
.Рендер происходит «снизу вверх». Каждый дочерний шаблон сразу исполняется и передаётся как значение переменной в родительский.
В шаблонизаторе Twig исполнение производится иначе:
- Исполнение контроллера;
- Подъём по цепочке шаблонов наследников и сохранение списка блоков без их исполнения;
- Рендер главного шаблона с рендером блоков.
Это уже отложенный рендер. Процесс происходит «сверху вниз». То есть, если у нас все пять последовательных шаблонов содержат блок
body
, то Twig сразу дойдёт до главного шаблона и встретив тамbody
отрендерит только самый «нижний» блокbody
. Полная аналогия с перекрытием методов при наследовании классов: будет выполняться метод только из самого нижнего класса. Остальные четыре он пропустит (если, конечно же, там не будет командыparent()
).
Вспомним наш код:
<section class="main"> {% include '::breadcrumbs.html.twig' %} {% block body %}{% endblock %} </section>
Соответственно, рендер хлебных крошек у нас происходит перед исполнением блока body
(в котором мы добавляли пункты). А нам нужно поместить компоновку пунктов выше рендера. Это наша ошибка. Исправим её.
Добавим в базовый шаблон новый блок breadcrumbs
выше рендера крошек:
{# app/Resources/views/base.html.twig #} ... <section class="main"> {% block breadcrumbs %}{% endblock%} {% include '::breadcrumbs.html.twig' %} {% block body %}{% endblock %} </section> ...
и будем «засылать» добавление пунктов в этот блок:
{# src/App/AdminBundle/Resources/views/User/show.html.twig #} {% extends '::base.html.twig' %} {% block breadcrumbs %} {{ breadcrumb('Панель управления', path('admin_homepage')) }} {{ breadcrumb('Пользователи', path('admin_user')) }} {{ breadcrumb(entity.username) }} {% endblock%} {% block body %} <h1>Пользователь {{ entity.username }}</h1> {% endblock %}
Теперь Twig запустит все фрагменты в нужной нам последовательности и мы увидим крошки.
В итоге наш пример из жизни может быть таким:
Для всего сайта задан двухколоночный базовый шаблон с местом для хлебных крошек:
{# app/Resources/views/base.html.twig #} <!DOCTYPE html> <html> <head>...</head> <body> <header></header> <div id="container"> {% block content %} <section class="main main-column"> {% block breadcrumbs %}{% endblock%} {% include '::breadcrumbs.html.twig' %} {% block body %}{% endblock %} </section> <aside class="sidebar"> {% block sidebar %}{% endblock %} </aside> {% endblock content %} </div> <footer>...</footer> </body> </html>
Их рендер вынесен во вспомогательный файл:
{# app/Resources/views/breadcrumbs.html.twig #} {{ wo_render_breadcrumbs() }}
В бандле панели управления боковая колонка нам не нужна. Для этого в нём имеется одноколоночный шаблон, переопределяющий блок content
:
{# src/App/AdminBundle/Resources/views/layout.html.twig #} {% extends '::base.html.twig' %} {% block content %} <section class="main"> {% block breadcrumbs %}{% endblock%} {% include '::breadcrumbs.html.twig' %} {% block body %}{% endblock %} </section> {% endblock content %}
И от этого шаблона уже наследуются представления имеющихся в этом бандле контроллеров:
{# src/App/AdminBundle/Resources/views/User/show.html.twig #} {% extends 'AdminBundle::layout.html.twig' %} {% block breadcrumbs %} {{ breadcrumb('Панель управления', path('admin_homepage')) }} {{ breadcrumb('Пользователи', path('admin_user')) }} {{ breadcrumb(entity.username) }} {% endblock %} {% block body -%} <h1>Пользователь {{ entity.username }}</h1> {% endblock %}
Вот и всё. Теперь всё работает. И достаточно удобно. А если у вас дождь за окном, то не расстраивайтесь. Лето снова придёт:
Если пользуетесь другими интересными компонентами, то напишите их названия в комментариях. И удачных вам проектов!
Хм... Забавно видеть под статьёй рекламу хлебопекарни :)
Здравствуйте, я тоже пишу в свой блог о php и рядом стоящих технологиях. Возможно, вам будет интересно сотрудничать с моим блогом. http://plutov.by
Для этих целей использую KnpMenuBundle. В случае если изменяется структура сайта, хлебные крошки меняются автоматически.
Польза бандла только для хлебных крошек сомнительная, в данном случае. Ведь можно обойтись обычным инклюдом:
, а в шаблоне вывести переданный массив в параметрах
Здравствуйте. Может вы подскажете. У меня возникла проблема с производительностью composer при установке этого бандла в частности и в целом с инициализацией нового symfony-проекта. Что бы я не пытался сделать, увеличивал memory_limit для cli по максимуму, swap увеличивал, ставил длякомпосера флаг --prefer-source, один фиг, без swap'a вылетает Uncaught exception 'ErrorException' with message 'proc_open(): fork failed - Cannot allocate memory', со свопом жду ~пол часа и консоль отваливается. Проверял на двух разных VPS:
Еще есть один VPS на 2 ядра по 2.4 и 2 Гига мозгов. Щас там попробую. Ну и если уже там компосер будет тупить, то я тогда уже и не знаю что делать, придется отказываться от этого УГ и делать все по старинке ручками. Это ведь нереально требовать для установки копирования и сравнения какой-то пары сотен файлов размером не более 20 Мб под гиг оперативки и больше.
Как успехи на втором VPS?
В общем ситуация более мене прояснилась с тех пор. Бандл этот не ставился потому что его нужно было поставить в уже имеющийся проект с кучей уже установленных бандлов (~40). Вот он и не устанавливался. А для инициализации нового проекта symfony через composer.phar достаточно одного гига оперативки. Я тут кстати на php.su по этому поводу тему заводил http://forum.php.su/topic.php?forum=81&topic=1945&v=l#1414468483 Но потом, с ростом проекта 1 гига уже недостаточно. Сейчас для ~40 сторонних бандлов и примерно столько же своих используем VPS с 4 GB RAM на борту.
Даешь курс по Symfony!
Спасибо, помогло!