Symfony: 9 – Doctrine

Symfony comes integrated with Doctrine, a library whose sole goal is to give you powerful tools to make this easy. By convention the database connection is usually configured in an app/config/parameters.yml file.

Introduction

You can have doctrine create the database for you:

$ php app/console doctrine:database:create

One mistake developers make when starting a Symfony2 project is forgetting to setup default charset and collation on their database. They might even remember to do it the very first time, but forget that it’s all gone after running a relatively common command during development:

$ php app/console doctrine:database:drop --force
$ php app/console doctrine:database:create

If you get PHP fatal errors about the allowed memory size you can try to raise it like this:

php -d memory_limit=128M symfony task
php -d memory_limit=128M app/console doctrine:database:create

There’s no way to configure defaults charset and collation inside Doctrine. One way to solve this problem is to configure server-level defaults. Setting UTF8 defaults for MySQL by added a few lines to (typically my.cnf):

[mysqld]
collation-server = utf8_general_ci
character-set-server = utf8

In order to clear the Doctrine caches, use:

# Clear all metadata cache for an entity manager
php app/console doctrine:cache:clear-metadata --env=prod
# Clear all query cache for an entity manager
php app/console doctrine:cache:clear-query --env=prod
# Clear all result cache for an entity manager
php app/console doctrine:cache:clear-result --env=prod

Configuration

Read the configuration reference to find all doctrine configuration settings.

Creating an Entity Class

Suppose you’re building an application where products need to be displayed.You already know that you need a Product object to represent those products. Create this class inside the Entity directory of your AcmeStoreBundle:

// src/Acme/StoreBundle/Entity/Product.php
namespace Acme\StoreBundle\Entity;
 
class Product
{
    protected $name;
    protected $price;
    protected $description;
}

This class (entity) is a basic class that holds data. You can have Doctrine create simple entity classes for you:

$ php app/console doctrine:generate:entity

Add Mapping Information

Doctrine allows you to work with databases in a much more interesting way than just fetching rows of a column-based table into an array. Instead, Doctrine allows you to persist entire objects to the database and fetch entire objects out of the database. This works by mapping a PHP class to a database table, and the properties of that PHP class to columns on the table:

table

For Doctrine to be able to do this, you just have to create metadata, or configuration that tells Doctrine exactly how the Product class and its properties should be mapped to the database. This metadata can be specified in a number of different formats including YAML, XML or directly inside the Product class via annotations:

// src/Acme/StoreBundle/Entity/Product.php
namespace Acme\StoreBundle\Entity;
 
use Doctrine\ORM\Mapping as ORM;
 
/**
* @ORM\Entity
* @ORM\Table(name="product")
*/
class Product
{
    /**
      * @ORM\Column(type="integer")
      * @ORM\Id
      * @ORM\GeneratedValue(strategy="AUTO")
      */
    protected $id;
 
    /**
      * @ORM\Column(type="string", length=100)
      */
    protected $name;
 
    /**
      * @ORM\Column(type="decimal", scale=2)
      */
 
    protected $price;
 
    /**
      * @ORM\Column(type="text")
      */
    protected $description;
}

Note: A bundle can accept only one metadata definition format. For example, it’s not possible to mix YAML metadata definitions with annotated PHP entity class definitions.

Note: The table name is optional and if omitted, will be determined automatically based on the name of the entity class.

Be careful that your class name and properties aren’t mapped to a protected SQL keyword (such as group or user). For example, if your entity class name is Group, then, by default, your table
name will be group, which will cause an SQL error in some engines.

See Doctrine’s Reserved SQL keywords documentation4 on how to properly escape these names. Alternatively, if you’re free to choose your database schema, simply map to a different table name or column name. See Doctrine’s Persistent classes5 and Property Mapping6 documentation.

Even though Doctrine now knows how to persist a Product object to the database, the class itself isn’t really useful yet. Since Product is just a regular PHP class, you need to create getter and setter methods (e.g. getName(), setName()) in order to access its properties (since the properties are protected). Fortunately, Doctrine can do this for you by running:

$ php app/console doctrine:generate:entities Acme/StoreBundle/Entity/Product

This command makes sure that all of the getters and setters are generated for the Product class. This is a safe command – you can run it over and over again: it only generates getters and setters that don’t exist.

The doctrine:generate:entities command saves a backup of the original Product.php named Product.php~. In some cases, the presence of this file can cause a “Cannot redeclare class” error.
It can be safely removed. You can also use the –no-backup option to prevent generating these backup files.

Doctrine can automatically create all the database tables needed for every known entity in your application. To do this, run:

$ php app/console doctrine:schema:update --force

This command generates the SQL statements needed to update the database to where it should be. An even better way to take advantage of this functionality is via migrations, which allow you to
generate these SQL statements and store them in migration classes that can be run systematically on your production server in order to track and migrate your database schema safely and reliably.

Now that you have a mapped Product entity and corresponding product table, you’re ready to persist data to the database. Add the following method to the DefaultController of the bundle:

// src/Acme/StoreBundle/Controller/DefaultController.php
 
use Acme\StoreBundle\Entity\Product;
use Symfony\Component\HttpFoundation\Response;
 
public function createAction()
{
    $product = new Product();
    $product->setName('A Foo Bar');
    $product->setPrice('19.99');
    $product->setDescription('Lorem ipsum dolor');
 
    $em = $this->getDoctrine()->getManager();
    $em->persist($product);
    $em->flush();
 
    return new Response('Created product id '.$product->getId());
}

The persist() method tells Doctrine to ‘manage’ the $product object. This does not actually cause a query to be made to the database (yet).

When the flush() method is called, Doctrine looks through all of the objects that it’s managing to see if they need to be persisted to the database. In this example, the $product object has not been persisted yet, so the entity manager executes an INSERT query and a row is created in the product table.

In fact, since Doctrine is aware of all your managed entities, when you call the flush() method, it calculates an overall changeset and executes the most efficient query/queries possible. For
example, if you persist a total of 100 Product objects and then subsequently call flush(), Doctrine will create a single prepared statement and re-use it for each insert. This pattern is called Unit of Work, and it’s used because it’s fast and efficient.

Note: Doctrine provides a library that allows you to programmatically load testing data into your project (i.e. “fixture data”, see DoctrineFixturesBundle).

Fetching an object back out of the database:

public function showAction($id)
{
    $product = $this->getDoctrine()
      ->getRepository('AcmeStoreBundle:Product')
      ->find($id);
 
    if (!$product) {
      throw $this->createNotFoundException(
        'No product found for id '.$id
      );
    }
 
    // ... do something, like pass the $product object into a template
}

You can achieve the equivalent of this without writing any code by using the @ParamConverter
shortcut. See FrameworkExtraBundle.

When you query for a particular type of object, you always use what’s known as its repository. You can think of a repository as a PHP class whose only job is to help you fetch entities of a certain class. You can access the repository object for an entity class via:

$repository = $this->getDoctrine()
      ->getRepository('AcmeStoreBundle:Product');

Note: The AcmeStoreBundle:Product string is a shortcut you can use anywhere in Doctrine instead of the full class name of the entity (i.e. Acme\StoreBundle\Entity\Product). As long as your entity
lives under the Entity namespace of your bundle, this will work.

Once you have your repository, you have access to all sorts of helpful methods:

// query by the primary key (usually "id")
$product = $repository->find($id);
 
// dynamic method names to find based on a column value
$product = $repository->findOneById($id);
$product = $repository->findOneByName('foo');
 
// find *all* products
$products = $repository->findAll();
 
// find a group of products based on an arbitrary column value
$products = $repository->findByPrice(19.99);

You can also issue complex queries and you can take advantage of the useful findBy and findOneBy methods to easily fetch objects based on multiple conditions:

// query for one product matching be name and price
$product = $repository->findOneBy(array('name' => 'foo', 'price' => 19.99));
 
// query for all products matching the name, ordered by price
$products = $repository->findBy(
    array('name' => 'foo'),
    array('price' => 'ASC')
);

Once you’ve fetched an object from Doctrine, updating it is easy. Suppose you have a route that maps a product id to an update action in a controller:

public function updateAction($id)
{
    $em = $this->getDoctrine()->getManager();
    $product = $em->getRepository('AcmeStoreBundle:Product')->find($id);
 
    if (!$product) {
      throw $this->createNotFoundException(
        'No product found for id '.$id
      );
    }
 
    $product->setName('New product name!');
    $em->flush();
 
    return $this->redirect($this->generateUrl('homepage'));
}

Updating an object involves just three steps:

  • fetching the object from Doctrine
  • modifying the object
  • calling flush() on the entity manager

Notice that calling $em->persist($product) isn’t necessary. Recall that this method simply tells Doctrine to manage or watch the $product object. In this case, since you fetched the $product object
from Doctrine, it’s already managed.

Deleting an object is very similar, but requires a call to the remove() method of the entity manager:

$em->remove($product);
$em->flush();

Doctrine allows you to write more complex queries using the Doctrine Query Language (DQL). DQL is similar to SQL except that you should imagine that you’re querying for one or more
objects of an entity class (e.g. Product) instead of querying for rows on a table (e.g. product).

When querying in Doctrine, you have two options: writing pure Doctrine queries or using Doctrine’s Query Builder. The biggest difference with SQL is that you need to think in terms of objects instead of rows in a database.

Imagine that you want to query for products, but only return products that cost more than 19.99, ordered from cheapest to most expensive. From inside a controller, do the following:

$em = $this->getDoctrine()->getManager();
$query = $em->createQuery(
    'SELECT p
    FROM AcmeStoreBundle:Product p
    WHERE p.price > :price
    ORDER BY p.price ASC'
)->setParameter('price', '19.99');
 
$products = $query->getResult();

The getResult() method returns an array of results. If you’re querying for just one object, you can use the getSingleResult() method instead. The getSingleResult() method throws a Doctrine\ORM\NoResultException exception if no results are returned and a Doctrine\ORM\NonUniqueResultException if more than one result is returned.

If you use this method, you may need to wrap it in a try-catch block and ensure that only one result is returned (if you’re querying on something that could feasibly return more than one result):

$query = $em->createQuery('SELECT ...')
    ->setMaxResults(1);
 
try {
    $product = $query->getSingleResult();
} catch (\Doctrine\Orm\NoResultException $e) {
    $product = null;
}

Take note of the setParameter() method. When working with Doctrine, it’s always a good idea to set any external values as placeholders, which was done in the above query (WHERE p.price > :price). You can then set the value of the price placeholder by calling the setParameter() method:

->setParameter('price', '19.99')

Using parameters instead of placing values directly in the query string is done to prevent SQL injection attacks and should always be done. If you’re using multiple parameters, you can set their
values at once using the setParameters() method:

->setParameters(array(
  'price' => '19.99',
  'name' => 'Foo',
))

Instead of writing the queries directly, you can alternatively use Doctrine’s QueryBuilder to do the same job using a nice, object-oriented interface. From inside a controller:

$repository = $this->getDoctrine()
    ->getRepository('AcmeStoreBundle:Product');
 
$query = $repository->createQueryBuilder('p')
    ->where('p.price > :price')
    ->setParameter('price', '19.99')
    ->orderBy('p.price', 'ASC')
    ->getQuery();
 
$products = $query->getResult();

The QueryBuilder object contains every method necessary to build your query. By calling the
getQuery() method, the query builder returns a normal Query object, which is the same object you built directly in the previous section.

In order to isolate, test and reuse these queries, it’s a good idea to create a custom repository
class for your entity and add methods with your query logic there. To do this, add the name of the repository class to your mapping definition.

// src/Acme/StoreBundle/Entity/Product.php
namespace Acme\StoreBundle\Entity;
 
use Doctrine\ORM\Mapping as ORM;
 
/**
* @ORM\Entity(repositoryClass="Acme\StoreBundle\Entity\ProductRepository")
*/
class Product
{
    //...
}

Doctrine can generate the repository class for you by running the same command used earlier to generate the missing getter and setter methods:

$ php app/console doctrine:generate:entities Acme

Next, add a new method – findAllOrderedByName() – to the newly generated repository class. This method will query for all of the Product entities, ordered alphabetically.
Note that the entity manager can be accessed via $this->getEntityManager() from inside the repository.

// src/Acme/StoreBundle/Entity/ProductRepository.php
namespace Acme\StoreBundle\Entity;
 
use Doctrine\ORM\EntityRepository;
 
class ProductRepository extends EntityRepository
{
  public function findAllOrderedByName()
  {
    return $this->getEntityManager()
    ->createQuery(
      'SELECT p FROM AcmeStoreBundle:Product p ORDER BY p.name ASC'
    )
    ->getResult();
  }
}

When using a custom repository class, you still have access to the default finder methods such as find() and findAll(). You can use this new method just like the default finder methods of the repository:

$em = $this->getDoctrine()->getManager();
$products = $em->getRepository('AppBundle:Product')
    ->findAllOrderedByName();

Entity Relationships/Associations

Suppose that the products in your application all belong to exactly one category. In this case, you’ll need a Category object and a way to relate a Product object to a Category object. Start by creating the Category entity:

$ php app/console doctrine:generate:entity \
    --entity="AppBundle:Category" \
    --fields="name:string(255)"

To relate the Category and Product entities, start by creating a products property on the Category class:

// src/AppBundle/Entity/Category.php
 
// ...
use Doctrine\Common\Collections\ArrayCollection;
 
class Category
{
    // ...
 
    /**
     * @ORM\OneToMany(targetEntity="Product", mappedBy="category")
     */
    protected $products;
 
    public function __construct()
    {
        $this->products = new ArrayCollection();
    }
}

First, since a Category object will relate to many Product objects, a products array property is added to hold those Product objects. Again, this isn’t done because Doctrine needs it, but instead because it makes sense in the application for each Category to hold an array of Product objects.

The code in the __construct() method is important because Doctrine requires the $products property to be an ArrayCollection object. This object looks and acts almost exactly like an array, but has some added flexibility. If this makes you uncomfortable, don’t worry. Just imagine that it’s an array and you’ll be in good shape.

The targetEntity value in the decorator used above can reference any entity with a valid namespace, not just entities defined in the same namespace. To relate to an entity defined in a different class or bundle, enter a full namespace as the targetEntity.

Next, since each Product class can relate to exactly one Category object, you’ll want to add a $category property to the Product class:

// src/AppBundle/Entity/Product.php
 
// ...
class Product
{
    // ...
 
    /**
     * @ORM\ManyToOne(targetEntity="Category", inversedBy="products")
     * @ORM\JoinColumn(name="category_id", referencedColumnName="id")
     */
    protected $category;
}
 
$ php app/console doctrine:generate:entities AppBundle

The metadata above the $category property on the Product class tells Doctrine that the related class is Category and that it should store the id of the category record on a category_id field that lives on the product table. In other words, the related Category object will be stored on the $category property, but behind the scenes, Doctrine will persist this relationship by storing the category’s id value on a category_id column of the product table. The metadata above the $products property of the Category object is less important, and simply tells Doctrine to look at the Product.category property to figure out how the relationship is mapped.

doctrine_image_2

Now you can see this new code in action! Imagine you’re inside a controller:

use AppBundle\Entity\Category;
use AppBundle\Entity\Product;
use Symfony\Component\HttpFoundation\Response;
 
class DefaultController extends Controller
{
    public function createProductAction()
    {
        $category = new Category();
        $category->setName('Main Products');
 
        $product = new Product();
        $product->setName('Foo');
        $product->setPrice(19.99);
        $product->setDescription('Lorem ipsum dolor');
        // relate this product to the category
        $product->setCategory($category);
 
        $em = $this->getDoctrine()->getManager();
        $em->persist($category);
        $em->persist($product);
        $em->flush();
 
        return new Response(
            'Created product id: '.$product->getId()
            .' and category id: '.$category->getId()
        );
    }
}

When you need to fetch associated objects, your workflow looks just like it did before. First, fetch a $product object and then access its related Category. What’s important is the fact that you have easy access to the product’s related category, but the category data isn’t actually retrieved until you ask for the category (i.e. it’s “lazily loaded”):

public function showAction($id)
{
    $product = $this->getDoctrine()
        ->getRepository('AppBundle:Product')
        ->find($id);
 
    $categoryName = $product->getCategory()->getName();
 
    // ...
}

You can also query in the other direction:

public function showProductsAction($id)
{
    $category = $this->getDoctrine()
        ->getRepository('AppBundle:Category')
        ->find($id);
 
    $products = $category->getProducts();
 
    // ...
}

This “lazy loading” is possible because, when necessary, Doctrine returns a “proxy” object in place of the true object. Look again at the above example:

$product = $this->getDoctrine()
    ->getRepository('AppBundle:Product')
    ->find($id);
 
$category = $product->getCategory();
 
// prints "Proxies\AppBundleEntityCategoryProxy"
echo get_class($category);

This proxy object extends the true Category object, and looks and acts exactly like it. The difference is that, by using a proxy object, Doctrine can delay querying for the real Category data until you actually need that data (e.g. until you call $category->getName()).

The proxy classes are generated by Doctrine and stored in the cache directory. And though you’ll probably never even notice that your $category object is actually a proxy object, it’s important to keep it in mind.

If you know up front that you’ll need to access both objects, you can avoid the second query by issuing a join in the original query. Add the following method to the ProductRepository class:

// src/AppBundle/Entity/ProductRepository.php
public function findOneByIdJoinedToCategory($id)
{
    $query = $this->getEntityManager()
        ->createQuery(
            'SELECT p, c FROM AppBundle:Product p
            JOIN p.category c
            WHERE p.id = :id'
        )->setParameter('id', $id);
 
    try {
        return $query->getSingleResult();
    } catch (\Doctrine\ORM\NoResultException $e) {
        return null;
    }
}

Now, you can use this method in your controller to query for a Product object and its related Category with just one query:

public function showAction($id)
{
    $product = $this->getDoctrine()
        ->getRepository('AppBundle:Product')
        ->findOneByIdJoinedToCategory($id);
 
    $category = $product->getCategory();
 
    // ...
}

If you’re using annotations, you’ll need to prepend all annotations with ORM\ (e.g. ORM\OneToMany), which is not reflected in Doctrine’s documentation. You’ll also need to include the use Doctrine\ORM\Mapping as ORM; statement, which imports the ORM annotations prefix.

Sometimes, you need to perform an action right before or after an entity is inserted, updated, or deleted. These types of actions are known as “lifecycle” callbacks, as they’re callback methods that you need to execute during different stages of the lifecycle of an entity (e.g. the entity is inserted, updated, deleted, etc). If you’re using annotations for your metadata, start by enabling the lifecycle callbacks.

/**
 * @ORM\Entity()
 * @ORM\HasLifecycleCallbacks()
 */
class Product
{
    // ...
}

Now, you can tell Doctrine to execute a method on any of the available lifecycle events. For example, suppose you want to set a createdAt date column to the current date, only when the entity is first persisted (i.e. inserted):

// src/AppBundle/Entity/Product.php
 
/**
 * @ORM\PrePersist
 * This example assumes that you've created and mapped a createdAt property (not shown here).
 */
public function setCreatedAtValue()
{
    $this->createdAt = new \DateTime();
}

Right before the entity is first persisted, Doctrine will automatically call this method and the createdAt field will be set to the current date. Notice that the setCreatedAtValue() method receives no arguments. This is always the case for lifecycle callbacks and is intentional: lifecycle callbacks should be simple methods that are concerned with internally transforming data in the entity (e.g. setting a created/updated field, generating a slug value).

If you need to do some heavier lifting – like performing logging or sending an email – you should register an external class as an event listener or subscriber and give it access to whatever resources you need.

DoctrineMigrationsBundle

The doctrine:schema:update –force task should only be used during development. For a more robust method of systematically updating your production database, you should use migrations. The database migrations feature is an extension of the database abstraction layer and offers you the ability to programmatically deploy new versions of your database schema in a safe, easy and standardized way.

The end goal of writing migrations is to be able to use them to reliably update your database structure when you deploy your application. By running the migrations locally (or on a beta server), you can ensure that the migrations work as you expect.


Leave a Reply