Buy Access to Course
16.

The 4 (2?) Possible Relation Types

Share this awesome video!

|

Keep on Learning!

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Remember way back when we used the make:entity command to generate the relationship between Comment and Article? When we did this, the command told us that there are four different types of relationships: ManyToOne, OneToMany, ManyToMany and OneToOne.

But, that's not really true... and the truth is a lot more interesting. For example, we quickly learned that ManyToOne and OneToMany are really two different ways to refer to the same relationship! Comment has a ManyToOne relationship to Article:

96 lines | src/Entity/Comment.php
// ... lines 1 - 10
class Comment
{
// ... lines 13 - 31
/**
* @ORM\ManyToOne(targetEntity="App\Entity\Article", inversedBy="comments")
* @ORM\JoinColumn(nullable=false)
*/
private $article;
// ... lines 37 - 94
}

But that same database relationship can be described as a OneToMany from Article to Comment.

OneToOne: The Cousin of ManyToOne

This means that there are truly only three different types of relationships: ManyToOne, ManyToMany and OneToOne. Um, ok, this is embarrassing. That's not true either. Yea, A OneToOne relationship is more or less the same as a ManyToOne. OneToOne is kind of weird. Here's an example: suppose you have a User entity and you decide to create a Profile entity that contains more data about that one user. In this example, each User has exactly one Profile and each Profile is linked to exactly one User.

But, in the database, this looks exactly like a ManyToOne relationship! For example, our ManyToOne relationship causes the comment table to have an article_id foreign key column. If you had a OneToOne relationship between some Profile and User entities, then the profile table would have a user_id foreign key to the user table. The only difference is that doctrine would make that column unique to prevent you from accidentally linking multiple profiles to the same user.

The point is, OneToOne relationships are kind of ManyToOne relationships in disguise. They also not very common, and I don't really like them.

The 2 Types of Relationships

So, really, if you are trying to figure out which relationship type to use in a situation... well... there are only two types: (1) ManyToOne/OneToMany or (2) ManyToMany.

For ManyToMany, imagine you have a Tag entity and you want to be able to add tags to articles. So, each article will have many tags. And, each Tag may be related to many articles. That is a ManyToMany relationship. And that is exactly what we're going to build.

Building the Tag Entity

Let's create the new Tag entity class first. Find your terminal and run:

php bin/console make:entity

Name it Tag and give it two properties: name, as a string and slug also as a string, so that we can use the tag in a URL later.

Cool! Before generating the migration, open the new class:

58 lines | src/Entity/Tag.php
// ... lines 1 - 2
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass="App\Repository\TagRepository")
*/
class Tag
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=255)
*/
private $name;
/**
* @ORM\Column(type="string", length=180)
*/
private $slug;
public function getId()
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function getSlug(): ?string
{
return $this->slug;
}
public function setSlug(string $slug): self
{
$this->slug = $slug;
return $this;
}
}

No surprises: name and slug. At the top, use our favorite TimestampableEntity trait:

63 lines | src/Entity/Tag.php
// ... lines 1 - 6
use Gedmo\Timestampable\Traits\TimestampableEntity;
// ... lines 8 - 11
class Tag
{
use TimestampableEntity;
// ... lines 15 - 61
}

And, just like we did in Article, configure the slug to generate automatically. Copy the slug annotation and paste that above the slug property:

63 lines | src/Entity/Tag.php
// ... lines 1 - 11
class Tag
{
// ... lines 14 - 27
/**
* @ORM\Column(type="string", length=180, unique=true)
* @Gedmo\Slug(fields={"name"})
*/
private $slug;
// ... lines 33 - 61
}

Oh, but we need a use statement for the annotation. An easy way to add it is to temporarily type @Slug on the next line and hit tab to auto-complete it. Then, delete it: that was enough to make sure the use statement was added on top:

63 lines | src/Entity/Tag.php
// ... lines 1 - 5
use Gedmo\Mapping\Annotation as Gedmo;
// ... lines 7 - 63

Let's also make the slug column unique:

63 lines | src/Entity/Tag.php
// ... lines 1 - 11
class Tag
{
// ... lines 14 - 27
/**
* @ORM\Column(type="string", length=180, unique=true)
// ... line 30
*/
private $slug;
// ... lines 33 - 61
}

Great! The entity is ready. Go back to your terminal and make that migration!

php bin/console make:migration

Whoops! My bad! Maybe you saw my mistake. Change the Slug annotation from title to name:

63 lines | src/Entity/Tag.php
// ... lines 1 - 11
class Tag
{
// ... lines 14 - 27
/**
// ... line 29
* @Gedmo\Slug(fields={"name"})
*/
private $slug;
// ... lines 33 - 61
}

Generate the migration again:

php bin/console make:migration

Got it! Open that class to make sure it looks right:

29 lines | src/Migrations/Version20180501142420.php
// ... lines 1 - 2
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
class Version20180501142420 extends AbstractMigration
{
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
$this->addSql('CREATE TABLE tag (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, slug VARCHAR(180) NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, UNIQUE INDEX UNIQ_389B783989D9B62 (slug), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
$this->addSql('DROP TABLE tag');
}
}

Yep: CREATE TABLE tag. Go run it:

php bin/console doctrine:migrations:migrate

Tip

If you get an error like:

Syntax error or access violation: 1071 Specified key was too long...

No worries! Just open your entity and modify the @Column annotation above the $slug property: set length=180. Then, remove the migration and re-run the last 2 commands. Or update MySQL to version 5.7 or higher.

Now that the entity & database are setup, we need some dummy data! Run:

php bin/console make:fixtures

Call it TagFixture. Then, like always, open that class so we can tweak it:

18 lines | src/DataFixtures/TagFixture.php
// ... lines 1 - 2
namespace App\DataFixtures;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\Persistence\ObjectManager;
class TagFixture extends Fixture
{
public function load(ObjectManager $manager)
{
// $product = new Product();
// $manager->persist($product);
$manager->flush();
}
}

First, extend BaseFixture, rename load() to loadData() and make it protected:

19 lines | src/DataFixtures/TagFixture.php
// ... lines 1 - 7
class TagFixture extends BaseFixture
{
protected function loadData(ObjectManager $manager)
{
// ... lines 12 - 16
}
}

We also don't need this use statement anymore. Call our trusty $this->createMany() to create 10 tags:

19 lines | src/DataFixtures/TagFixture.php
// ... lines 1 - 4
use App\Entity\Tag;
// ... lines 6 - 7
class TagFixture extends BaseFixture
{
protected function loadData(ObjectManager $manager)
{
$this->createMany(Tag::class, 10, function(Tag $tag) {
// ... line 13
});
$manager->flush();
}
}

For the name, use $tag->setName() with $this->faker->realText() and 20, to get about that many characters:

19 lines | src/DataFixtures/TagFixture.php
// ... lines 1 - 7
class TagFixture extends BaseFixture
{
protected function loadData(ObjectManager $manager)
{
$this->createMany(Tag::class, 10, function(Tag $tag) {
$tag->setName($this->faker->realText(20));
});
$manager->flush();
}
}

We could use $this->faker->word to get a random word, but that word would be in Latin. The realText() method will give us a few words, actually, but they will sound, at least "kinda" real.

And, that's all we need! To make sure it works, run:

php bin/console doctrine:fixtures:load

We are ready! Article entity, check! Tag entity, check, check! It's time to create the ManyToMany relationship.