Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

The 4 (2?) Possible Relation Types

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $10.00

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:

... 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:

... 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:

... 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:

... 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:

... lines 1 - 5
use Gedmo\Mapping\Annotation as Gedmo;
... lines 7 - 63

Let's also make the slug column unique:

... 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:

... 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:

... 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:

... 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:

... 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:

... 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:

... 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.

Leave a comment!

11
Login or Register to join the conversation

"Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes"
I've got this error when I tried to migrate. The migration executes just fine if I remove "unique=true" from the annotation. Any ideas how to fix it? It looks like a bug in Symfony (my version is 4.0.14).

1 Reply

Hey boykodev!

Try making that column length a little shorter - like length=180. The problem is InnoDB + using the utf8mb4 encoding. It ultimately tries to make a key length that is longer than is supported. If you're able to shorten the field length, it also makes the key a bit shorter.

Here is a discussion about that, from FOSUserBundleL https://github.com/FriendsO...

I'm a little surprised I didn't have that issue during this tutorial - I have seen it in other places.

Cheers!

2 Reply
Symfonybeard Avatar

Thanks for this hint. I ran into the same problem. Shortening the length to 180 worked for me. I edited the VersionXXXXX.php file within the Migrations folder and executed the php bin/console doctrine:migrations:migrate afterwards without errors.

Reply
Maik T. Avatar

this solution works fine

Reply

That problem occurs when you are on MySql 5.6 and using "utf8mb4" charset, if you upgrade to MySql 5.7 then you won't have that problem again.

Cheers!

Reply
David B. Avatar
David B. Avatar David B. | posted 3 years ago

Hey guys, what's up!
I often ask myself, is there more simple way to create some dummy data to test applications instead of Fixtures? Do not misunderstand me, but I find Eloquent/Factory more friendly (simple) and flexible than Fixtures (Doctrine's or Codeception's, I've been used both). Do doctrine has the same or an analog?

Reply

Hey David B.!

I couldn't agree more - I've always thought Doctrine fixtures were clunky - and it's been on my list for a long time to improve it. But, that's *also* why we added our own mini-fixture system on top of Doctrine in this tutorial. I think we finish/user that system in this video - https://symfonycasts.com/sc... - you can see how you can create a collection of (for example) Article objects by using fake data inside a callback. You don't return an array like an Eloquent factory, but that's due to the way Doctrine is built - you tend to deal more directly with your objects vs returning arrays and having Eloquent magically put those onto your objects (it does look good though in the factory)!

Does the system we create in this tutorial somewhat address what you're thinking? I'd love to know!

Cheers!

Reply
David B. Avatar
David B. Avatar David B. | weaverryan | posted 3 years ago | edited

Thanks for your answer weaverryan ! I'm not sure that Fixtures are completely clear for me to use it without any problems, but I hope they will. The main misunderstanding point for me is generic types while getting a reference to relation, saving references and their naming.
I think we should pass it ($className . '_') to it's own method which will return the reference prefix. This also could change in the future. BTW, we don't need to pass the $entityManager as an argument and to the callback because we an use $this->manager, am I right?

Reply

Hey David B.!

Sorry for my slow reply!

I think we should pass it ($className . '_') to it's own method which will return the reference prefix. This also could change in the future.

I didn't quite understand what you mean by this part. Could you tell me more? If you fully create the BaseFixture class like we do on that tutorial, you should never need to worry about setting references (it's done in the background for you when you use the $this->createMany() method) and you can use $this->getRandomReference() to fetch a random reference. But if you find ways to improve the data for your own use, please do :).

BTW, we don't need to pass the $entityManager as an argument and to the callback because we an use $this->manager, am I right?

Correct! They are exactly the same object - so you can use either. Using $this->manager is just easier in this case (because you don't need to pass anything to the callback directly).

Cheers!

Reply
Otto K. Avatar
Otto K. Avatar Otto K. | posted 4 years ago

I didn't know my mistake or not, but if I didn't write: use Doctrine\Bundle\FixturesBundle\BaseFixture;
I can't do doctrine:fixtures:load

The autoloader expected class "App\DataFixtures\TagsFixtures" to be defined in file "\vendor\composer/../../src\DataFixtures\TagsFixtures.php". The file was found but the class was not in it, the class name or namespace probably has a typo.

Reply

Hey Student,

This error looks like you have namespace problems. Please, open "src\DataFixtures\TagsFixtures.php" file and make sure it has a proper namespace i.e. "App\DataFixtures\TagsFixtures".

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

The course is built on Symfony 4, but the principles still apply perfectly to Symfony 5 - not a lot has changed in the world of relations!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
        "knplabs/knp-paginator-bundle": "^2.7", // v2.7.2
        "knplabs/knp-time-bundle": "^1.8", // 1.8.0
        "nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.1.4
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.0.4
        "symfony/console": "^4.0", // v4.0.14
        "symfony/flex": "^1.0", // v1.17.6
        "symfony/framework-bundle": "^4.0", // v4.0.14
        "symfony/lts": "^4@dev", // dev-master
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/twig-bundle": "^4.0", // v4.0.4
        "symfony/web-server-bundle": "^4.0", // v4.0.4
        "symfony/yaml": "^4.0", // v4.0.14
        "twig/extensions": "^1.5" // v1.5.1
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.0.2
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.4
        "fzaninotto/faker": "^1.7", // v1.7.1
        "symfony/debug-bundle": "^3.3|^4.0", // v4.0.4
        "symfony/dotenv": "^4.0", // v4.0.14
        "symfony/maker-bundle": "^1.0", // v1.4.0
        "symfony/monolog-bundle": "^3.0", // v3.1.2
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.0.4
        "symfony/profiler-pack": "^1.0", // v1.0.3
        "symfony/var-dumper": "^3.3|^4.0" // v4.0.4
    }
}
userVoice