集合原型和主义持久性ManyToOne关系

时间:2014-07-01 13:44:17

标签: symfony doctrine-orm

上下文:我正在构建我的小TodoList包(这是一个很好的练习,以便与Symfony2逐步深入),难度来自递归:每个任务可以有子和父,所以我使用Gedmo树。 我有一个任务集合,每个任务都有一个子集合,子集合已启用原型,所以我可以在单击“添加子任务”时显示一个新的子任务表单。 我希望子任务的默认名称是“New Sub Task”,而不是在Task构造函数中设置的“New Task”,所以我想出了如何为原型传递自定义实例并注意防止无限循环。 所以我差不多完成了,我的新任务添加了我在保存时设置的名称......

问题:我无法将父任务持久化到新的子任务,新任务保持名称很好,但不是parentId,我可能忘记了Doctrine的某个地方,这里有一些相关的部分:

//实体任务

/**
 * @Gedmo\Tree(type="nested")
 * @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository")
 * @ORM\HasLifecycleCallbacks
 * @ORM\Table(name="task")
 */
class Task {

    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @Gedmo\Timestampable(on="create")
     * @ORM\Column(type="datetime")
     */
    protected $created;

    /**
     * @ORM\Column(type="string", length=255)
     * @Assert\NotBlank(message="Name must be not empty")
     */
    protected $name = 'New Task';

     //....

    /**
     * @Gedmo\TreeLeft
     * @ORM\Column(name="lft", type="integer")
     */
    private $lft;

    /**
     * @Gedmo\TreeLevel
     * @ORM\Column(name="lvl", type="integer")
     */
    private $lvl;

    /**
     * @Gedmo\TreeRight
     * @ORM\Column(name="rgt", type="integer")
     */
    private $rgt;

    /**
     * @Gedmo\TreeRoot
     * @ORM\Column(name="root", type="integer", nullable=true)
     */
    private $root;

    /**
     * @Gedmo\TreeParent
     * @ORM\ManyToOne(targetEntity="Task", inversedBy="children")
     * @ORM\JoinColumn(name="parentId", referencedColumnName="id", onDelete="SET NULL")
     */
    protected $parent = null;// 

    /**
     * @ORM\Column(type="integer", nullable=true)
     */
    protected $parentId = null;


    /**
     * @Assert\Valid()
     * @ORM\OneToMany(targetEntity="Task", mappedBy="parent", cascade={"persist", "remove"})
     * @ORM\OrderBy({"status" = "ASC", "created" = "DESC"})
     */
    private $children;

    public function __construct(){
        $this->children = new ArrayCollection();
    }


   /**
     * Set parentId
     *
     * @param integer $parentId
     * @return Task
     */
    public function setParentId($parentId){
        $this->parentId = $parentId;

        return $this;
    }

    /**
     * Get parentId
     *
     * @return integer 
     */
    public function getParentId(){
        return $this->parentId;
    }

    /**
     * Set parent
     *
     * @param \Dmidz\TodoBundle\Entity\Task $parent
     * @return Task
     */
    public function setParent(\Dmidz\TodoBundle\Entity\Task $parent = null){
        $this->parent = $parent;
        return $this;
    }

    /**
     * Get parent
     *
     * @return \Dmidz\TodoBundle\Entity\Task 
     */
    public function getParent(){
        return $this->parent;
    }

    /**
     * Add children
     *
     * @param \Dmidz\TodoBundle\Entity\Task $child
     * @return Task
     */
    public function addChild(\Dmidz\TodoBundle\Entity\Task $child){
        $this->children[] = $child;

        return $this;
    }

    /**
     * Remove child
     *
     * @param \Dmidz\TodoBundle\Entity\Task $child
     */
    public function removeChild(\Dmidz\TodoBundle\Entity\Task $child){
        $this->children->removeElement($child);
    }
}

// TaskType

class TaskType extends AbstractType {

    public function buildForm(FormBuilderInterface $builder, array $options){
        $builder
            ->add('name', null, ['label' => false])
            ->add('notes', null, ['label' => 'Notes'])
            ->add('status', 'hidden')
            ->add('parentId', 'hidden')
            ;

        $builder->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $event) use ($builder){
            $record = $event->getData();
            $form = $event->getForm();

            if(!$record || $record->getId() === null){// if prototype
                $form->add('minutesEstimated', null, ['label' => 'Durée', 'attr'=>['title'=>'Durée estimée en minutes']]);
            }elseif($record && ($children = $record->getChildren())) {
                // this is where I am able to customize the prototype default values
                $protoTask = new Task();
                $protoTask->setName('New Sub Task');
                // here I am loosely trying to set the parentId I want
                // so the prototype form input has the right value
                // BUT it goes aways when INSERT in mysql, the value is NULL
                $protoTask->setParentId($record->getId());

                $form->add('sub', 'collection', [// warn don't name the field 'children' or it will conflict
                    'property_path' => 'children',
                    'type' => new TaskType(),
                    'allow_add' => true,
                    'by_reference' => false,
                    // this option comes from a form type extension
                    // allowing customizing prototype default values
                    // extension code : https://gist.github.com/jumika/e2f0a5b3d4faf277307a
                    'prototype_data' => $protoTask
                ]);
            }
        });
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver){
        $resolver->setDefaults([
            'data_class' => 'Dmidz\TodoBundle\Entity\Task',
            'label' => false,
        ]);
    }

    public function getParent(){    return 'form';}
}

//我的控制器

/**
 * @Route("/")
 * @Template("DmidzTodoBundle:Task:index.html.twig")
 */
public function indexAction(Request $request){
    $this->request = $request;

    $repo = $this->doctrine->getRepository('DmidzTodoBundle:Task');
    $em = $this->doctrine->getManager();

    //__ list of root tasks (parent null)
    $query = $repo->createQueryBuilder('p')
        ->select(['p','FIELD(p.status, :progress, :wait, :done) AS HIDDEN field'])
        ->addOrderBy('field','ASC')
        ->addOrderBy('p.id','DESC')
        ->andWhere('p.parent IS NULL')
        ->setParameters([
            'progress' => Task::STATUS_PROGRESS,
            'wait' => Task::STATUS_WAIT,
            'done' => Task::STATUS_DONE
        ])
        ->setMaxResults(20)
        ->getQuery();

    $tasks = $query->getResult();

    //__ form building : collection of tasks
    $formList = $this->formFactory->createNamed('list_task', 'form', [
            'records' => $tasks
        ])
        ->add('records', 'collection', [
            'type'=>new TaskType(),
            'label'=>false,
            'required'=>false,
            'by_reference' => false,
        ])
        ;

    //__ form submission
    if ($request->isMethod('POST')) {
        $formList->handleRequest($request);
        if($formList->isValid()){
            // persist tasks
            // I thought persisting root tasks will persist their children relation
            foreach($tasks as $task){
                $em->persist($task);
            }
            $em->flush();
            return new RedirectResponse($this->router->generate('dmidz_todo_task_index'));
        }
    }

    return [
        'formList' => $formList->createView(),
    ];
}

正如在TaskType的注释中所提到的,新子任务的表单原型具有发布的parentId的正确值,但是该值在db中的INSERT上消失并且为NULL(查看学说日志)。 所以你认为这是正确的做法,然后我忘记了为正确坚持新子任务的父任务而忘了什么?

2 个答案:

答案 0 :(得分:2)

在您的孩子设置中,您应该在添加时设置父级,例如..

/**
 * Add children
 *
 * @param \Dmidz\TodoBundle\Entity\Task $children
 * @return Task
 */
public function addChild(\Dmidz\TodoBundle\Entity\Task $children){
    $this->children->add($children);
    $children->setParent($this);

    return $this;
}

/**
 * Remove children
 *
 * @param \Dmidz\TodoBundle\Entity\Task $children
 */
public function removeChild(\Dmidz\TodoBundle\Entity\Task $children){
    $this->children->removeElement($children);
    $children->setParent(null);
}

当您的原型添加并删除一行时,它会调用addChildremoveChild,但不会调用相关子项中的setParent

这样,添加或删除/删除的所有子项都会在此过程中自动设置。

此外,您可以将$children更改为$child,因为它具有语法意义,因为我是小孩,所以它真的让我烦恼。

答案 1 :(得分:0)

我尝试将parentId字段用作简单列,而它是关系列,这似乎很奇怪。从理论上讲,你不应该:

$task->getParentId(); //fetching a DB column's value

但是:

$task->getParent()->getId(); //walking through relations to find an object's attribute

但是,如果您确实需要此功能以避免加载完整的父对象并只获取其ID,那么您的setParentId方法应该是透明的(尽管如我所知,我不确定使用相同的DB字段是有效的):

public function setParent(Task $t = null) {
    $this->parent = $t;
    $this->parentId = null === $t ? null : $t->getId();
    return $this;
}

回到你的问题:在TaskType课程中,你应该致电:

$protoTask->setParent($record);

而不是:

$protoTask->setParentId($record->getId());

原因:

  • 你告诉Doctrine parentId是一个关系字段(在$parent属性声明中),因此Doctrine期望一个正确类型的对象
  • 你还告诉Doctrine将这个关系字段直接映射到一个属性($parentId属性声明),我既不相信这是有效的,也不相信这是一个好习惯,但我猜你做了一些研究在进入这个结构之前
  • 您设置了$parentId,但$parent尚未设置(即null),因此Doctrine必须使用$parentId值删除$parent值:你的代码证明了Doctrine首先处理属性,然后计算关系;)

请记住,Doctrine是一个对象关系映射器,而不是一个简单的查询帮助器: mapper 就是它所做的(用您的代码映射持久层),关系是它如何做的(一对多等),对象就是它所做的(因此不直接使用ID)。

希望这有帮助!