#symfony #doctrine-orm
#symfony #доктрина-orm #doctrine-orm
Вопрос:
Контекст: я создаю свой маленький пакет ToDoList (что является хорошим упражнением для постепенного углубления в Symfony2), сложность связана с рекурсивностью: у каждой задачи могут быть дочерние и родительские элементы, поэтому я использовал дерево Gedmo. У меня есть коллекция задач, каждая из которых имеет дочернюю коллекцию дочерних элементов, в дочерней коллекции включен прототип, поэтому я могу отобразить новую форму подзадачи при нажатии «добавить подзадачу». Я хотел, чтобы именем подзадачи по умолчанию было «Новая подзадача», а не «Новая задача», установленная в конструкторе задач, поэтому я выяснил, как передать пользовательский экземпляр для прототипа, и позаботился о предотвращении бесконечного цикла. Итак, я почти закончил, и моя новая задача добавлена с именем, которое я установил при сохранении…
Проблема: я не могу сохранить родительскую задачу в новой подзадаче, новая задача хорошо сохраняет имя, но не родительский идентификатор, я, вероятно, где-то забыл с Doctrine, вот некоторые соответствующие части :
// Задача сущности
/**
* @GedmoTree(type="nested")
* @ORMEntity(repositoryClass="GedmoTreeEntityRepositoryNestedTreeRepository")
* @ORMHasLifecycleCallbacks
* @ORMTable(name="task")
*/
class Task {
/**
* @ORMId
* @ORMColumn(type="integer")
* @ORMGeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* @GedmoTimestampable(on="create")
* @ORMColumn(type="datetime")
*/
protected $created;
/**
* @ORMColumn(type="string", length=255)
* @AssertNotBlank(message="Name must be not empty")
*/
protected $name = 'New Task';
//....
/**
* @GedmoTreeLeft
* @ORMColumn(name="lft", type="integer")
*/
private $lft;
/**
* @GedmoTreeLevel
* @ORMColumn(name="lvl", type="integer")
*/
private $lvl;
/**
* @GedmoTreeRight
* @ORMColumn(name="rgt", type="integer")
*/
private $rgt;
/**
* @GedmoTreeRoot
* @ORMColumn(name="root", type="integer", nullable=true)
*/
private $root;
/**
* @GedmoTreeParent
* @ORMManyToOne(targetEntity="Task", inversedBy="children")
* @ORMJoinColumn(name="parentId", referencedColumnName="id", onDelete="SET NULL")
*/
protected $parent = null;//
/**
* @ORMColumn(type="integer", nullable=true)
*/
protected $parentId = null;
/**
* @AssertValid()
* @ORMOneToMany(targetEntity="Task", mappedBy="parent", cascade={"persist", "remove"})
* @ORMOrderBy({"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 DmidzTodoBundleEntityTask $parent
* @return Task
*/
public function setParent(DmidzTodoBundleEntityTask $parent = null){
$this->parent = $parent;
return $this;
}
/**
* Get parent
*
* @return DmidzTodoBundleEntityTask
*/
public function getParent(){
return $this->parent;
}
/**
* Add children
*
* @param DmidzTodoBundleEntityTask $child
* @return Task
*/
public function addChild(DmidzTodoBundleEntityTask $child){
$this->children[] = $child;
return $this;
}
/**
* Remove child
*
* @param DmidzTodoBundleEntityTask $child
*/
public function removeChild(DmidzTodoBundleEntityTask $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 amp;amp; ($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' => 'DmidzTodoBundleEntityTask',
'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 (просмотр журнала doctrine).
Итак, считаете ли вы, что это правильный способ сделать, и затем, что я забыл для правильного сохранения родительской задачи новой подзадачи?
Ответ №1:
В вашей дочерней настройке вы должны установить родительский параметр при добавлении, вот так..
/**
* Add children
*
* @param DmidzTodoBundleEntityTask $children
* @return Task
*/
public function addChild(DmidzTodoBundleEntityTask $children){
$this->children->add($children);
$children->setParent($this);
return $this;
}
/**
* Remove children
*
* @param DmidzTodoBundleEntityTask $children
*/
public function removeChild(DmidzTodoBundleEntityTask $children){
$this->children->removeElement($children);
$children->setParent(null);
}
Когда ваш прототип добавляет и удаляет строку, он вызывает addChild
и removeChild
, но не вызывает setParent
в связанном дочернем элементе.
Таким образом, любой дочерний элемент, который добавляется или удаляется / deleted, автоматически устанавливается в процессе.
Также вы могли бы изменить $children
на $child
, поскольку это имеет грамматический смысл, и это действительно беспокоит меня, потому что я ребенок (ren).
Комментарии:
1. спасибо, чувак, это работает, ты сделал мой день 🙂 Мне не хватало этого небольшого фрагмента отношения, я перечитал несколько документов, но не смог правильно применить к этому случаю… Теперь я лучше понимаю, что вы говорите мне, что addChild и removeChild вызываются в процессе создания прототипа коллекции. Да, вы абсолютно правы, я также должен изменить эту опечатку, это сбивает с толку, больше не нужно 😉 вы сохраняете некоторые моменты, еще раз спасибо!
Ответ №2:
Мне кажется странным, что вы пытаетесь использовать parentId
поле как простой столбец, тогда как это столбец отношений. Теоретически, вы не должны:
$task->getParentId(); //fetching a DB column's value
но вместо:
$task->getParent()->getId(); //walking through relations to find an object's attribute
Однако, если вам действительно нужна эта функция, чтобы избежать загрузки полного родительского объекта и просто получить его идентификатор, ваш 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, а не простой помощник для запросов: mapper — это то, что он делает (сопоставление уровня сохраняемости с вашим кодом), relational — это то, как он это делает (один ко многим и тому подобное), object — это то, с чем он это делает (поэтому напрямую не использует идентификаторы).
Надеюсь, это поможет!
Комментарии:
1. ну, я использовал аннотации дерева Gedmo и следил за несколькими документами для этого, но я довольно новичок в Symfony и Doctrine. Я попробовал $ protoTask-> SetParent ($ record); уже и отношение все еще не сохраняется. Мне интересно, подходящее ли это место для установки отношения (в методе buildForm типа формы), это может быть в контроллере? именно этот тип точности мне нужен…
2. Я почти уверен, что
$task->getParent()->getId()
не вызовет запрос к базе данных, поскольку прокси-объект уже «знает» свой идентификатор.3. На самом деле я использовал input ParentID для его назначения без необходимости и молюсь, чтобы отношение было сохранено…
4. @dmidz Где бы вы ни устанавливали отношение, сущности по-прежнему и всегда управляются менеджером сущностей Doctrine, поэтому установка отношения — это просто вопрос дизайна, а не техническое ограничение. Если установка родительского элемента (я имею в виду, как объекта, в отличие от установки идентификатора родителя) не работает, либо это двойное
parentId
объявление сводит Doctrine с ума (хотя, я полагаю, это вызвало бы какое-то исключение / ошибку), либо, что более вероятно,setParent
вызывается сnull
. Возможно, запуск исключения (или сброс стека вызовов и завершение работы) приnull
передаче в SetParent даст вам больше информации?5. да, возможно, где-то передан родительский номер null, я даю попробовать… в любом случае, если я изменю входное значение ParentID для подзадачи sibbling, уже существующей в db, конечно, это сработает, единственная разница в том, что подзадача уже существует… Я чувствую, что это неподходящее место для установки отношения, но я недостаточно экспериментировал