使用 Zend\Form 和 Doctrine 2 实现多对多关系
implementing many-to-many relationship with Zend\Form and Doctrine 2
我有一个 Zend\Form
和相应的 Doctrine 实体 class,其中该实体与另一个实体有 ManyToMany
关系。更准确地说,用户需要能够从包含 12,000 个姓名的数据 table 中选择一个或多个姓名——对于普通的 SELECT 元素来说太多了。
在这个使用 ZF1 的项目的早期迭代中,我有一个 MultiSelect
元素,选项为零,我根本就没有渲染过。相反,我使用 JQueryUI 创建了一个自动完成文本字段,用于动态插入人类可读的名称和 id 作为隐藏元素。效果很好。
我看过 Zend\Form\Element\Collection
但是文档说你不能用比你开始时更少的元素来更新它——也就是说,如果在更新形式水合时你有 2 个任何东西,你必须至少提交 2 项。那不行。
在其他地方我很乐意使用 DoctrineModule\Form\Element\ObjectSelect
但它似乎不是这种情况的正确选择。
在我开始尝试使用与 ZF1 相同的技术之前,如果有人能给我更好的主意,我会很高兴。
我现在所能想到的就是使用 HTML5 模式(正则表达式)验证器。但是我会说这比您当前的解决方案更不友好。我也非常怀疑正则表达式是否可以处理 12000 个名称作为约束。老实说,这听起来像是个坏主意。
您可以通过限制为字母字符来增强您当前的解决方案:
Name: <input type="text" name="name" pattern="[A-Za-z]*" placeholder="Write a valid name">
我的回答是:没有奇异的秘密知识。像使用任何其他 tools/libraries.
一样使用自动完成和数据库
稍微描述一下我的用例:用户为将来的法庭诉讼提交口译员请求。他们必须提供被告的姓名以及其他数据(例如语言)。我们的数据库中已经有超过 12K 个这样的名字,我们重复使用重复出现的名字(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's 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's 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>
还有一些来自 defts.js
的 Javascript,在我们的 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);
}
class 定义 Application\Entity\DefendantName 很简单,为简洁起见省略。
还有待做:在我们的自动完成文本元素的右侧添加一个搜索按钮,以便他们在没有自动完成匹配项时单击,这样我们就可以告诉他们 "sorry, no matching records found." 并且 - 尽管这与最初的问题——为他们提供一种方式来提交我们从未听说过的名字。
我有一个 Zend\Form
和相应的 Doctrine 实体 class,其中该实体与另一个实体有 ManyToMany
关系。更准确地说,用户需要能够从包含 12,000 个姓名的数据 table 中选择一个或多个姓名——对于普通的 SELECT 元素来说太多了。
在这个使用 ZF1 的项目的早期迭代中,我有一个 MultiSelect
元素,选项为零,我根本就没有渲染过。相反,我使用 JQueryUI 创建了一个自动完成文本字段,用于动态插入人类可读的名称和 id 作为隐藏元素。效果很好。
我看过 Zend\Form\Element\Collection
但是文档说你不能用比你开始时更少的元素来更新它——也就是说,如果在更新形式水合时你有 2 个任何东西,你必须至少提交 2 项。那不行。
在其他地方我很乐意使用 DoctrineModule\Form\Element\ObjectSelect
但它似乎不是这种情况的正确选择。
在我开始尝试使用与 ZF1 相同的技术之前,如果有人能给我更好的主意,我会很高兴。
我现在所能想到的就是使用 HTML5 模式(正则表达式)验证器。但是我会说这比您当前的解决方案更不友好。我也非常怀疑正则表达式是否可以处理 12000 个名称作为约束。老实说,这听起来像是个坏主意。
您可以通过限制为字母字符来增强您当前的解决方案:
Name: <input type="text" name="name" pattern="[A-Za-z]*" placeholder="Write a valid name">
我的回答是:没有奇异的秘密知识。像使用任何其他 tools/libraries.
一样使用自动完成和数据库稍微描述一下我的用例:用户为将来的法庭诉讼提交口译员请求。他们必须提供被告的姓名以及其他数据(例如语言)。我们的数据库中已经有超过 12K 个这样的名字,我们重复使用重复出现的名字(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's 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's 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>
还有一些来自 defts.js
的 Javascript,在我们的 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);
}
class 定义 Application\Entity\DefendantName 很简单,为简洁起见省略。
还有待做:在我们的自动完成文本元素的右侧添加一个搜索按钮,以便他们在没有自动完成匹配项时单击,这样我们就可以告诉他们 "sorry, no matching records found." 并且 - 尽管这与最初的问题——为他们提供一种方式来提交我们从未听说过的名字。