0

I have self-referencing menu entity (created using make:entity):

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

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $name;

    /**
     * @ORM\ManyToOne(targetEntity="App\Entity\Menu", inversedBy="children")
     */
    private $parent;

    /**
     * @ORM\OneToMany(targetEntity="App\Entity\Menu", mappedBy="parent")
     */
    private $children;

and the data:

| id | name      | parent |
|----|-----------|--------|
| 1  | MainItem  | null   |
| 2  | Child 1-1 | 1      |
| 3  | Child 1-2 | 1      |
| 4  | Child 1-3 | 1      |

My function to get the top-level menu items:

private function getTopMenu (MenuRepository $menuRepository) {
        return $menuRepository->findBy(['parent' => null]);
    }

returns the following:

array:1 [▼
  0 => Menu^ {#1186 ▼
    -id: 1
    -name: "MainItem"
    -parent: null
    -children: PersistentCollection^ {#1188 ▼
      -snapshot: []
      -owner: Menu^ {#1186}
      -association: array:15 [ …15]
      -em: EntityManager^ {#1050 …11}
      -backRefFieldName: "parent"
      -typeClass: ClassMetadata {#1096 …}
      -isDirty: false
      #collection: ArrayCollection^ {#1189 ▼
        -elements: []
      }
      #initialized: false
    }
  }
]

Because I wasn't getting anything in the children, I tried working backwards using $menuRepository->find(2)->getParent(); which returned:

Menu^ {#1205 ▼
  +__isInitialized__: false
  -id: 1
  -name: null
  -parent: null
  -children: null
   …2
}

Lastly I tried $menuRepository->find(2)->setParent($menuRepository->find(3))->getParent() which finally seemed to get me somewhere:

Menu^ {#1201 ▼
  -id: 3
  -name: "Child 1-2"
  -parent: Menu^ {#1205 ▶}
  -children: PersistentCollection^ {#1200 ▶}
}

I can't figure out why getChildren and getParent aren't returning the data.

1 Answers1

0

Accessing the children from the parent should work. The contrary also.

First of all, make sure your constructor declares a new ArrayCollection for your children. Very important when using a OneToMany relationship because Doctrine uses the ArrayCollection to load the children data properly.

use Doctrine\Common\Collections\ArrayCollection;

...

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

If this still doesn't work, I would suggest trying to change the fetch mode for your entity mapping.

/**
 * @ORM\OneToMany(targetEntity="App\Entity\Menu", mappedBy="parent", fetch="EAGER")
 */
private $children;

Careful, an eager fetch mode will load the full array on first DB retrieval. Normally, doctrine will use lazy loading, which triggers a DB call when using the get() method. Eager fetch mode can be dangerous because eveytime you would query for a menu, all the children would also be loaded which could be a big overhead.

Hope this helps!

EDIT: There is an annotation you could try, I believe, that would make it work with lazy loading. The annotation is @ORM\JoinColumn.

/**
* @ORM\ManyToOne(targetEntity="App\Entity\Menu", inversedBy="children", fetch="LAZY")
* @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onUpdate="CASCADE", onDelete="CASCADE")
*/
protected $parent;

It might still not work, there is a specific behavior with self-referencing entities, you can read more on that here: Doctrine - self-referencing entity - disable fetching of children It talks about how collections might not be properly initialized when working with self referencing entities.

Regarding fetching eagerly, it should load the whole tree, but there is a limitation to that. The limitation is the actual MySQL number of join limit (By default is 64 from memory). This can be hit pretty quickly.

If you ever need to load the whole menu structured as a tree, I would suggest querying the whole table in a flat array, then building your tree with the function mentionned in here Build a tree from a flat array in PHP. Therefore 1 query instead of N queries depending on the number of branches you have in your tree. If ever you need to load menus from a specific node inside the tree as the start point, you could query only the result from the parent node you want as flat data, then again build a tree. As shown here How to create a MySQL hierarchical recursive query

Tortus
  • 141
  • 7
  • My constructor was as you described. I changed the fetch mode and that seemed to fix it! I am still curious as to why it wasn't working with lazy loading. I have other entity relationships that work as expected, but they are not self-referencing. With eager fetching, Doctrine will basically be grabbing the whole entire menu. Each child it fetches, it will then fetch those children as well, is this correct? – Jakob Knigga Jan 29 '20 at 14:09