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 записей, поэтому я изменяю ее на ввод текста с автозаполнением.
Шаги:
Установите хорошую библиотеку Javascript для автозаполнения, такую как jquery-typeahead
Мне нравится использовать Wepack Encore в Symfony. С Webpack, Npm и Yarn установка очень проста
yarn add jquery-typeahead --dev
Вам нужно будет бежать
yarn run encore dev
после установки.Создайте новый 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
Создать город в строку 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.
Использование на 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
Блок 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, получить записи.
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"}
}
]
}