Pull to refresh

Создание CRUD приложения на Symfony 2, часть 2

Reading time 6 min
Views 8.1K
Продолжение вводной статьи по Symfony 2. В первой части было описан процесс модификации формы редактирования записей, во второй части будем модифицировать интерфейс списка записей. В заготовке шаблона и контроллера списка записей, которую генерирует команда doctrine:generate:crud как минимум не хватает формы поиска записей и постраничной навигации.

Классы для формы поиска


Начнем с добавления формы поиска. Чтобы создать форму поиска нужно создать класс доменного объекта, в котором будут храниться параметры, по которым производится поиск. Например для списка новостей это будут «поиск по подстроке», выбор из списка категорий и поиск по дате новости. Также нужно создать класс формы поиска. Место размещения подобных классов в 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>

Теперь список новостей выглядит так:



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

Articles