上下文:我正在构建我的小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(查看学说日志)。 所以你认为这是正确的做法,然后我忘记了为正确坚持新子任务的父任务而忘了什么?
答案 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);
}
当您的原型添加并删除一行时,它会调用addChild
和removeChild
,但不会调用相关子项中的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());
原因:
parentId
是一个关系字段(在$parent
属性声明中),因此Doctrine期望一个正确类型的对象$parentId
属性声明),我既不相信这是有效的,也不相信这是一个好习惯,但我猜你做了一些研究在进入这个结构之前$parentId
,但$parent
尚未设置(即null
),因此Doctrine必须使用$parentId
值删除$parent
值:你的代码证明了Doctrine首先处理属性,然后计算关系;)请记住,Doctrine是一个对象关系映射器,而不是一个简单的查询帮助器: mapper 就是它所做的(用您的代码映射持久层),关系是它如何做的(一对多等),对象就是它所做的(因此不直接使用ID)。
希望这有帮助!