У меня есть Zend\Form
и соответствующий класс сущности Doctrine, где объект как отношение ManyToMany
к другому объекту. Точнее, пользователь должен иметь возможность выбирать одно или несколько имен из таблицы данных, содержащей 12 000 имен - слишком много для обычного элемента SELECT.
В более ранней итерации этого проекта, в котором использовался ZF1, у меня был элемент MultiSelect
с нулевыми параметрами, который я просто никогда не отображал. Вместо этого я создал текстовое поле автозаполнения с JQueryUI для динамической вставки человекочитаемых имен и идентификаторов в виде скрытых элементов. Отлично.
Я посмотрел на Zend\Form\Element\Collection
но документы говорят, что вы не можете обновить его с меньшим количеством элементов, чем вы начали с этого, то есть, если в момент гидратации формы обновления у вас есть 2 whatevers, вы должны отправить по крайней мере 2 удара. Этого не будет.
В другом месте я с радостью использую DoctrineModule\Form\Element\ObjectSelect
но это не похоже на правильный выбор для этого случая.
Прежде чем я уйду и попытаюсь использовать ту же технику, которую использовал с ZF1, я был бы рад, если бы кто-нибудь мог дать мне лучшую идею.
Мой ответ: нет экзотических, секретных знаний. Используйте автозаполнение и базу данных, как и для любых других инструментов/библиотек.
Несколько описать мой прецедент: пользователи отправляют запросы на переводчика для будущего судебного разбирательства. Они должны указать имя (имена) ответчика (-ов), среди других данных (например, язык). В нашей базе уже более 12 тыс. Таких имен, и мы повторно используем повторяющиеся имена (IOW, сущность представляет собой собственное имя человека, а не человека).
Теперь для некоторых фрагментов кода. В передней части сценария вида:
<?php
$this->headScript()->appendFile('/dev-requests/js/jquery-ui.min.js');
$this->headScript()->appendFile('/dev-requests/js/defts.js');
// stuff omitted...
// within the form:
// $element is a \Zend\Form\Element\Select with attribute 'multiple' => 'multiple', zero options,
// and it hidden via css because no one needs to see it
<div class="form-group">
<label class="control-label col-sm-3" for="<?=$element->getName()?>"><?=$element->getLabel()?></label>
<div class="col-sm-9"><?= $this->formElement($element)?><?= $this->formElementErrors($element) ?>
<?php if ($this->defendants):
// if we are an update (as opposed to create) action, our controller updateAction
// will have set $this->defendants to a (possibly empty) array of 'defendantName' entities
foreach($this->defendants as $deft): ?>
<div id="deft-div-<?=$deft->getDeftId()?>"><span class="remove-div"><a href="#">[x]</a></span>
<?=$deft->getFullname()?>
<input value="<?=$deft->getDeftId()?>" name="request-fieldset[defendantNames][]" type="hidden">
</div>
<?php
endforeach;
endif;
?>
</div>
</div>
И некоторые Javascript из defts.js
, внутри нашего document.ready() обратного вызова:
// the autocomplete textfield itself
$('#deft-select').after(
$('<input>').attr({id:'deftname-autocomplete',size:25})
);
// for deleting a name from the form
($('form').on('click','span.remove-div',function(event){
event.preventDefault();
$(this).parent().slideUp(function(){$(this).remove();});
}));
$('#deftname-autocomplete').autocomplete({
source : '/dev-requests/defendants/autocomplete',
select: function( event, ui ) {
// add a human-readable label and hidden form element
$(this).val('');
var elementName = $('#deft-select').attr('name');
var deftName = ui.item.label;
var deft_id = ui.item.value;
if ($( '#deft-div-'+ deft_id ).length) {
return false; // already exists
}
var div = $(this).closest('div');
div.append(
$('<div/>').attr({id: "deft-div-"+ deft_id})
.html([
'<span class="remove-div"><a href="#">[x]</a></span> ' + deftName,
$('<input/>').attr({type:'hidden',name:elementName}).val(deft_id)
])
);
return false;
}
});
В нашем контроллере:
public function autocompleteAction()
{
$term = $this->getRequest()->getQuery('term');
if (! $term) { return false; }
/**
* @var $em Doctrine\ORM\EntityManager
*/
$em = $this->getServiceLocator()->get('entity-manager');
/**
* @var $repo Application\Entity\DefendantNameRepository
*/
$repo = $em->getRepository('Application\Entity\DefendantName');
$data = json_encode($repo->autocomplete($term));
$response = $this->getResponse();
$response->getHeaders()->addHeaders(['Content-type'=>'application/json;charset=UTF-8']);
return $response->setContent($data);
}
В нашем пользовательском репозитории Doctrine Application\Entity\DefendantNameRepository:
/**
* return array of value/label for autocompletion via xhr
* @param string $term name
* @param int limit max number of records to return
*
* $term is expected to be proper name in the format la[stname][,f[irstname]]
*/
public function autocomplete($term, $limit = 20) {
/**
* @var $connection Doctrine\DBAL\Connection
*/
$connection = $this->getEntityManager()->getConnection();
list($lastname,$firstname) = $this->parseName($term);
if (! strstr($lastname,'-')) {
$where = 'lastname LIKE '.$connection->quote("$lastname%");
} else {
// they frequently insert gratuitous hyphens between the
// paternal and maternal surnames of Spanish-speaking people
$lastname = str_replace('-','( |-)',$lastname);
$where = 'lastname REGEXP '.$connection->quote("^$lastname");
}
if ($firstname) {
$where .= " AND firstname LIKE ".$connection->quote("$firstname%");
} else {
$where .= " AND firstname <> '' "; // some old records have no firstname, but we don't like that
}
$sql = 'SELECT CONCAT(lastname, ", ",firstname) AS label, deft_id AS value
FROM deft_names WHERE '.$where . " ORDER BY lastname, firstname LIMIT $limit ";
return $connection->fetchAll($sql);
}
... и этот маленький помощник, в другом месте нашего хранилища:
/**
* parses first and last names out of $name. expected format is
* la[stname][,f[irstname]]
* @param string $name
* @return array ($lastname, $firstname)
*/
public function parseName($name) {
$name = preg_split('/ *, */',trim($name),2,PREG_SPLIT_NO_EMPTY);
if (2 == sizeof($name)) {
list($last, $first) = $name;
} else {
$last = $name[0];
$first = false;
}
return array($last,$first);
}
Определение класса Application\Entity\DefendantName прост и опущено для краткости.
Еще нужно: добавьте кнопку поиска справа от нашего текстового элемента автозаполнения, чтобы они нажимали, когда не получили совпадений автозаполнения, поэтому мы можем сказать им "извините, никаких совпадающих записей не найдено". И, хотя это касательно первоначального вопроса, предоставьте им возможность представить имена, о которых мы никогда не слышали.
Все, что я могу придумать, это использовать валидатор шаблона HTML5 (regex). Но я бы сказал, что это гораздо менее удобное для пользователя, а затем ваше текущее решение. Я также очень сомневаюсь в том, может ли регулярное выражение обрабатывать 12000 имен в качестве ограничения. Честно говоря, это звучит неплохо.
Возможно, вы можете улучшить свое текущее решение с помощью ограничения алфавитных символов:
Name: <input type="text" name="name" pattern="[A-Za-z]*" placeholder="Write a valid name">