#native_company# #native_desc#
#native_cta#

Using Doctrine ORM with Symfony PHP Framework, Part 2

By Voja Janjic
on May 31, 2016

Welcome to the second part of the Using Doctrine ORM with Symfony PHP Framework tutorial. This section will cover querying object relationships.

Querying Object Relationships

All types of relationships between database tables can be represented in Doctrine classes —  one-to-one, one-to-many and many-to-many. But, before going into details about each of these relationships, we have to mention two important concepts.

Relationship Direction

The first one is the relationship direction. A relationship can be either unidirectional or bidirectional. As their names indicate, unidirectional relationships go only in one direction, while the bidirectional relationship is a two-way relationship. Let’s take a relationship between categories and products for example. If we need to fetch the list of products that belong to a particular category, but we don’t need to fetch a category from a product, you would use a unidirectional relationship. If you need to do both things, i.e. fetch the data in both directions, you would use a bidirectional relationship.

It is possible to use all bidirectional relationships and not worry about this, but it would add unnecessary complexity in large projects.

Owning Side and Inverse Side

In Doctrine, each relationship has one entity as the owning side, and the other as the inverse side of the relationship. This is important because when the relationship needs to be updated, Doctrine will check the owning side to see whether there are any changes.

In one-to-many relationships, the “many” side is always the owning side, while the “one” side is the inverse side. In many-to-many relationships, any side can be the owning side. The owning side in one-to-one relationship is the entity whose database table contains the foreign key.

One-to-Many Relationships

A common example of this type of relation would be the relation between categories and products. A category can have multiple products, while a product can belong to only one category. As you have read in the first part of the tutorial, each database table has a corresponding class in the AppBundle/Entity directory. That means that a relation between tables will become a relation between two entity classes. Instead of columns and foreign keys, we will use properties and annotations. Let’s start with the Category class:

use DoctrineCommonCollectionsArrayCollection;

class Category
{
    /**
     * @ORMOneToMany(targetEntity="Product", mappedBy="category")
     */
    private $products; 

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

Property $products is supposed to hold data about products belonging to the category, i.e. it will be an array of Product objects. Note that, instead of an array, Doctrine requires this property to be an ArrayCollection object. Because of that, it needs to be instantiated in the constructor.

The type of relation is defined in annotation above the property, where targetEntity value represents the name of the class whose objects will be stored in the array (in this case, the Product class), and mappedBy refers to the property that is the other side of the relation (in this case, it is the $category property in Category class).

The Product class would look like this:

class Product
{
    /**
     * @ORMManyToOne(targetEntity="Category", inversedBy="products")
     * @ORMJoinColumn(name="category_id", referencedColumnName="id")
     */
    private $category;
}

As a product can belong to only one category, we don’t need to have an ArrayCollection property to instantiate it. The $category property will hold a single Category object.

Note that we have to define the inverse relation as well. This time, the type of the relation is ManyToOne and targetEntity is the Category class.

After setting up the relation, let’s see how to get the data. First, we will fetch related objects:

$category = $this->getDoctrine()
        ->getRepository('AppBundle:Category')
        ->find($categoryId);

$products = $category->getProducts();

The code above will fetch a category (using the category ID) and then fetch all the products belonging to that category. As discussed in the first part of the tutorial, note that getProducts is a getter function (getter and setter functions are created for each class property) generated by executing the following command:

php app/console doctrine:generate:entities AppBundle/Entity/User

To save related entities, we will do the following:

$category = new Category();
$category->setName('T-shirts');

$product = new Product();
$product->setName('White buttoned t-shirt');
$product->setPrice(19.99);
$product->setDescription('100% cotton');

// relate this product to the category
$product->setCategory($category);

// In case the relationship is bidirectional, we need to update the entities on both sides
$category->getProducts()->add($product);

$em = $this->getDoctrine()->getManager();
$em->persist($category);
$em->persist($product);
$em->flush();

To save the entities in the opposite direction of the relationship:

$category = new Category();
$category->setName('T-shirts');

foreach($products as $product) {
	// In case the relationship is bidirectional, we need to update the entities on both sides
	$product->setCategory($category);

	$category->getProducts()->add ($product);
}

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

Removing the entity can be done in the following way:

$category->getProducts()->removeElement($product);

// In case the relationship is bidirectional, we need to update the entities on both sides
$product->setCategory(null); 

It is also possible to remove all related entities at once:

$category->getProducts()->clear();

These changes will be updated once the object is persisted to the database. However, note that the most common use-cases of deleting related entities can be handled by Doctrine automatically. Read more about orphan removal.

Many-to-Many Relationship

A typical many-to-many relation can be found in the product tags feature. A single product can have multiple tags, while a single tag can also have multiple products.

use DoctrineCommonCollectionsArrayCollection;

class Product
{
    /**
     * @ManyToMany(targetEntity="Tag", inversedBy="products")
     * @JoinTable(name="product_tags")
     */
    private $tags;

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

Tag class would be similar to this:

use DoctrineCommonCollectionsArrayCollection;

class Tag
{
    /**
     * @ManyToMany(targetEntity="Product", mappedBy="tags")
     */
    private $products;

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

}

Saving related entities is similar to one-to-many relationship:

$product = new Product();
$product->setName('White buttoned t-shirt');
$product->setPrice(19.99);
$product->setDescription('100% cotton');

foreach($tags as $tag) {
	// In case the relationship is bidirectional, we need to update the entities on both sides
	$tag->setProduct($product);

	$product->getTags()->add($tag);
}

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

Other database operations work in a similar manner. One important thing to note — in this example, only primary keys from each side are saved in the pivot table. This setup would not be used if you wanted to save additional fields to the pivot table, such as the timestamp. In that case, the pivot table will become an entity class with one-to-many relationships with the two entity classes.

One-to-One Relationship

The one-to-one relationship is basically a parent-child relationship. In other words, the child object is a weak object and cannot exist without the parent. An example of this type of relationship from the Doctrine documentation is the relationship between the customer and the shopping cart. A customer can have only one shopping cart, while one cart can belong to only one customer.

The entity classes would look like this:

class Customer
{
    /**
     * @OneToOne(targetEntity="Cart", mappedBy="customer")
     */
    private $cart;

   // …
}


class Cart
{
    /**
     * @OneToOne(targetEntity="Customer", inversedBy="cart")
     * @JoinColumn(name="customer_id", referencedColumnName="id")
     */
    private $customer;

   // …
}

Saving related entities is simple:

$customer = new Customer();
$customer->setName('John Doe');

$cart = new Cart();

$customer->setCart($cart);

// In case the relationship is bidirectional, we need to update the entities on both sides
$cart->setCustomer($customer);

$em = $this->getDoctrine()->getManager();
$em->persist($customer);
$em->persist($cart);
$em->flush();

Removing the entities is also done in a similar way as with other types of relationships:

$customer->setCart(null);
$cart->setCustomer(null); 

For use-cases that were not covered in this tutorial, check Doctrine ORM documentation.