Как стать автором
Обновить

Внедряем Bootstrap 3 Datepicker в SonataAdminBundle

Время на прочтение8 мин
Количество просмотров18K
В этой маленькой заметке я расскажу о том, как подключить удобный datepicker в админку Symfony. По умолчанию datepicker в SonataAdminBundle выглядит так:



А мы его превратим в удобные и красивые контролы:



Те, кто еще мучаются с неудобным datepicker-ом, добро пожаловать под кат.

Если вам не нужен выбор времени, то вы может воспользоваться готовым решением, спасибо dmkuznetsov

Я не буду рассказывать о том, как установить SonataAdminBundle, об этом можно прочитать в этой статье. Я предполагаю, что у вас уже установлено приложение и админка. Ну чтож, приступим.

Тип поля datetime


Первое с чего стоит начать это создание нового поля формы как описано в документации. Его нужно обязательно создавать в namespace <vendor_name>\<bundle_name>\Form\Type\ иначе будет ругаться SensioLabsInsight при тестировании.

namespace Acme\Bundle\DemoBundle\Form\Type\Field;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\DataTransformerInterface; 
// Symfony >=2.8
//use Symfony\Component\Form\Extension\Core\Type\TextType;

class DateTime extends AbstractType implements DataTransformerInterface
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        // результатом заполнения форму является строка и ее необходимо конвертировать в \DateTime
        $builder->addModelTransformer($this);
    }

    public function transform($value)
    {
        return $value; // нужно для интерфейса DataTransformerInterface
    }

    public function reverseTransform($value)
    {
        // собственно конвертирование значения в \DateTime
        return $value instanceof \DateTime ? $value : new \DateTime($value);
    }

    public function buildView(FormView $view, FormInterface $form, array $options)
    {
        // объект даты нужно преобразовать в строку
        if ($form->getData() instanceof \DateTime) {
            $view->vars['value'] = $form->getData()->format('Y-m-d H:i');
        }
        // css класс для bootstrap форм
        $view->vars['attr']['class'] = 'form-control';
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        // без указания data_class не работает DataTransformer
        $resolver->setDefaults([
             'data_class' => \DateTime::class
        ]);
    }

    public function getParent()
    {
        // Symfony >=2.8
        //return TextType::class;
        // Symfony <2.8
        return 'text';
    }

    public function getName()
    {
        return 'datetime'; // мы потом перегрузим стандартный datetime
    }
}

Я не делал перенос стандартных опций datetime в новый класс за ненадобностью, но вы можете это сделать если вам это необходимо. В разделе Тип поля Time я опишу как это сделать на примере опции with_seconds.

Следующим пунктом нашей программы будет создание общего шаблон форм (темы для форм). В нем мы наследуемся от темы Sonata и переопределим шаблон даты. Шаблон сохраняем в файд Resources/views/Form/fields.html.twig . Можно выбрать и другой путь, но мне так привычней.

{% extends 'SonataAdminBundle:Form:form_admin_fields.html.twig' %}

{% block datetime_widget %}
{% spaceless %}
    <div class="input-group date form-field-datetime">
        <input type="text" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value }}" {% endif %}/>
        <span class="input-group-addon">
            <span class="glyphicon glyphicon-calendar"></span>
        </span>
    </div>
{% endspaceless %}
{% endblock datetime_widget %}

Класс form-field-datetime нам потом будет нужен для навешивания JavaScript. Теперь укажем Sonata что ей необходимо использовать другую тему для форм прописав в конфиге app/config/config.yml следующие строчки:

sonata_doctrine_orm_admin:
    templates:
        form: [ AcmeDemoBundle:Form:fields.html.twig ]

Не забываем создать сервис для нового поля формы:

    acme.demo.form.type.datetime:
        class: Acme\Bundle\DemoBundle\Form\Type\Field\DateTime
        public: false

Мы не создаем метку для сервиса как описано тут потому что мы не создаем новое поля и будем перегружать старое. По той же причине он нам не нужен в публичном доступе. Теперь приступим к перезаписи стандартных полей формы. Создадим компилятор для DI контейнера:

namespace Acme\Bundle\DemoBundle\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class FormTypePass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        $container->setAlias('form.type.datetime', 'acme.demo.form.type.datetime');
    }
}

Здесь мы указываем что form.type.datetime является псевдонимом для нашего, вновь созданного сериса acme.demo.form.type.datetime. Таким образом когда в формах мы будем создавать поле типа datetime будет использоваться наш сервис. Так мы меняем контрол не меняя код проекта. Теперь подключим компилятор в бандл:

namespace Acme\Bundle\DemoBundle

use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Acme\BundleDemoBundle\DependencyInjection\Compiler\FormTypePass;

class AcmeDemoBundle extends Bundle
{
    public function build(ContainerBuilder $container)
    {
        parent::build($container);
        $container->addCompilerPass(new FormTypePass());
    }
}

Сейчас datepicker уже имеет красивую и удобную форму, осталось только навесить JavaScript для открытия выпадающего окна с выбором даты.



Устанавливать мы будем Bootstrap 3 Datepicker который есть на packagist.org, за что им большое спасибо. Пропишем зависимость в composer.json:

{
    "require": {
        …
        "eonasdan/bootstrap-datetimepicker": "~4.17.37",
        …
    }
}

При такой установки пакета его удобней подключать через assetic что мы и сделаем. Прописываем в app/config/config.yml следующие строчки:

assetic:
    assets:
        admin-js:
            inputs:
                - '%kernel.root_dir%/../vendor/eonasdan/bootstrap-datetimepicker/build/js/bootstrap-datetimepicker.min.js'
                - '@AcmeDemoBundle/Resources/public/js/admin.js'
            output: js/admin.js

sonata_admin:
    templates:
        layout: AcmeDemoBundle:Admin:standard_layout.html.twig

Мы определили файл js/admin.js в который будет билдиться наш datepicker и JavaScript код который его инициализирует и навешивает на соответствующие поля формы. Этот файл будет лежать по адресу web/js/admin.js. Так же мы переопределили лайаут Sonata для того что бы подключить наш JavaScript. Давайте этим и займемся:

{% extends 'SonataAdminBundle::standard_layout.html.twig' %}

{% block javascripts %}
    {{ parent() }}
    <script src="{{ asset('js/admin.js') }}" type="text/javascript"></script>
{% endblock %}

Теперь мы создадим файл Resources/public/js/admin.js в котором обвяжем наши поля формы JavaScript-ом.
$(function(){
    $('.form-field-datetime').datetimepicker({
        format: 'YYYY-MM-DD HH:mm',
        locale: 'ru'
    });
});

Вот собственно и все. Выполняем сбору assetic и радуемся жизни:

app/console assetic:dump web --no-debug

Тип поля date


По аналогии создаем поле date с небольшими отличиями. Класс для поля формы:

namespace Acme\Bundle\DemoBundle\Form\Type\Field;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\DataTransformerInterface; 
// Symfony >=2.8
//use Symfony\Component\Form\Extension\Core\Type\TextType;

class Date extends AbstractType implements DataTransformerInterface
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->addModelTransformer($this);
    }

    public function transform($value)
    {
        return $value;
    }

    public function reverseTransform($value)
    {
        return $value instanceof \DateTime ? $value : new \DateTime($value);
    }

    public function buildView(FormView $view, FormInterface $form, array $options)
    {
        if ($form->getData() instanceof \DateTime) {
            $view->vars['value'] = $form->getData()->format('Y-m-d');
        }

        $view->vars['attr']['class'] = 'form-control';
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
             'data_class' => \DateTime::class
        ]);
    }

    public function getParent()
    {
        // Symfony >=2.8
        //return TextType::class;
        // Symfony <2.8
        return 'text';
    }

    public function getName()
    {
        return 'date';
    }
}

Шаблон:

{% block date_widget %}
{% spaceless %}
    <div class="input-group date form-field-date">
        <input type="text" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value }}" {% endif %}/>
        <span class="input-group-addon">
            <span class="glyphicon glyphicon-calendar"></span>
        </span>
    </div>
{% endspaceless %}
{% endblock date_widget %}

Сервис:

    acme.demo.form.type.date:
        class: Acme\Bundle\DemoBundle\Form\Type\Field\Date
        public: false

Добавляем псевдоним:

// ..
class FormTypePass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        // ..
        $container->setAlias('form.type.date', 'acme.demo.form.type.date');
    }
}

Ну и JavaScript:

$(function(){
    // .. 
    $('.form-field-date').datetimepicker({
        format: 'YYYY-MM-DD',
        locale: 'ru'
    });
});

Тип поля time


По аналогии с предыдущими, но с небольшими отличиями. В нашем проекте публикуются видео ролики и необходимо в админке указывать их продолжительность. Для этого мы используем поле time и выставляем ему опцию with_seconds в true. В новом поле формы нужно было сохранить эту функциональность.

namespace Acme\Bundle\DemoBundle\Form\Type\Field;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\DataTransformerInterface; 
// Symfony >=2.8
//use Symfony\Component\Form\Extension\Core\Type\TextType;

class Time extends AbstractType implements DataTransformerInterface
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->addModelTransformer($this);
    }

    public function transform($value)
    {
        return $value;
    }

    public function reverseTransform($value)
    {
        return $value instanceof \DateTime ? $value : new \DateTime($value);
    }

    public function buildView(FormView $view, FormInterface $form, array $options)
    {
        if ($form->getData() instanceof \DateTime) {
            // формат даты соответственно различается
            $view->vars['value'] = $form->getData()->format($options['with_seconds'] ? 'H:i:s' : 'H:i');
        }
        $view->vars['attr']['class'] = 'form-control'; 
        // сохраняем переменную для шаблона
        $view->vars['with_seconds'] = $options['with_seconds'];
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => \DateTime::class,
            'with_seconds' => false // по умолчанию опция выключена
        ]);
    }

    public function getParent()
    {
        // Symfony >=2.8
        //return TextType::class;
        // Symfony <2.8
        return 'text';
    }


    public function getName()
    {
        return 'time';
    }
}

Шаблон:

{% block time_widget %}
{% spaceless %}
    <div class="input-group date form-field-time" data-with-seconds="{{ with_seconds == true ? 1 : 0 }}">
        <input type="text" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value }}" {% endif %}/>
        <span class="input-group-addon">
            <span class="glyphicon glyphicon-time"></span>
        </span>
    </div>
{% endspaceless %}
{% endblock time_widget %}

Сервис:

    acme.demo.form.type.time:
        class: Acme\Bundle\DemoBundle\Form\Type\Field\Time
        public: false

Добавляем псевдоним:

// ..
class FormTypePass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        // ..
        $container->setAlias('form.type.time', 'acme.demo.form.type.time');
    }
}

JavaScript будет немного отличатся:

$(function(){
    // .. 
    $('.form-field-time') .each(function () {
        var el = $(this),
            options = {locale: 'ru'};
        if (el.data('with-seconds') == 1) {
            options.format = 'HH:mm:ss';
        } else {
            options.format = 'HH:mm';
        }
        el.datetimepicker(options);
});

Заключение


В место заключения скажу, что есть очень интересная библиотека ClockPicker для выбора времени. Если она вас заинтересует, то вы с легкостью сможете подключить ее по моим примерам.
Теги:
Хабы:
Всего голосов 12: ↑9 и ↓3+6
Комментарии20

Публикации

Истории

Работа

PHP программист
106 вакансий

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань