If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeOfficially, there are four types of relations in Doctrine: ManyToOne
, OneToMany
, OneToOne
and ManyToMany
. But... that's kind of a lie! In reality, there are only two types.
Let me explain. We already know that a ManyToOne
relationship and a OneToMany
relationship are really just the same relationship seen from the two different sides. So that means that instead of four different types of relations, there are really only three.
But... the OneToOne
relationship is... kind of not a different relationship.
For example, you decide to add a OneToOne
relationship from a User
entity to a Profile
entity... which would hold more data about that user. If you did this, in the database, your user
table would have a profile_id
foreign key column. But wait: isn't that exactly what a ManyToOne
relationship looks like?
Yup! In reality, a OneToOne
relationship is the same as a ManyToOne
, except that Doctrine puts a unique key on the profile_id
column to prevent a single profile from being being linked to multiple users. But... that's really the only difference!
And, by the way, I try to avoid OneToOne
relationships. Instead of splitting user data across two different entities, I tend to put it all in one class to reduce complexity. Splitting into two different entities could help performance, but I think it's almost always more of a bother than a help. Wait until you have real performance problems and then debug it.
Anyways, this means that ManyToOne
, OneToMany
and OneToOne
are all... really just the same relationship! That leaves only ManyToMany
, which is a bit different. So let's build one!
Imagine that every Question
can get be tagged with text descriptors.
In order to store tags in the database, let's make a Tag
entity. Spin over to your console and run:
symfony console make:entity
Call the new entity Tag
... and it's going to be real simple: a single field called name
that will be a string
type, 255
length, not nullable. Hit enter again to finish up.
Before I generate that migration, open up the new Tag
class...
... lines 1 - 2 | |
namespace App\Entity; | |
use App\Repository\TagRepository; | |
use Doctrine\ORM\Mapping as ORM; | |
/** | |
* @ORM\Entity(repositoryClass=TagRepository::class) | |
*/ | |
class Tag | |
{ | |
/** | |
* @ORM\Id | |
* @ORM\GeneratedValue | |
* @ORM\Column(type="integer") | |
*/ | |
private $id; | |
/** | |
* @ORM\Column(type="string", length=255) | |
*/ | |
private $name; | |
public function getId(): ?int | |
{ | |
return $this->id; | |
} | |
public function getName(): ?string | |
{ | |
return $this->name; | |
} | |
public function setName(string $name): self | |
{ | |
$this->name = $name; | |
return $this; | |
} | |
} |
because you know that I love to use TimestampableEntity
.
... lines 1 - 6 | |
use Gedmo\Timestampable\Traits\TimestampableEntity; | |
... lines 8 - 11 | |
class Tag | |
{ | |
use TimestampableEntity; | |
... lines 16 - 43 | |
} |
We could also add a slug
column if we wanted to be able to go to a nice url like /tags/{slug}
to show all the questions related a slug. I won't do that... mostly because we showed how to do that in the last tutorial: how to generate a slug
automatically from some other property.
Ok: we now have a functional Tag
entity. So let's generate a migration for it:
symfony console make:migration
Beautiful! Go give it a quick peek to make sure nothing funny snuck in. Nope! That looks boring: CREATE TABLE tag
with id
, name
and the date fields.
... lines 1 - 6 | |
use Gedmo\Timestampable\Traits\TimestampableEntity; | |
... lines 8 - 11 | |
class Tag | |
{ | |
use TimestampableEntity; | |
... lines 16 - 43 | |
} |
Go run it:
symfony console doctrine:migrations:migrate
Awesomesauce. So let's think about our goal: each Question
could have many tags... and each Tag
could be related to many questions. In other words, this is a many to many relationship. Next: let's generate that and see what it looks like!
// composer.json
{
"require": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*",
"babdev/pagerfanta-bundle": "^3.3", // v3.3.0
"composer/package-versions-deprecated": "^1.11", // 1.11.99.3
"doctrine/doctrine-bundle": "^2.1", // 2.4.2
"doctrine/doctrine-migrations-bundle": "^3.0", // 3.1.1
"doctrine/orm": "^2.7", // 2.9.5
"knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
"knplabs/knp-time-bundle": "^1.11", // v1.16.1
"pagerfanta/doctrine-orm-adapter": "^3.3", // v3.3.0
"pagerfanta/twig": "^3.3", // v3.3.0
"sensio/framework-extra-bundle": "^6.0", // v6.2.1
"stof/doctrine-extensions-bundle": "^1.4", // v1.6.0
"symfony/asset": "5.3.*", // v5.3.4
"symfony/console": "5.3.*", // v5.3.7
"symfony/dotenv": "5.3.*", // v5.3.7
"symfony/flex": "^1.3.1", // v1.17.5
"symfony/framework-bundle": "5.3.*", // v5.3.7
"symfony/monolog-bundle": "^3.0", // v3.7.0
"symfony/runtime": "5.3.*", // v5.3.4
"symfony/stopwatch": "5.3.*", // v5.3.4
"symfony/twig-bundle": "5.3.*", // v5.3.4
"symfony/validator": "5.3.*", // v5.3.14
"symfony/webpack-encore-bundle": "^1.7", // v1.12.0
"symfony/yaml": "5.3.*", // v5.3.6
"twig/extra-bundle": "^2.12|^3.0", // v3.3.1
"twig/string-extra": "^3.3", // v3.3.1
"twig/twig": "^2.12|^3.0" // v3.3.2
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.0
"symfony/debug-bundle": "5.3.*", // v5.3.4
"symfony/maker-bundle": "^1.15", // v1.33.0
"symfony/var-dumper": "5.3.*", // v5.3.7
"symfony/web-profiler-bundle": "5.3.*", // v5.3.5
"zenstruck/foundry": "^1.1" // v1.13.1
}
}