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.