Продолжение вводной статьи по Symfony 2. В первой части было описан процесс модификации формы редактирования записей, во второй части будем модифицировать интерфейс списка записей. В заготовке шаблона и контроллера списка записей, которую генерирует команда doctrine:generate:crud как минимум не хватает формы поиска записей и постраничной навигации.
Начнем с добавления формы поиска. Чтобы создать форму поиска нужно создать класс доменного объекта, в котором будут храниться параметры, по которым производится поиск. Например для списка новостей это будут «поиск по подстроке», выбор из списка категорий и поиск по дате новости. Также нужно создать класс формы поиска. Место размещения подобных классов в Symfony не регламентировано, я для поисковых классов использую пространства имен

В доменный объект переносятся данные после отправки формы. Затем данные из этого объекта используются при составлении условий отбора записей в списке.
Т.к. для доменного объекта, используемого для хранения параметров поиска, не прописана связь с таблицами БД, для поля category (выпадающий список) нужно явно прописывать сущность, которая будет использоваться для наполнения списка. Поля, в которые вводятся даты, отображаются как текстовые поля (параметр widget=single_text). К текстовому полю добавляется атрибут class=date, который используется как селектор в jQueryUI (дальше описано в шаблоне). В форме отключена CSRF-защита чтобы в URL после отправки формы не было дополнительного параметра.
В контроллере Test/NewsBundle/Controller/NewsController в метод indexAction() добавлям использование созданных классов.
Далее модифицируем шаблон Test/NewsBundle/Resources/views/News/index.html.twig в который добавлем код отображения формы.
Теперь список новостей выглядит так:

Теперь нужно добавить постраничную навигацию. Можно конечно написать свой код, но гораздо проще воспользоваться готовым бандлом, например KnpPaginatorBundle. Для установки этого бандла нужно добавить в файл deps:
Для загрузки нужно в командной строке набрать:
Скрипт bin/vendors использует Git, для загрузки новых бандлов он должен быть установлен в вашей системе.
В файл app/autoload.php нужно добавить:
В файл app/AppKernel.php
В контроллере Test/NewsBundle/Controller/NewsController в метод indexAction() добавлям использование KnpPaginator после формирования запроса к БД в объекте QueryBuilder. Вместо стандартного списка записей в шаблон возвращаем объект класса Paginator.
В шаблоне Test/NewsBundle/Resources/views/News/index.html.twig под таблицей со списком записей добавляем вызов тэга paginate:
Теперь список новостей выглядит так:

В интерфейсе списка записей теперь есть форма поиска и постраничная навигация. Нужно отметить что значительная часть представленного кода уже была сгененирована автоматически, так что объем кода добработок не очень велик.
Классы для формы поиска
Начнем с добавления формы поиска. Чтобы создать форму поиска нужно создать класс доменного объекта, в котором будут храниться параметры, по которым производится поиск. Например для списка новостей это будут «поиск по подстроке», выбор из списка категорий и поиск по дате новости. Также нужно создать класс формы поиска. Место размещения подобных классов в Symfony не регламентировано, я для поисковых классов использую пространства имен
- {Название банла}/Entity/Search/{Имя сущности} — для параметров поиска
- {Название банла}/Form/Search/{Имя сущности} — для формы поиска

Доменный объект c параметрами поиска новостей
В доменный объект переносятся данные после отправки формы. Затем данные из этого объекта используются при составлении условий отбора записей в списке.
<?php namespace Test\NewsBundle\Entity\Search; use Symfony\Component\Validator\Constraints as Assert; class News { /** * Строка поиска * @var string */ public $search; /** * Идентификатор категории новостей * @var integer */ public $category; /** * Дата с которой искать новости * @var DateTime * @Assert\DateTime */ public $dateFrom; /** * Дата, до которой искать новости * @var DateTime * @Assert\DateTime */ public $dateTo; }
Форма поиска новостей
Т.к. для доменного объекта, используемого для хранения параметров поиска, не прописана связь с таблицами БД, для поля category (выпадающий список) нужно явно прописывать сущность, которая будет использоваться для наполнения списка. Поля, в которые вводятся даты, отображаются как текстовые поля (параметр widget=single_text). К текстовому полю добавляется атрибут class=date, который используется как селектор в jQueryUI (дальше описано в шаблоне). В форме отключена CSRF-защита чтобы в URL после отправки формы не было дополнительного параметра.
<?php namespace Test\NewsBundle\Form\Search; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilder; class NewsType extends AbstractType { public function buildForm(FormBuilder $builder, array $options) { $builder->add('search', 'search', array('required' => false, 'label' => 'Поиск ')) ->add('category', 'entity', array( 'label' => 'Категория', 'required' => false, 'class' => 'Test\\NewsBundle\\Entity\\NewsCategory')) ->add('dateFrom', 'date', array( 'label' => 'с', 'widget' => 'single_text', 'format' => 'yyyy-MM-dd', 'attr' => array('class' => 'date'), 'required' => false)) ->add('dateTo', 'date', array( 'label' => 'по', 'widget' => 'single_text', 'format' => 'yyyy-MM-dd', 'attr' => array('class' => 'date'), 'required' => false)); } public function getDefaultOptions(array $options) { return array( 'csrf_protection' => false, ); } function getName() { return 'searchorg'; } }
Контроллер
В контроллере Test/NewsBundle/Controller/NewsController в метод indexAction() добавлям использование созданных классов.
.. use Test\NewsBundle\Entity\Search\News as SearchNews; use Test\NewsBundle\Form\Search\NewsType as SearchNewsType; /** * News controller. * * @Route("/news") */ class NewsController extends Controller { /** * Список новостей * * @Route("/", name="news") * @Template() */ public function indexAction() { //Создаем доменный объект, в котором хранятся параметры поиска $searchNews = new SearchNews(); //Создаем форму поиска $searchForm = $this->createForm(new SearchNewsType(), $searchNews); $searchForm->bindRequest($this->getRequest()); //Создаем построитель запросов Doctrine $qb = $this->getDoctrine()->getEntityManager()->getRepository('TestNewsBundle:News') ->createQueryBuilder('n'); //Добавляем к запросу left join c сущностью "Категория" //при выводе в списке названия категории нового запроса не будет $qb->select('n,c')->leftJoin('n.newsCategory', 'c')->orderBy('n.pubDate'); //Если есть строка поиска - добавляем ИЛИ условие LIKE пои полям title, announce, text if ($searchNews->search) { foreach (array('n.title', 'n.announce', 'n.text') as $field) $qb->orWhere($qb->expr()->like($field, $qb->expr()->literal('%' . $searchNews->search . '%'))); } //Категория новостей if ($searchNews->category) $qb->andWhere($qb->expr()->eq('c.id', $searchNews->category)); //Дата С которой искать новости if ($searchNews->dateFrom) $qb->andWhere($qb->expr()->gt('n.pubDate', $qb->expr()->literal($searchNews->dateFrom->format('Y-m-d')))); //Дата До которой искать новости if ($searchNews->dateTo) $qb->andWhere($qb->expr()->lt('n.pubDate', $qb->expr()->literal($searchNews->dateTo->format('Y-m-d')))); $entities = $qb->getQuery()->getResult(); return array('entities' => $entities, 'search_form' => $searchForm->createView()); } .... }
Шаблон формы поиска и списка записей
Далее модифицируем шаблон Test/NewsBundle/Resources/views/News/index.html.twig в который добавлем код отображения формы.
{% extends '::base.html.twig' %} {% block body %} <h1>Новости</h1> {% form_theme search_form 'form_table_layout.html.twig' %} <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script> <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/jquery-ui.min.js"></script> <link rel="stylesheet" href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.0/themes/redmond/jquery-ui.css"> <script> $(function() { $("form input.date").datepicker({ dateFormat: 'yy-mm-dd'}); }); </script> <form action="{{ path('news') }}" method="get"> {{ form_errors(search_form) }} <table> {{ form_row(search_form.search) }} {{ form_row(search_form.category)}} <tr> <td colspan="2">Дата новости</td> </tr> {{ form_row(search_form.dateFrom)}} {{ form_row(search_form.dateTo)}} {{ form_rest(search_form) }} </table> <button type="submit">Искать</button> </form> <table class="records_list"> <thead> <tr> <th>Id</th> <th>Название</th> <th>Анонс</th> <th>Категория</th> <th>Дата</th> <th>Действия</th> </tr> </thead> <tbody> {% for entity in entities %} <tr> <td><a href="{{ path('news_show', { 'id': entity.id }) }}">{{ entity.id }}</a></td> <td>{{ entity.title }}</td> <td>{{ entity.announce }}</td> <td>{{ entity.newsCategory }}</td> <td>{{ entity.pubDate|date('Y-m-d') }}</td> <td> <ul> <li><a href="{{ path('news_show', { 'id': entity.id }) }}">Смотреть</a></li> <li><a href="{{ path('news_edit', { 'id': entity.id }) }}">Редактировать</a></li> </ul> </td> </tr> {% endfor %} </tbody> </table> <ul> <li> <a href="{{ path('news_new') }}"> Создать новость </a> </li> </ul> {% endblock %}
Теперь список новостей выглядит так:

Установка бандла для постраничной навигации
Теперь нужно добавить постраничную навигацию. Можно конечно написать свой код, но гораздо проще воспользоваться готовым бандлом, например KnpPaginatorBundle. Для установки этого бандла нужно добавить в файл deps:
[knp-components] git=http://github.com/KnpLabs/knp-components.git [KnpPaginatorBundle] git=http://github.com/KnpLabs/KnpPaginatorBundle.git target=bundles/Knp/Bundle/PaginatorBundle
Для загрузки нужно в командной строке набрать:
php bin/vendors install --reinstall
Скрипт bin/vendors использует Git, для загрузки новых бандлов он должен быть установлен в вашей системе.
В файл app/autoload.php нужно добавить:
$loader->registerNamespaces(array( 'Knp\\Component' => __DIR__.'/../vendor/knp-components/src', 'Knp\\Bundle' => __DIR__.'/../vendor/bundles', // ... ));
В файл app/AppKernel.php
public function registerBundles() { return array( // ... new Knp\Bundle\PaginatorBundle\KnpPaginatorBundle(), // ... ); }
Модификация контроллера
В контроллере Test/NewsBundle/Controller/NewsController в метод indexAction() добавлям использование KnpPaginator после формирования запроса к БД в объекте QueryBuilder. Вместо стандартного списка записей в шаблон возвращаем объект класса Paginator.
$paginator = $this->get('knp_paginator'); $pagination = $paginator->paginate( $qb->getQuery(), $this->get('request')->query->get('page', 1)/*page number*/, 10/*limit per page*/ ); return array('entities' => $pagination, 'search_form' => $searchForm->createView());
Модификация шаблона
В шаблоне Test/NewsBundle/Resources/views/News/index.html.twig под таблицей со списком записей добавляем вызов тэга paginate:
<div id="navigation"> {{ entities.render()|raw }} </div>
Теперь список новостей выглядит так:

В интерфейсе списка записей теперь есть форма поиска и постраничная навигация. Нужно отметить что значительная часть представленного кода уже была сгененирована автоматически, так что объем кода добработок не очень велик.
