Прототип коллекции и отношение ManyToOne к постоянству доктрины

#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, конечно, это сработает, единственная разница в том, что подзадача уже существует… Я чувствую, что это неподходящее место для установки отношения, но я недостаточно экспериментировал