我有两张桌子:
分行:
+-------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| name | varchar(255) | NO | | NULL | |
+-------------+--------------+------+-----+---------+----------------+
分支元:
+------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| branch_id | int(11) | YES | MUL | NULL | |
| metaname | varchar(255) | NO | | NULL | |
| metavalue | varchar(255) | NO | | NULL | |
+------------+--------------+------+-----+---------+----------------+
我想要有不同的元字段,例如“电话”、“电子邮件”等,也可以是多个。
目前我有这个代码(省略了setter/getter):
//实体/Branch.php
<?php
namespace Acme\Bundle\ConsysBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* Branch
*
* @ORM\Entity(repositoryClass="Acme\Bundle\ConsysBundle\Entity\BranchRepository")
* @ORM\Table(name="branch")
*/
class Branch
{
/**
* @var integer
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @var string
*
* @ORM\Column(name="name", type="string", length=255)
*/
private $name;
/**
*
* @ORM\OneToMany(targetEntity="BranchMeta", mappedBy="branch")
*/
private $metadata;
public function __construct()
{
$this->metadata = new ArrayCollection();
}
}
?>
//实体/BranchMeta.php
<?php
namespace Acme\Bundle\ConsysBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* BranchMeta
*
* @ORM\Entity(repositoryClass="Acme\Bundle\ConsysBundle\Entity\BranchMetaRepository")
* @ORM\Table(name="branchmeta")
*/
class BranchMeta
{
/**
* @var integer
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\ManyToOne(targetEntity="Branch", inversedBy="metadata")
* @ORM\JoinColumn(name="branch_id", referencedColumnName="id")
*/
private $branch;
/**
* @var string
*
* @ORM\Column(name="metaname", type="string", length=255)
*/
private $name;
/**
* @var string
*
* @ORM\Column(name="metavalue", type="string", length=255)
*/
private $value;
?>
然后我需要构建一个表单来添加这样的分支:
Branch Name: [_________]
Branch Phone: [_________] [-]
Branch Phone: [_________] [+]
Branch Email: [_________] [-]
Branch Email: [_________] [+]
[Submit]
其中“Branch Phone”字段指向元名称为“phone”和对应值的branchmeta 表,“Branch Email”字段指向元名称为“email”的branchmeta 表。两者都可以动态添加/删除。
如何使用 Symfony2 的 FormBuilder 构建它?我像这样创建了 BranchType:
class BranchType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name');
}
public function getName()
{
return 'branch';
}
}
和 BranchMetaType:
class BranchMetaType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name');
$builder->add('value');
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\Bundle\ConsysBundle\Entity\BranchMeta',
));
}
public function getName()
{
return 'branch_meta';
}
}
但是后来我陷入了困境......如何正确使用 BranchMetaType 在我的情况下构建所需的表单?也许我在数据映射中遗漏了一些东西?
事实上,还有更好的解决方案:
/**
* Meta
*
* @ORM\Entity
* @ORM\InheritanceType("SINGLE_TABLE")
* @ORM\DiscriminatorColumn(name="metaname", type="string")
* @ORM\DiscriminatorMap({"phone" = "MetaPhone", "email" = "MetaEmail", "dummy" = "MetaDummy"})
* @ORM\MappedSuperclass
*/
class Meta
{
// ...
}
/**
* @ORM\Entity
*/
class MetaPhone
{
}
/**
* @ORM\Entity
*/
class MetaEmail
{
}
/**
* @ORM\Entity
*/
class MetaDummy
{
}
这样做,我们将拥有相同的表、相同的实体(+一个超类实体),并且我们将像使用普通实体一样使用它们。不再有什么黑客方法了。我希望我在 7 个月前就知道了这个功能...
不知何故,信息分散在食谱中http://symfony.com/doc/current/cookbook/form/form_collections.html附加注释部分学说:级联关系并保存“逆”面。
这个问题给我留下了深刻的印象,因此我对其进行了一些实际的编码/研究。 :)
这是我发现的。
在
Entity\Branch
中,告诉原则将其持久化操作级联到元数据属性上,并修改setMetadata()
以设置要持久化的每个元数据的当前分支。
/**
*
* @ORM\OneToMany(targetEntity="BranchMeta", mappedBy="branch", cascade={"persist"})
*/
private $metadata;
public function setMetadata(ArrayCollection $metadata) {
foreach ($metadata as $m) {
$m->setBranch($this);
}
$this->metadata = $metadata;
}
在
Form\BranchType
中,使用适当的选项添加元数据字段,当然将默认的data_class
设置为Entity\Branch
(上面的BranchType
没有这个)。在此表单类型中, 'by_reference' => false
选项对于确保在持久期间调用 setMetadata()
函数非常重要(http://symfony.com/doc/current/reference/forms/types/collection.html#by-参考)。
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add('name');
$builder->add('metadata', 'collection',
array(
'type' => new BranchMetaType(),
'allow_add' => true,
'by_reference' => false,
));
}
public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setDefaults(
array(
'data_class' => 'Acme\Bundle\ConsysBundle\Entity\Branch'
));
}
最后尝试在控制器中添加一些虚拟分支元
public function newAction()
{
$entity = new Branch();
$branchmeta1 = new BranchMeta();
$branchmeta1->setMetaname('dummy meta name #1');
$branchmeta1->setMetavalue('dummy meta value #1');
$entity->getMetadata()->add($branchmeta1);
$branchmeta2 = new BranchMeta();
$branchmeta2->setMetaname('dummy meta name #2');
$branchmeta2->setMetavalue('dummy meta value #2');
$entity->getMetadata()->add($branchmeta2);
$form = $this->createForm(new BranchType(), $entity);
return array(
'entity' => $entity,
'form' => $form->createView(),
);
}
最后我找到了一个解决方案,我不能称之为优雅,但至少它有效。
分支.php:
namespace Acme\Bundle\ConsysBundle\Entity;
//use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* Branch
*
* @ORM\Entity(repositoryClass="Acme\Bundle\ConsysBundle\Entity\BranchRepository")
* @ORM\HasLifecycleCallbacks
* @ORM\Table(name="branch")
*/
class Branch
{
/**
* @var integer
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
*
* @ORM\OneToMany(targetEntity="BranchMeta", mappedBy="branch", cascade={"persist", "remove"})
*/
private $metadata;
/**
* Virtual OneToMany field
*/
private $telephones;
/**
* Virtual OneToMany field
*/
private $faxes;
/**
* Virtual OneToMany field
*/
private $emails;
public function __construct()
{
$this->metadata = new ArrayCollection();
$this->telephones = new ArrayCollection();
$this->faxes = new ArrayCollection();
$this->emails = new ArrayCollection();
}
/* ...setters and getters are skipped... */
/**
* @ORM\PostLoad
*/
public function onPostLoad()
{
foreach (array('telephone' => 'Telephones', 'fax' => 'Faxes', 'email' => 'Emails') as $metakey => $metaname) {
$metadata = $this->getMetadata()->filter(
function($entry) use ($metakey) {
return $entry->getName() == $metakey;
}
);
$meta = call_user_func(array($this, 'set'.$metaname), $metadata);
}
}
}
控制器代码:
private function checkBranchAdded($branch)
{
foreach (array('telephone' => 'Telephones', 'fax' => 'Faxes', 'email' => 'Emails') as $metakey => $metaname) {
$metadata = call_user_func(array($branch, 'get'.$metaname));
foreach ($metadata as $item) {
if (!$item->getBranch()) {
$item->setBranch($branch);
$branch->getMetadata()->add($item);
}
}
}
}
private function checkBranchDeleted($branch)
{
$newMeta = array();
foreach (array('telephone' => 'Telephones', 'fax' => 'Faxes', 'email' => 'Emails') as $metakey => $metaname) {
$metadata = call_user_func(array($branch, 'get'.$metaname));
$newMeta = array_merge($newMeta, array_keys($metadata->toArray()));
}
$em = $this->getDoctrine()->getEntityManager();
foreach($branch->getMetadata() as $key => $item) {
if (!in_array($key, $newMeta)) {
$em->remove($item);
$branch->getMetadata()->remove($key);
}
}
}
/**
*
* @Template()
*/
public function editAction($activeMenu, $activeSubmenu, $activeSubsubmenu, $id, Request $request)
{
/* ... some code ... */
$repository = $this->getDoctrine()->getRepository('AcmeConsysBundle:Branch');
$branch = $repository->find($id);
if (!$branch) {
throw $this->createNotFoundException('No branch found for id '.$id);
}
$form = $this->createForm(new BranchType(), $branch);
if ($request->isMethod('POST')) {
$form->bind($request);
$this->checkBranchDeleted($branch);
$this->checkBranchAdded($branch);
/* ... some code ... */
}
/* ... some code ... */
}
表单构建器代码。 BranchType.php:
class BranchType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('telephones', 'collection', array(
'type' => new BranchMetaType('telephone'),
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
'options' => array(
),
));
$builder->add('faxes', 'collection', array(
'type' => new BranchMetaType('fax'),
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
'options' => array(
),
));
$builder->add('emails', 'collection', array(
'type' => new BranchMetaType('email'),
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
'options' => array(
),
));
/* ... other fields ... */
}
/* ... other methods ... */
}
BranchMetaType.php:
class BranchMetaType extends AbstractType
{
protected $name;
public function __construct($name)
{
$this->name = $name;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name', 'hidden', array(
'data' => $this->name,
));
$builder->add('value', 'text', array(
));
}
/* ... other methods ... */
}