Luka
Luka

Reputation: 758

ManytoMany Relationship in Doctrine 2

I have recently startet with Zend Framework 2 and came now across Doctrine 2, which I would now like to integrate in my first project.

I have now got the following situation and even after days, I can not find a solution.

I have 3 Tables:

Advert

advert_id
advert_title
etc

Category

category_id
name
label
etc

advert2category

advert2category_category_id
advert2category_advert_id

An Advert can be in different Categories and different Categories have different Adverts, therefore the table Advert2Category (ManytoMany).

After reading through the www, I have decided that it should be a ManytoMany Bidirectional, with the "owning side" at the Advert Entity.

Don't ask me why I decided that, I still don't understand Doctrine fully. Anyway, I created 3 Entities, but guess I only need Advert and Category Entity.

I now want the following to happen.

I click on a Category and want to see a list of Articles within this category., that means I have to read out the Table advert2category. I have created the Entities, here my Advert Entity:

So here is first my Advert Entity:

namespace Advert\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;

/**
 * Advert
 *
 * @ORM\Table(name="advert")
 * @ORM\Entity
 */

class Advert
{

    /**
     * @var integer
     *
     * @ORM\Column(name="advert_id", type="integer", nullable=false)
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    private $advertId;


    /**
     * @var string
     *
     * @ORM\Column(name="advert_title", type="string", length=255, nullable=true)
     */
     private $advertTitle;


    /**
    * @ORM\ManyToMany(targetEntity="Category", inversedBy="advertCategory", cascade={"persist"})
    * @ORM\JoinTable(name="advert2category",
    * joinColumns={@ORM\JoinColumn(name="advert2category_category_id", referencedColumnName="category_id")},
    * inverseJoinColumns={@ORM\JoinColumn(name="advert2category_advert_id", referencedColumnName="advert_id")}
    * )
    */

    protected $category;

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


    /**
     * Get advertId
     *
     * @return integer 
     */
    public function getAdvertId()
    {
        return $this->advertId;
    }


    /**
     * Set advertTitle
     *
     * @param string $advertTitle
     * @return Advert
     */
    public function setAdvertTitle($advertTitle)
    {
        $this->advertTitle = $advertTitle;

        return $this;
    }

    /**
     * Get advertTitle
     *
     * @return string 
     */
    public function getAdvertTitle()
    {
        return $this->advertTitle;
    }


    /**
     * Set category
     *
     * @param \Advert\Entity\User $category
     * @return Advert
     */
    public function setCategory(\Advert\Entity\Category $category = null)
    {
        $this->category = $category;

        return $this;
    }

    /**
     * Get category
     *
     * @return \Advert\Entity\Category
     */
    public function getCategory()
    {
        return $this->category;
    }
}  

And my Category Entity:

namespace Advert\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;

/**
 * Category
 *
 * @ORM\Table(name="category")
 * @ORM\Entity
 */

class Category
{

    /**
     * @var integer
     *
     * @ORM\Column(name="category_id", type="integer", nullable=false)
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    private $categoryId;

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



    /**
     * @ORM\ManyToMany(targetEntity="Advert", mappedBy="category")
     **/
    private $advertCategory;

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


    /**
     * Get categoryId
     *
     * @return integer 
     */
    public function getCategoryId()
    {
        return $this->categoryId;
    }


    /**
     * Set name
     *
     * @param string $name
     * @return Category
     */
    public function setName($name)
    {
        $this->name = $name;

        return $this;
    }

    /**
     * Get name
     *
     * @return string 
     */
    public function getName()
    {
        return $this->name;
    }
}  

Just as a first test, I have now tried the following in my Controller:

//Below Controller now works to echo the categories ArrayCollection

$data = $this->getEntityManager()->getRepository('Advert\Entity\Advert')->findAll();

foreach($data as $key=>$row)
{

  echo $row->getAdvertTitle();
  echo $row->getUser()->getUsername();
  $categories = $row->getCategory();

  foreach($categories as $row2) {
    echo $row2->getName();
 }

What am I doing wrong here? Can anyone give me an advice? Thank you very much in advance !

Upvotes: 0

Views: 152

Answers (1)

RedactedProfile
RedactedProfile

Reputation: 2808

Honestly, and it's a very honest and fine thing, that this is way overcomplicating what you want to do, but only in specific areas.

If you used Composer to include Doctrine (the recommended way), also include symfony/console and you will get a whole mess of awesome tools to help you on your quest. There is a very specific command that will kick you in your seat for how awesome it is: $ doctrine orm:schema-tool:update --force --dump-sql. This will get Doctrine to run through your Entities (you only need the two) and will generate your tables and even setup the *To* associations for you. Int he case of ManyToOne's it will generate the appropriate Foreign Key schema. In the case of ManyToMany's it will automatically create, AND manage it's own association table, you just need only worry about giving the table a name in the Entity.

I'm not kidding you, Do this. It will make your life worth living.

As for your entity setup, this is all you need:

<?php 

namespace Advert\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;

/**
 * Advert
 *
 * @ORM\Table(name="advert")
 * @ORM\Entity
 */

class Advert
{

    /**
     * @var integer
     *
     * @ORM\Column(name="advert_id", type="integer", nullable=false)
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    private $advertId;


    /**
     * @var string
     *
     * @ORM\Column(name="advert_title", type="string", length=255, nullable=true)
     */
     private $advertTitle;


    /**
    * @ORM\ManyToMany(targetEntity="Category", cascade={"persist"})
    * @JoinTable(name="advert_categories")
    */
    protected $category;


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


    /**
     * Get advertId
     *
     * @return integer 
     */
    public function getAdvertId()
    {
        return $this->advertId;
    }


    /**
     * Set advertTitle
     *
     * @param string $advertTitle
     * @return Advert
     */
    public function setAdvertTitle($advertTitle)
    {
        $this->advertTitle = $advertTitle;

        return $this;
    }

    /**
     * Get advertTitle
     *
     * @return string 
     */
    public function getAdvertTitle()
    {
        return $this->advertTitle;
    }


    /**
     * Set category
     *
     * @param ArrayCollection $category
     * @return Advert
     */
    public function setCategory(ArrayCollection $category)
    {
        $this->category = $category;

        return $this;
    }

    /**
     * Get category
     *
     * @return ArrayCollection
     */
    public function getCategory()
    {
        return $this->category;
    }
}  

Notice that the getters and setters are Documented to Set and Return ArrayCollection, this is important for IDE's and tools that read PHPDoc and Annotations to understand how in-depth PHP class mapping works.

In addition, notice how much simpler the ManyToMany declaration is? The @JoinTable annotation is there to give a name to the table that doctrine will generate and manage. That's all you need!

But now, you probably should remove the $advertCategory property out of the Category Entity. Doctrine is going to auto-hydrate embedded Entities in properties with the Entity Association Mappings.

This is also potentially dangerous as it can result in infinite recursion. Basically, if all you requested was an Advert with ID of 1, it would go in and find ALL of the Category Entities associated to Advert 1, but inside of those Categories it's re-referencing Advert 1, which Doctrine will sub-query for and inject, which will contain a Category association, which will then Grab those categories, and so on and so fourth until PHP kills itself from lack of memory.

Once everything is good to go, and you got some Categories associated with your Advert, using the Getter for your category in the Advert entity will return an array of Category Entities. Simply iterate through them:

foreach($category as $advert->getCategories()) {
   echo $category->getName();
}

or

echo current($advert->getCategories())->getName();

Upvotes: 1

Related Questions