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 SubscribeRemember 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
.
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.
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.
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.
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!
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.
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!
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?
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!
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?
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!
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.
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!
// 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
}
}
"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).