Symfony 4 - проблема производительности в форме с окном выбора со многими опциями

Я построил форму с selectbox (EntityType) с большим количеством вариантов (около 50 000):

->add(
    'destination', EntityType::class, array(
        'label' => 'à', 
        'multiple' => false,
        'required' => false,
        'class' => Stop::class,
        'attr' => array(
            'class' => 'form-control',
        )
    )
);

Я сталкиваюсь с большой проблемой производительности: десятки секунд до отображения списка, когда я нажимаю на него.

Я думаю, что решение было бы вначале загрузить только несколько элементов (например, сотню), а затем использовать Ajax для запроса БД, когда пользователь начинает печатать (я использую поле select2 с полем поиска). Проблема в том, что я не могу найти наиболее эффективный способ сделать это через Symfony.

Я видел, что функциональность choice_loader могла бы это сделать, но нет подробной документации: https://symfony.com/blog/new-in-symfony-3-2-lazy-loading-of-form-choices

Было бы здорово, если кто-то может помочь в этом,

Спасибо за вашу поддержку,

1 ответ

Когда я сталкиваюсь с такой проблемой, я использую другой подход. Если опция выбора будет иметь более 20 записей, поэтому я изменяю ее на ввод текста с автозаполнением.

Шаги:

  1. Установите хорошую библиотеку Javascript для автозаполнения, такую ​​как jquery-typeahead

    Мне нравится использовать Wepack Encore в Symfony. С Webpack, Npm и Yarn установка очень проста

    yarn add jquery-typeahead --dev
    

    Вам нужно будет бежать yarn run encore dev после установки.

  2. Создайте новый FieldType для вашей формы, чтобы заменить EntityType

    Предположим, нам нужно создать форму регистрации с полем city. Поведение по умолчанию будет использовать EntityType и покажет вариант выбора со всеми городами. Чтобы изменить его на автозаполнение, давайте создадим еще один FieldType.

    <?php    
    // src/Form/Type/AutocompleteCityType.php
    namespace App\Form\Type; 
    use Doctrine\ORM\EntityManagerInterface; 
    use Symfony\Component\Form\AbstractType; 
    use Symfony\Component\Form\Extension\Core\Type\SearchType; 
    use Symfony\Component\OptionsResolver\OptionsResolver; 
    class AutocompleteCityType extends AbstractType
    {
    
        public function configureOptions(OptionsResolver $resolver)
        {
            $resolver->setDefaults(array(
                'attr' => ['autocomplete' => 'off']
            ));
        }
    
        public function getParent()
        {
            return SearchType::class;
        }
    }
    

ПРИМЕЧАНИЕ. В приведенном выше коде я расширяю SearchType::class, который является поиском типа ввода (HTML 5).

Наш новый тип поля можно использовать в наших формах, но это просто другое строковое поле. Не будет работать правильно, чтобы заменить EntityType. Нам нужно преобразовать эту строку в сущность. Итак, нам нужен DataTransformer

  1. Создать город в строку DataTransformer

    <?php
    // src/Form/DataTransformer/CityToStringTransformer.php    
    namespace App\Form\DataTransformer;                
    use App\Entity\City; // Pay attention to use your Entities correctly
    use Symfony\Component\Form\DataTransformerInterface;
    use Symfony\Component\Form\Exception\TransformationFailedException;
    
    class CityToStringTransformer implements DataTransformerInterface
    {
        private $entityManager;
    
        public function __construct(EntityManagerInterface $entityManager)
        {
            $this->entityManager = $entityManager;
        }
    
        /**
         * Transforms an object (City) to a string.
         *
         * @param  City|null $city
         * @return string
         */
        public function transform($city)
        {
            if (null === $city) {
                return '';
            }
    
            return $city->getSomethingUnique()
        }
    
        /**
         * Transforms a string to an object (city).
         *
         * @param  string $somethingUnique
         * @return City|null
         * @throws TransformationFailedException if object (city) is not found.
         */
        public function reverseTransform($somethingUnique)
        {
            // empty City? It's optional, so that's ok
            if (!$somethingUnique) {
                return;
            }        
    
            $city = $this->entityManager
                    ->getRepository(City::class)
                    ->findByThatSomethingUnique($somethingUnique);                
    
            if (null === $city) {
                // causes a validation error
                // this message is not shown to the user
                // see the invalid_message option
                throw new TransformationFailedException(sprintf(
                    'The city "%s" cannot be found!',
                    $somethingUnique
                ));
            }
    
            return $city;
        }
    }
    

Примечание: строка должна быть своего рода уникальным ключом для правильной работы и должна хорошо отображаться при автозаполнении и заполнении поля (например, [CityCode] CityName). DataTransformation не может возвращать более одного результата findByThatSomethingUnique() метод.

Почти сделано. Мы можем использовать оба класса в нашем FormType для замены EntityType.

  1. Использование на FormType

    // src/Form/MyFormType.php
    class MyFormType extends AbstractType
    {
        private $cityTransformer;
    
        public function __construct(CityToStringTransformer $cityTransformer)
        {
            $this->cityTransformer = $cityTransformer;
        }
    
        public function buildForm(FormBuilderInterface $builder, array $options)
        {
            /* @var $myEntity MyEntity */
            $myEntity = $builder->getData();
            $builder
                ->add('city', AutocompleteCityType::class, [
                    'label' => 'Custom City Label',
                ]) 
            // (...) Other fields (...)
            ;
    
            $builder->get('city')
                ->addModelTransformer($this->cityTransformer);
    
        }
    

С кодом до здесь форма будет отображаться правильно, но заголовок должен быть правильно настроен.

Вы можете создать новый тип веточек для этого нового типа поля. Код ниже должен находиться в вашей пользовательской форме form_theme

  1. Блок Twig

    {% block autocomplete_city_widget %}
        {% spaceless %}
            <div class="typeahead__container">
                <div class="typeahead__field">
                    <div class="typeahead__query">
                        {{ form_widget(form) }}
                    </div>
                </div>
            </div>
        {% endspaceless %}
    {% endblock %}
    

ПРИМЕЧАНИЕ. Приведенный выше код Twig относится к jquery-typeahead и работает только с полем AutocompleteCityType. Если вы устанавливаете другую библиотеку или изменяете имя класса FieldType, измените его правильно. Также обратите внимание на имена форм, которые изменяют имя блока для визуализации.

Последнее, что нужно написать javascript для typeahead, получить записи.

  1. Typeahead Javascript

    jQuery.typeahead({
        input: "#myForm_city", // Related to FormName and Autocomplete Field Name
        minLength: 1,
        maxItem: 20,
        order: "asc",
        dynamic: true,
        delay: 500,
        backdrop: { "background-color": "#eeeeee" },
        template: "<small style='color:#999;'>{{ '[{{citycode}}] {{cityname}}' }}</small>", // remember that this is a Twig template...
        emptyTemplate: "No results for typed string",
        source: {
            city: {
                display: ["citycode", "cityname"],
                ajax: function (query) {
                    return {
                        type: "POST",
                        url: '{{ path('controller_with_city_list_json_response') }}',
                        path: "city",
                        data: {
                            "q": "{{ '{{query}}' }}",
                            "length" : "40",
                        },
                        callback: {
                            done: function (res) {
                                var d = {};
                                d.city = [];
                                jQuery(res.data).each(function(index, value) {
                                    d.city.push(value.city);
                                });
                                return d;
                            }
                        }
                    }
                }
            }
        },
        callback: {
            onClickAfter: function (node, a, item, event) {
                event.preventDefault();
                jQuery(node).val("[" + item.citycode + "] " + item.cityname);
            }
        },
        debug: false
    });
    

ПРИМЕЧАНИЕ. Приведенный выше код Typeahead требует ответа json в формате

{"data":
    [
        {"city":
            {"cityname":"City Name X", "citycode": "NXNX"}
        },
        {"city":
            {"cityname":"City Name Y", "citycode": "NYNY"}
        }
    ]
 }
Другие вопросы по тегам