Search code examples
phpdoctrine-ormorm

Doctrine2 ManyToMany bidirectionnal attribute populated by find method


I want to create my entities from an existing database, it's an N - N relation which create articlesCategories table.

I currently have 2 entities, Article and Category, and I want to have a ManyToMany bidirectional relationship.

But when I try to get article or category with a findByXXX or findOneByXXX method, my ManyToMany attribute is NULL or I have an ORM Exception : Entity 'App\Models\Article' has no field 'categories'. You can therefore not call 'findByCategories' on the entities' repository

Database :

Table article : idArticle, name, description, priceHT, size

Table category : idCategory, name, percentTaxe

Table articlesCategories : idArticle, idCategory

Entities :

Category

/**
* @Entity
* @Table(name="category")
*/
class Category
{
/**
 * @var integer
 *
 * @Id
 * @Column(name="idCategory", type="integer", nullable=false)
 * @GeneratedValue(strategy="AUTO")
 */
private $id;

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


/**
 * @var string
 * @Column(name="percentTaxe",type="decimal", precision=10, scale=0, nullable=false)
 */
private $percentTaxe;

/*
 * Many categories have many articles
 * @ManyToMany(targetEntity="App\Models\Article", inversedBy="categories")
 * @JoinTable(name="articlesCategories",
 *  joinColumns={@JoinColumn(name="idCategory", referencedColumnName="idCategory")},
 *  inverseJoinColumns={@JoinColumn(name="idArticle", referencedColumnName="idArticle")}
 * )
*/
private $articles;


/*Constructor*/
public function __construct(){
    $toto = "toto";
    var_dump($toto);
    $this->articles = new ArrayCollection();
}

/***************************
    Getters / Setters
****************************/

public function getId(){
    return $this->id;
}
public function getName(){
    return $this->name;
}
public function getPercentTaxe(){
    return $this->percentTaxe;
}
public function getArticles(){
    return $this->articles;
}

/************************/

public function setId($id){
    $this->id = $id;
}
public function setName($name){
    
    $this->name = htmlspecialchars($name);  
}
public function setPercentTaxe($percentTaxe){
    $this->percentTaxe = htmlspecialchars($percentTaxe);    
}
public function setArticles(\Doctrine\Common\Collections\Collection $articles)
{
    $this->articles = $articles;
}

/***************************
    Getters / Setters
****************************/

public function addArticle(App\Models\Article $article)
{
    var_dump($article);
    $article->addCategory($this); // synchronously updating inverse side
    $this->articles[] = $article;
}

Article

/**
* @Entity
* @Table(name="article")
*/
class Article
{
/**
 * @var integer
 *
 * @Id
 * @Column(name="idArticle", type="integer", nullable=false)
 * @GeneratedValue(strategy="AUTO")
 */
private $id;

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

/**
 * @var string
 * @Column(name="description",type="string", nullable=true)
 */
private $description;

/**
 * @var string
 * @Column(name="priceHT",type="decimal", precision=10, scale=3, nullable=false)
 */
private $priceHT;

/**
 * @var string
 * @Column(name="size", type="string", length=3, nullable=true)
 */
private $size;

/*
 * Many articles have many categories
 * @ManyToMany(targetEntity="App\Models\Category", inversedBy="articles")
 * @JoinTable(name="articlesCategories",
 *  joinColumns={@JoinColumn(name="idArticle", referencedColumnName="idArticle")},
 *  inverseJoinColumns={@JoinColumn(name="idCategory", referencedColumnName="idCategory")}
 * )
*/
private $categories;

/*Constructor*/
public function __construct(){
    echo"tata";
    $this->categories = new ArrayCollection();
    echo"tata";
}

/***************************
    Getters / Setters
****************************/

public function getId(){
    return $this->id;
}
public function getName(){
    return $this->name;
}
public function getDescription(){
    return $this->description;
}
public function getPriceHT(){
    return $this->priceHT;
}
public function getSize(){
    return $this->size;
}
public function getCategories(){
    return $this->categories;
}

/************************/

public function setId($id){
    $this->id = $id;
}
public function setName($name){
    
    $this->name = htmlspecialchars($name);  
}
public function setDescription($description){
    $this->description = htmlspecialchars($description);    
}
public function setPriceHT($priceHT){
    $this->priceHT = htmlspecialchars($priceHT);    
}
public function setSize($size){
    $this->size = htmlspecialchars($size);  
}
public function setCategories($categories){
    $this->categories = $categories;
}

/***************************
    Getters / Setters
****************************/

public function addCategory(App\Models\Category $category)
{
    $category->addArticle($this); // synchronously updating inverse side
    $this->categories[] = $category;
}

/************************/

public function hydrate($data)
{
    foreach($data as $key => $value)
    {
        // Get back the setter name which corresponds to the attribute 
        $method = 'set'.ucfirst($key);
        // if the good setter exist.
        if(methodexists($this, $method))
        {
            $this->$method($value);
        }
    }
}
}

Manager

 /**
 * @param category : category of article we want
 * @return an array of Article object or null 
*/
public function getArticlesByCategory($categoryName)
{
    $articles = NULL;
    
    $repository = $this->getEntityManager()->getRepository("App\Models\Category");

    $category = $repository->findOneByName($categoryName);
    var_dump($category);

    if($category != NULL)
    {
        $articles = $category->getArticles();
    }
    return $articles;
}

And when I var_dump my $category, I have:

class App\Models\Category#122 (4) {
  private $id => int(2)
  private $name => string(7) "clothes"
  private $percentTaxe => string(2) "20"
  private $articles => NULL
}

I found my categories and articles are null instead of to be an empty array because of Doctrine create instances of mapped entities without invoking constructor but I don't understand why It doesn't populate it.

I just use Doctrine2, I don't use Symfony.


Solution

  • First, you have an error at your annotations on categories and articles fields of both entities. To be valid they should start with /** not /*.Change:

    /*
    * Many categories have many articles
    

    to

    /**
    * Many categories have many articles
    

    Same for $categories field of Article Entity.

    Also change one of the fields(articles or categories) to be the inverse side of the relation, e.g.

    inversedBy="categories"
    

    to mappedBy="categories"

    On the other part of your question, default EntityRepository find methods(findBy, findOneBy) of the does not support filter by many to many relation(at least not yet). You will have to do the extra effort and use createQueryBuilder, e.g.:

    $qb = $doctrine->getRepository(Article::class)->createQueryBuilder('a');
    
        $qb->select(
                'a',
                'cat'
            )
            ->innerJoin( 'a.categories', 'cat' )
            ->where('cat.id =1');
    

    And better create your own ArticleRepository and CategoryRepository and define your find methods there. 7.8.8. Custom repositories