Web hints Navbar toggle

Doctrine and Symfony, setup and usage tutorial

Doctrine is ready to use with Symfony and is the layer between your code and database.

In this tutorial I will explain the setup and usage of Doctrine with Symfony:

  • The installation of Doctrine
  • How to set it up
  • How ORM works
  • How to fetch data using Doctrine
  • Relational data fetching
  • Inserting and updating of data
  • Lifecycle events

How to install Doctrine

Symfony itself is really bare bone, so most components you will have to install yourself.

Luckily, the Symfony developers made it easy since Symfony 4 and you can install most bundles without configuration.

Enter the command:

composer req orm

This is short for composer require doctrine/doctrine-bundle, where ORM is an alias for the full bundle name.

The setup

Since we’re using Symfony 4, the configuration is really easy. In the root of your project the command above create a .env file.

In the file look for the line:

DATABASE_URL=mysql://<username>:<password>@<host>:3306/<database>

Replace username, database, and host with your credentials and that your setup is finished.

How does it work?

I’m not going to explain the internals of Doctrine, just some basic principles. Read more about Doctrine on their documentation.

Doctrine uses entity mapping, to relate your custom entities with the actual database tables. Usually, this mapping is done through annotations:

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

Here the annotation @Column is defining a string (VARCHAR) of length 155, the name of the column is by default the camelCased class property. “Name” in this case.

A different method to annotations is using the .yaml files, while this works perfectly I find the previous method to be cleaner and more verbose.

Read about Doctrine yaml configuration here

How to usage Doctrine in Symfony

Right inside the controller

Usually, we will use repositories to separate our logic from the controllers, but for quick “queries” this might just do the trick.

public function detail()
{
    $post = $this->getDoctrine()->getRepository(Post::class)->find(1);
}

This method above, will fill the $post variable with a Post entity, if one find with ID == 1.

Inside a repository

Now inside a repository things are a bit different, let’s tackle a different problem now though. Let’s fetch a post by the title.

In PostRepository.php:

public function findOneByTitle(string $title)
{
    return $this->createQueryBuilder('p')
        ->where('p.title = :title')
        ->setParameter('title', $title)
        ->getQuery()
        ->getOneOrNullResult();
}

In PostController.php:

/**
 * @Route('posts/{title}')
 */
public function detail(string $title, PostRepository $postRepository)
{
    $post = $postRepository->findOneByTitle($title);
}

Ok, using this logic within a repository might be overkill, but you should understand now that keeping larger queries should be within a repository, not within a controller.

Take note that we’re using Symfony’s autowiring by type-hinting the PostRepository in the detail-function.

Just so u know, findOneByTitle actually already exists on the PostRepository, so even without writing the PostRepository findOneByTitle function, it would work.

But, let’s say you only want to show the active ones?

public function findOneByTitle(string $title)
{
    return $this->createQueryBuilder('p')
        ->where('p.title = :title')
        ->andWhere('p.active = 1')
        ->getQuery()
        ->getOneOrNullResult();
}

Common use-cases

Get last 10 active posts by category.

The post entity has a title, status, and category as properties.

public function getLastActivePosts(Category $category, $limit = 10)
{
    return $this->createQueryBuilder('p')
        ->where('p.active = 1')
        ->andWhere('p.category = :category')
        ->setParameter('category', $category')
        ->orderBy('p.id', 'DESC')
        ->setMaxResults($limit)
        ->getQuery()
        ->getResult();
}

Inner join another table (entity)

What if you only have the title of the category? Inner join to the rescue!

public function getLastActivePosts(string $categoryTitle, $limit = 10)
{
    return $this->createQueryBuilder('p')
        ->where('p.active = 1')
        ->innerJoin('p.category', 'c')
        ->andWhere('c.title' = :categoryTitle')
        ->setParameter('categoryTitle', $categoryTitle)
        ->orderBy('p.id', 'DESC')
        ->setMaxResults($limit)
        ->getQuery()
        ->getResult();
}

Relations

Relations can also be set through annotations, let’s take that same post entity as an example.

class Post
{
    /**
     * @ORM\ManyToOne(targetEntity="Category", inversedBy="posts")
     */
    private $category;
}

On the Category side

class Category
{
    /**
     * @ORM\OneToMany(targetEntity="Post", mappedBy="category")
     */
    private $posts;

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

Well, that’s it! A category can now be mapped to multiple posts and every post can have a single category. This makes inner joins possible as described above or even:

$post->getCategory();

// and

$category->getPosts();

Both of these will work if those methods exist on the entity, or the properties are public.

Insert and update

You probably noticed you didn’t write any queries yet, that’s because Doctrine is doing all the hard lifting for you.

Same goes for inserting and updating, you’re not working with the database but with your entities.

To create a new Post do the following:

$post = new Post();
$post->title = 'My first post';
$post->active = false;
$this->getDoctrine()->getManager()->persist($post); // this will tell Doctrine a Entity has been made.
$this->getDoctrine()->getManager()->flush(); // This will flush/insert everything too the database.

Now ok, this is fine and will work. Once you get the hang of all of this I suggest researching the command bus.

Let’s update the status, so we see the post in the last posts. To update a Post do the following:

$post = $postRepository->find(1);
$post->active = true;
$this->getDoctrine()->getManager()->flush();

Notice that persisting the object is not needed while updating, this is because Doctrine is already aware of the entity.

Lifecycle events

Last but not least, lifecycle events.

Lifecycle events are trigger on prePersist, postPersist, preUpdate, postUpdate and preFlush.
These can be easily used to set createdAt and updateAt timestamps like this:


/**
 * @ORM\HasLifecycleEvents()
 */
class Post {
...
/**
 * @ORM\PrePersist()
 */
public function setCreatedAt()
{
    $this->createdAt = $this-updatedAt = new DateTime();
}

/**
 * @ORM\PreUpdate()
 */
public function setUpdatedAt()
{
    $this->updatedAt = new DateTime();
}

This can also be used for file uploading.

Every single time the Post entity gets persisted, the setCreatedAt function will be triggered, also the setUpdatedAt method will be triggered on preUpdate.

So no need to worry about updating these dates, as we tend to forget that as developers.

Have any questions about this article?