Хлебные крошки в 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-код исполняется сразу. То есть код исполняется по цепочке:

  1. Исполнение контроллера;
  2. Рендер представления и запись результата в буфер;
  3. Рендер шаблона с передачей результата из буфера в переменной $content.

Рендер происходит «снизу вверх». Каждый дочерний шаблон сразу исполняется и передаётся как значение переменной в родительский.

В шаблонизаторе Twig исполнение производится иначе:

  1. Исполнение контроллера;
  2. Подъём по цепочке шаблонов наследников и сохранение списка блоков без их исполнения;
  3. Рендер главного шаблона с рендером блоков.

Это уже отложенный рендер. Процесс происходит «сверху вниз». То есть, если у нас все пять последовательных шаблонов содержат блок 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 %}

Вот и всё. Теперь всё работает. И достаточно удобно. А если у вас дождь за окном, то не расстраивайтесь. Лето снова придёт:

Если пользуетесь другими интересными компонентами, то напишите их названия в комментариях. И удачных вам проектов!

Другие статьи

В прошлых частях мы создали новое приложение на Yii2, доработали конфигурационный файлы и разбили приложение на модули. Сегодня мы, как было обещано ранее, перенесём хранение пользователей в базу данных и добавим возможность регистрации и восстановления пароля.

В комментариях и в обратной связи повышенный интерес проявляется к Git и Composer от тех, кто не сталкивался с ними ранее. Статьи и инструкции написаны, но их сразу мало кто поймёт, так как в ходе изучения вопросы задать некому. Обычная проблема документации. Команды выучить можно, но что именно с ними делать и как правильно применять – понять сложно.

Продолжаем разработку нашего чудо-сервиса на Yii2. На прошлом уроке мы создали через Composer новый проект и дополнили его раздельной системой конфигурационных файлов. Сегодня мы внедрим в проект модульную структуру и немного лучше познакомимся с базовыми настройками и некоторыми возможностями авторефакторинга в PhpStorm IDE.

Многие сайты и некоторые серверные приложения позволяют обращаться к ним по сети посредством стандартизированного протокола SOAP. Они выкладывают некий открытый API, через который позволяют вызывать некоторые их методы с передачей параметров. При этом масштабы таких систем могут быть совершенно разными: получение прогноза погоды, сеть 1C крупной организации или система бронирования авиабилетов.

Комментарии

 

Дмитрий Елисеев

Хм... Забавно видеть под статьёй рекламу хлебопекарни :)

Ответить

 

Саша П.

Здравствуйте, я тоже пишу в свой блог о php и рядом стоящих технологиях. Возможно, вам будет интересно сотрудничать с моим блогом. http://plutov.by

Ответить

 

Александр

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

{{ include('breadcrumbs.html.twig', {
    'items' : {
        'Панель управления': path('admin_homepage'), 
        'Пользователи': path('admin_user'),
        entity.username ~ '': null
    }
}) }}


, а в шаблоне вывести переданный массив в параметрах

Ответить

 

nkl90

Здравствуйте. Может вы подскажете. У меня возникла проблема с производительностью composer при установке этого бандла в частности и в целом с инициализацией нового symfony-проекта. Что бы я не пытался сделать, увеличивал memory_limit для cli по максимуму, swap увеличивал, ставил длякомпосера флаг --prefer-source, один фиг, без swap'a вылетает Uncaught exception 'ErrorException' with message 'proc_open(): fork failed - Cannot allocate memory', со свопом жду ~пол часа и консоль отваливается. Проверял на двух разных VPS:

  • CPU - 2.4GHz 1 ядро, ОЗУ - 512 МБ (здесь есть swap на 512 мб)
  • CPU - 1.7GHz 1 ядро, ОЗУ - 1 ГБ (здесь нет swap)


Еще есть один VPS на 2 ядра по 2.4 и 2 Гига мозгов. Щас там попробую. Ну и если уже там компосер будет тупить, то я тогда уже и не знаю что делать, придется отказываться от этого УГ и делать все по старинке ручками. Это ведь нереально требовать для установки копирования и сравнения какой-то пары сотен файлов размером не более 20 Мб под гиг оперативки и больше.

Ответить

 

Дмитрий Елисеев

Как успехи на втором VPS?

Ответить

 

nkl90

В общем ситуация более мене прояснилась с тех пор. Бандл этот не ставился потому что его нужно было поставить в уже имеющийся проект с кучей уже установленных бандлов (~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 на борту.

Ответить

Оставить комментарий

Войти | Завести аккаунт


(никто не увидит)



Можно использовать теги <p> <ul> <li> <b> <i> <a> <pre>