Продолжение вводной статьи по 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>
Теперь список новостей выглядит так:
В интерфейсе списка записей теперь есть форма поиска и постраничная навигация. Нужно отметить что значительная часть представленного кода уже была сгененирована автоматически, так что объем кода добработок не очень велик.