在Doctrine2中为元表结构创建映射以在FormBuilder中使用

时间:2013-02-23 10:56:09

标签: symfony orm doctrine-orm formbuilder

我有两张桌子:

分支机构:

+-------------+--------------+------+-----+---------+----------------+
| Field       | Type         | Null | Key | Default | Extra          |
+-------------+--------------+------+-----+---------+----------------+
| id          | int(11)      | NO   | PRI | NULL    | auto_increment |
| name        | varchar(255) | NO   |     | NULL    |                |
+-------------+--------------+------+-----+---------+----------------+

BranchMeta:

+------------+--------------+------+-----+---------+----------------+
| 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 / getters):

// Entity / 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();
    }
}
?>

// Entity / 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]

其中“分支电话”字段指向具有元名称“phone”和对应值的branchmeta表,“分支电子邮件”字段指向具有元名称“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构建所需的表单?也许我在数据映射中遗漏了什么?

3 个答案:

答案 0 :(得分:1)

事实上,有一个更好的解决方案:

/**
 * 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个月前新增这个功能......

答案 1 :(得分:0)

不知何故,这些信息散布在食谱http://symfony.com/doc/current/cookbook/form/form_collections.html的附加说明部分 Doctrine:级联关系并保存“反向”

这个问题令我印象深刻,因此我对它进行了一些实际的编码/研究。 :)
这是我发现的。

Entity\Branch中,告诉doctrine将其持久化操作级联到元数据属性,并修改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选项对于确保在persist(http://symfony.com/doc/current/reference/forms/types/collection.html#by-reference)期间调用setMetadata()函数非常重要。

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(),
    );
}

答案 2 :(得分:0)

最后,我找到了一个我称之为优雅的解决方案,但至少它起作用了。

Branch.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 ... */
}