Doctrine 2 Many to Many Associations

This article will provide detailed instructions on how to setup Doctrine 2 Many to Many associations that automatically cascade the changes to their relationships.

The instructions below assume you have a basic understanding of PHP, Doctrine 2 ORM, and database table structures, but I will briefly go over what Doctrine 2 and many to many relationships are, and the basic creation of a many to many table relationship.

What is Doctrine 2?

Doctrine 2 is a powerful and easy to use Database Abstraction Layer (DBAL) and Object Relationship Mapping(ORM) library for writing PHP applications that utilize a Database Management System (DBMS). You can find more on Doctrine at theirwebsite.

One of the caveats of Doctrine 2 ORM is that certain features that one may expect to function out of the box simply don’t. Instead the ORM relies on the developer of the application to program the desired functionality.

Such is the case with Doctrine Many to Many associations between their relationship tables not automatically cascading, unless the entity is written to do so or the developer writes the model to specifically execute the desired functionality for each entity object.

 

Top of page

 

What is a Many to Many Relationship?

A many to many relationship is used in data normalization in order to reduce table size by lowering the amount of duplicate data.

For example, if you have multiple movies and multiple categories and the movies and categories are associated with each other. Meaning a movie can have multiple categories and a category can have multiple movies.

Instead of creating a single table for movies and creating a new row for each category the movie is under, a separate table can be used to store the relationships between movie and category, otherwise known as a Junction Table.

many-to-many
Many to Many relationship using a junction table.

To learn more about many to many relationships in regards to associative entity mapping please see the wiki article.

 

Top of page

 

Creating a Many to Many Relationship.

Normally a Many to Many relationship involves 3 tables. Two tables to house the data, and a third table to house the relationship. The relationship table only contains 2 columns that are combined as a primary key. If the table that establishes the relationship contains additional columns, it is not a true many to many relationship and should be handled as a One to Many and Many to One relationship. I will be writing another article covering this functionality soon.

As in my example above we have movies and categories, these will be the two data tables. A third table is needed for the relationship, usually named by combining the two tables names, with the owning table first. For this example we will say that a movie is the owner to the categories.

The end result is a table filled with movies, a table filled with categories, and a table pointing to each table’s identifiers in order to define the relationship like so.

movie
Table
 id title
1 Star Wars
2 Spaceballs
SQL

CREATE TABLE `movie` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`title` VARCHAR(50) NOT NULL,
PRIMARY KEY (`id`),
INDEX `title` (`title`)
);

INSERT INTO `movie` (`id`, `title`) VALUES (1, 'Star Wars'),(2, 'Spaceballs');

category
Table
id name
1 Action
2 Comedy
SQL

CREATE TABLE `category` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(50) NOT NULL,
PRIMARY KEY (`id`),
INDEX `name` (`name`)
);

INSERT INTO `category` (`id`, `name`) VALUES (1, 'Action'),(2, 'Comedy');

Table
movie category
1  1
2  1
2  2

 

This creates the relationships of Star Wars = Action and Spaceballs = Action+Comedy.

SQL

(use your database to establish foreign keys)


CREATE TABLE `movie_category` (
`movie` INT(11) NOT NULL,
`category` INT(11) NOT NULL,
PRIMARY KEY (`movie`, `category`),
INDEX `FK_movie_category_category` (`category`),
INDEX `FK_movie_category_movie` (`movie`),
CONSTRAINT `FK_movie_category_movie` FOREIGN KEY (`movie`) REFERENCES `movie` (`id`) ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT `FK_movie_category_category` FOREIGN KEY (`category`) REFERENCES `category` (`id`) ON UPDATE CASCADE ON DELETE CASCADE
)
INSERT INTO `movie_category` (`movie`, `category`) VALUES (1, 1),(2, 1),(2, 2);

 

Top of page

 

Define Doctrine 2 Many to Many Associations

Those of you who have been using Doctrine 2 have probably already created a table relationship using association mapping.

In order to define the relationships I will be creating the entities with PHP annotations. A few edits to the auto generated add/remove methods will ensure any changes to the relationships are carried over.

 

Movie

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class Movie
{
    /**
     * @var int
     * @ORM\Id @ORM\Column(type="integer") @ORM\GeneratedValue
     */
    private $id;

    /**
     * @var string
     * @ORM\Column(type="string")
     */
    private $title;

    /**
     * @var \Doctrine\Common\Collections\ArrayCollection|Category[]
     * @ORM\ManyToMany(targetEntity="Category", inversedBy="movies")
     * @ORM\JoinTable(
     *  name="movie_category",
     *  joinColumns={
     *      @ORM\JoinColumn(name="movie", referencedColumnName="id")
     *  },
     *  inverseJoinColumns={
     *      @ORM\JoinColumn(name="category", referencedColumnName="id")
     *  }
     * )
     */
    private $categories;

    public function __construct()
    {
        $this->categories = new \Doctrine\Common\Collections\ArrayCollection();
    }

    /**
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * @return string
     */
    public function getTitle()
    {
        return $this->title;
    }

    /**
     * @param string $title
     * @return Movie
     */
    public function setName($title)
    {
        $this->title = $title;

        return $this;
    }

    /**
     * @return \Doctrine\Common\Collections\ArrayCollection|Category[]
     */
    public function getCategories()
    {
        return $this->categories;
    }

    /**
     * @param Category $category
     */
    public function removeCategory(Category $category)
    {
        if (false === $this->categories->contains($category)) {
            return;
        }
        $this->categories->removeElement($category);
        $category->removeMovie($this);
    }

    /**
     * @param Category $category
     */
    public function addCategory(Category $category)
    {
        if (true === $this->categories->contains($category)) {
            return;
        }
        $this->categories->add($category);
        $category->addMovie($this);
    }
}

 

Category

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class Category
{

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

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

    /**
     * @var \Doctrine\Common\Collections\ArrayCollection|Movie[]
     * @ORM\ManyToMany(targetEntity="Movie", mappedBy="categories")
     */
    private $movies;

    public function __construct()
    {
        $this->movies = new \Doctrine\Common\Collections\ArrayCollection();
    }

    /**
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

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

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

        return $this;
    }

    /**
     * @return \Doctrine\Common\Collections\ArrayCollection|Movie[]
     */
    public function getMovies()
    {
        return $this->movies;
    }

    /**
     * @param Movie $movie
     */
    public function removeMovie(Movie $movie)
    {
        if (false === $this->movies->contains($movie)) {
            return;
        }
        $this->movies->removeElement($movie);
        $movie->removeCategory($this);
    }

    /**
     * @param Movie $movie
     */
    public function addMovie(Movie $movie)
    {
        if (true === $this->movies->contains($movie)) {
            return;
        }
        $this->movies->add($movie);
        $movie->addCategory($this);
    }
}

 

Top of page

 

Working with the relationships

Now that the entities are set to add or remove the relationships automatically you only need to call either one of the desired add or remove methods.

In the snippets below I use getReference() which reduces the work doctrine has to do by using a proxy instead of making a call to the database.
See more about reference proxies in the Doctrine Advanced Configuration.

 

Removing an associated relationship

Here I demonstrate the ability to remove a category from a movie or remove a movie from a category.

In either instance the Action category is removed from the movie Spaceballs.

Remove a category from a movie
$movie = $entityManager->getRepository('Path\To\Entities\Movie')->findOneByTitle('Spaceballs');
$category = $entityManager->getReference('Path\To\Entities\Category', 1);
$movie->removeCategory($category);
$entityManager->flush();

 

Remove a movie from a category
$category = $entityManager->getRepository('Path\To\Entities\Category')->findOneByName('Action');
$movie = $entityManager->getReference('Path\To\Entities\Movie', 2);
$category->removeMovie($movie);
$entityManager->flush();

 

Adding an associated relationship

Here I demonstrate the ability to add a category to a movie or add a movie to a category.

Add a category to a movie

Creates a new category Drama and Adds the category to the movie Star Wars

$category = new Category;
$category->setName('Drama');
$movie = $entityManager->getRepository('Path\To\Entities\Movie')->findOneByTitle('Star Wars');
$movie->addCategory($category);
$entityManager->persist($category);
$entityManager->flush();

 

Add a movie to a category

Creates a new movie Indiana Jones and adds the the new movie to the category Action

$movie = new Movie;
$movie->setTitle('Indiana Jones');
$category = $entityManager->getRepository('Path\To\Entities\Category')->findOneByName('Action');
$category->addMovie($movie);
$entityManager->persist($movie);
$entityManager->flush();

PHP Doctrine ZF2 : Generate YML

<?php
include ‘vendor/autoload.php’;
$classLoader = new \Doctrine\Common\ClassLoader(‘Entities’, DIR);
$classLoader->register();
$classLoader = new \Doctrine\Common\ClassLoader(‘Proxies’, DIR);
$classLoader->register();
// config
$config = new \Doctrine\ORM\Configuration();
$config->setMetadataDriverImpl($config->newDefaultAnnotationDriver(DIR . ‘/Entities’));
$config->setMetadataCacheImpl(new \Doctrine\Common\Cache\ArrayCache);
$config->setProxyDir(DIR . ‘/Proxies’);
$config->setProxyNamespace(‘Proxies’);
$connectionParams = array(
‘driver’ => ‘pdo_mysql’,
‘host’ => ‘localhost’,
‘port’ => ‘3306’,
‘user’ => ‘root’,
‘password’ => ‘root’,
‘dbname’ => ‘db_dev’,
‘charset’ => ‘utf8’,
);

$em = \Doctrine\ORM\EntityManager::create($connectionParams, $config);
// custom datatypes (not mapped for reverse engineering)
$em->getConnection()->getDatabasePlatform()->registerDoctrineTypeMapping(‘set’, ‘string’);
$em->getConnection()->getDatabasePlatform()->registerDoctrineTypeMapping(‘enum’, ‘string’);
// fetch metadata
$driver = new \Doctrine\ORM\Mapping\Driver\DatabaseDriver(
$em->getConnection()->getSchemaManager()
);

$em->getConfiguration()->setMetadataDriverImpl($driver);

$cmf = new  \Doctrine\ORM\Tools\DisconnectedClassMetadataFactory();
$cmf->setEntityManager($em);
$metadata = $cmf->getAllMetadata();
$cme = new \Doctrine\ORM\Tools\Export\ClassMetadataExporter();
$exporter = $cme->getExporter(‘yml’, DIR.’/yml’);
$exporter->setMetadata($metadata);
$exporter->export();
print ‘Done!’;

PHP Doctrain ZF2 : Learning

I’ve just started looking at Zend Framework 2 and Doctrine ORM, with a view to possibly using it in one of my future products. So far I have a very simple application which allows the user to select data from a ‘configuration’ table. This table has just one row and several columns with settings that the application will use at some point.

So far this is what I have come up with:

ConfigController.php (my Zend controller) Continue reading