'Symfony / Doctrine: OneToMany insert results in null id

I have two entities that I'm trying to apply a OneToMany / ManyToOne relationship to (one Game has many GameContent).

Game

/**
 * @ORM\OneToMany(targetEntity="GameContent", mappedBy="game")
 */
private $contents;

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

public function getContents()
{
    return $this->contents;
}

GameContent

/**
 * @ORM\ManyToOne(targetEntity="Game", inversedBy="contents")
 */
private $game;

And the following code inserts both records into their respective tables:

$game = $form->getData();
$content = new GameContent();
$content->setType('some type');
$game->getContents()->add($content);

$em = $this->getDoctrine()->getManager();
$em->persist($content);
$em->persist($game);
$em->flush();

However, the GameContent's game_id is inserted as null:

INSERT INTO game_content (type, game_id) VALUES (?, ?)
Parameters: { 1: 'some type', 2: null }

I've also tried:

  • changing the order of persist()
  • replacing $game->getContents()->add($content) with $game->addContents($content) by doing $this->contents[] = $content;
  • removing persist($content) and having cascade={"persist"} on the Game entity.

Why is game_id being inserted as null?


My current workaround is:

$em = $this->getDoctrine()->getManager();

$game = $form->getData();
$em->persist($game);

$content = new GameContent();
$content->setType('some type');
$content->setGame($game);
$em->persist($content);

$em->flush();


Solution 1:[1]

You have 2 solutions :

Persist children in controller

Without cascade={"persist"}

$em = $this->getDoctrine()->getManager();

// Get data
$game = $form->getData();

// Create new GameContent and hydrate
$content = new GameContent();
$content->setType('some type');

// Associate Game <> GameContent
$content->setGame($game);

// Persist GameContent
$em->persist($content);

// Persist Game and commit
$em->persist($game);
$em->flush();

Persist children in cascade

With cascade={"persist"} in OneToMany relation.

Add in setGame() function, to force association :

$game->addContent($this);

And remove persist :

$em = $this->getDoctrine()->getManager();

// Get data
$game = $form->getData();

// Create new GameContent and hydrate
$content = new GameContent();
$content->setType('some type');

// Associate Game <> GameContent
$content->setGame($game);

// Persist Game and commit
$em->persist($game);
$em->flush();

I think the error was also due to the positioning of the persist on game.

Solution 2:[2]

Add in setGame() function, to force association :

$game->addContent($this);

And remove persist :

$em = $this->getDoctrine()->getManager();

// Get data
$game = $form->getData();

// Create new GameContent and hydrate
$content = new GameContent();
$content->setType('some type');

// Associate Game <> GameContent
$content->setGame($game);

// Persist Game and commit
$em->persist($game);
$em->flush();

Note that today (Doctrine 2.7.1), the make:entity utility creates methods that does this stuff for you, in your case you would have had a method like this on your Game entity:

public function addContent(GameContent $content): self
{
    $this->contents->add($content);
    $content->setGame($this); // <-- IMPORTANT PART IS HERE

    return $this;
}

Then, calling this on the game would have done the job:

this->addContent((new GameContent())->setType('some type'));

Solution 3:[3]

Further to the accepted answer, my next step was to create a form to handle the GameContent data, which led to further changes and some simplified logic.

I now setGame() in Game::addContent(), and so I've removed $game->addContent($this); in GameContent::setGame().

Game

/**
 * @var ArrayCollection
 * @ORM\OneToMany(targetEntity="GameContent", mappedBy="game", cascade={"persist"})
 */
private $contents;

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

public function getContents()
{
    return $this->contents;
}

public function addContent(GameContent $content)
{
    $this->contents->add($content);
    $content->setGame($this);

    return $this;
}

public function removeContent(GameContent $content)
{
    $this->contents->removeElement($content);

    return $this;
}

GameContent

/**
 * @ORM\ManyToOne(targetEntity="Game", inversedBy="contents")
 */
private $game;

public function setGame(Game $game)
{
    $this->game = $game;

    return $this;
}

/**
 * @return Game
 */
public function getGame()
{
    return $this->game;
}

The real-world form-handling logic will look like this:

$game = $form->getData();
$em = $this->getDoctrine()->getManager();
$em->persist($game);
$em->flush();

More information at: http://symfony.com/doc/2.8/form/form_collections.html (see Doctrine: Cascading Relations and saving the "Inverse" side).

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Le Menach Florian
Solution 2 Ryierth
Solution 3 rybo111