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

    Продолжение вводной статьи по 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>
    

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



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

    Комментарии 16

      +10
      Теперь нужно добавить постраничную навигацию. Можно конечно написать свой код, но гораздо проще воспользоваться готовым бандлом, например KnpPaginatorBundle. Для установки этого бандла нужно добавить в файл deps

      [...]

      Да, этот бандл тянет за собой Zend Framework, но т.к. ZF вещь полезная, то пусть будет


      Это гениально!
        +2
        Автор просто не в теме: ipsum.knplabs.com/pagerfanta ;-)
          0
          Для симфонии это в порядке вещей. У нас в симфоневском проекте подключено 27 библиотек общим размером 131 Мб.
            +1
            А как вы управляетесь с такой кучей разнообразного кода?
              0
              Большинство классов, естественно, не используется — части больших библиотек.
              Тот же ZF нужно притянуть весь, для использования некоторого количества классов.

              Так же в симфонии очень просто инициализировать любые библиотеки через сервисы, а в контроллерах запрашивать их из контейнера, например $this->get('doctrine');
                0
                Да не, я про менеджмент кода, обучение новых сотрудников, документирование и все такое.

                Ну, например, приходит к вам новый сотрудник а вы ему «вот основное приложение, а вот доступ к репозиторию, разбирайся». И он начинает разбираться и трагическим голосом спрашивает «а что из этого используется, а что нет?».
                  0
                  А вы всегда новых сотрудников обучаете вслепую натравливая их на все *.php файлы проекта? Типа распечатываете на принтере и заставляете читать?

                  Разбираешься с фреймворком, понимаешь как работает фронт-контроллер и роутинг. По роутингу вытаскиваешь класс и метод контроллера, где и смотришь что и как используется. Абсолютно не вижу проблемы.
                    0
                    Принято знакомить сотрудников с API, методами классов, какими-то паттернами кодинга, принятыми в компании. У вас же куча различных библиотек и, скорее всего, фреймворков, которые порвут мозг любому программисту, потому что требуют не единого подхода.
                      0
                      Чего требуют? Есть класс искомый (необходимый для выполнения конкретной задачи), у него есть конструктор, в который надо передать зависимости (объекты других классов) попутно заполняя их параметрами. Это один общий подход для всех ООП языков программирования. Нужен класс — инициализируем и используем. Какие «не единые» подходы? О чем вы говорите вообще? О том что человек не знающий ООП или базовых паттернов не сможет в этом разобраться? Так это не программист тогда и никому он такой не нужен, включая нас.
                    0
                    Обучение происходит постепенно, от одного зависимого класса к другому, по мере поступления задач.
                    0
                    ZF не обязетально тянуть весь для исопльзования какой-то его компоненты, обычно достаточно несколько зависимых классов.
                      0
                      Все внешние библиотеки мы подключаем используя git submodules, а там уже не выберешь конкретные файлы.

                      Либо добавлять куски библиотек в основной репозиторий либо добавлять ссылку на полную библиотеку. Для меня выбор очевиден.
              +1
              Если по сабжу, мы используем github.com/makerlabs/PagerBundle
                +1
                Как я счастлив, что мы с симфони перешли на yii
                  +2
                  Простите, но симфони правда упрощает жизнь? Глядя на вермишель в каждом туториале и на офф.сайте не подумаешь, что это сделано для людей
                    +1
                    Если из контроллеров вынести создание форм и запросы к базе то выглядит намного лучше. А аутентификация/авторизация в симфони2 вообще сказка:)

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое