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 SubscribeHead back to /genus
and click into one of our genuses. Thanks to our hard work, we can link genuses and users. So I know that Eda Farrell is a User
that studies this Genus
.
But, hmm, what if I need to store a little extra data on that relationship, like the number of years that each User
has studied the Genus
. Maybe Eda has studied this Genus
for 10 years, but Marietta Schulist has studied it for only 5 years.
In the database, this means that we need our join table to have three fields now: genus_id
, user_id
, but also years_studied
. How can we add that extra field to the join table?
The answer is simple, you can't! It's not possible. Whaaaaat?
You see, ManyToMany
relationships only work when you have no extra fields on the relationship. But don't worry! That's by design! As soon as your join table need to have even one extra field on it, you need to build an entity class for it.
In your Entity
directory, create a new class: GenusScientist
. Open Genus
and steal the ORM use
statement on top, and paste it here:
... lines 1 - 2 | |
namespace AppBundle\Entity; | |
use Doctrine\ORM\Mapping as ORM; | |
... lines 6 - 10 | |
class GenusScientist | |
{ | |
... lines 13 - 70 | |
} |
Next, add some properties: id
- we could technically avoid this, but I like to give every entity an id
- genus
, user
, and yearsStudied
:
... lines 1 - 2 | |
namespace AppBundle\Entity; | |
use Doctrine\ORM\Mapping as ORM; | |
... lines 6 - 10 | |
class GenusScientist | |
{ | |
... lines 13 - 17 | |
private $id; | |
... lines 19 - 23 | |
private $genus; | |
... lines 25 - 29 | |
private $user; | |
... lines 31 - 34 | |
private $yearsStudied; | |
... lines 36 - 70 | |
} |
Use the "Code"->"Generate" menu, or Command
+N
on a Mac, and select "ORM Class" to generate the class annotations:
... lines 1 - 2 | |
namespace AppBundle\Entity; | |
use Doctrine\ORM\Mapping as ORM; | |
/** | |
* @ORM\Entity | |
* @ORM\Table(name="genus_scientist") | |
*/ | |
class GenusScientist | |
{ | |
... lines 13 - 70 | |
} |
Oh, and notice! This generated a table name of genus_scientist
: that's perfect! I want that to match our existing join table: we're going to migrate it to this new structure.
Go back to "Code"->"Generate" and this time select "ORM Annotation". Generate the annotations for id
and yearsStudied
:
... lines 1 - 10 | |
class GenusScientist | |
{ | |
/** | |
* @ORM\Id | |
* @ORM\GeneratedValue(strategy="AUTO") | |
* @ORM\Column(type="integer") | |
*/ | |
private $id; | |
... lines 19 - 31 | |
/** | |
* @ORM\Column(type="string") | |
*/ | |
private $yearsStudied; | |
... lines 36 - 70 | |
} |
Perfect!
So how should we map the genus
and user
properties? Well, think about it: each is now a classic ManyToOne
relationship. Every genus_scientist
row should have a genus_id
column and a user_id
column. So, above genus
, say ManyToOne
with targetEntity
set to Genus
Below that, add the optional @JoinColumn
with nullable=false
:
... lines 1 - 10 | |
class GenusScientist | |
{ | |
... lines 13 - 19 | |
/** | |
* @ORM\ManyToOne(targetEntity="Genus") | |
* @ORM\JoinColumn(nullable=false) | |
*/ | |
private $genus; | |
... lines 25 - 70 | |
} |
Copy that and put the same thing above user
, changing the targetEntity
to User
:
... lines 1 - 10 | |
class GenusScientist | |
{ | |
... lines 13 - 25 | |
/** | |
* @ORM\ManyToOne(targetEntity="User") | |
* @ORM\JoinColumn(nullable=false) | |
*/ | |
private $user; | |
... lines 31 - 70 | |
} |
And... that's it! Finish the class by going back to the "Code"->"Generate" menu, or Command
+N
on a Mac, selecting Getters and choosing id
:
... lines 1 - 10 | |
class GenusScientist | |
{ | |
... lines 13 - 36 | |
public function getId() | |
{ | |
return $this->id; | |
} | |
... lines 41 - 70 | |
} |
Do the same again for Getters and Setters
: choose the rest of the properties:
... lines 1 - 10 | |
class GenusScientist | |
{ | |
... lines 13 - 41 | |
public function getGenus() | |
{ | |
return $this->genus; | |
} | |
public function setGenus($genus) | |
{ | |
$this->genus = $genus; | |
} | |
public function getUser() | |
{ | |
return $this->user; | |
} | |
public function setUser($user) | |
{ | |
$this->user = $user; | |
} | |
public function getYearsStudied() | |
{ | |
return $this->yearsStudied; | |
} | |
public function setYearsStudied($yearsStudied) | |
{ | |
$this->yearsStudied = $yearsStudied; | |
} | |
} |
Entity, done!
Now that the join table has an entity, we need to update the relationships in Genus
and User
to point to it. In Genus
, find the genusScientists
property. Guess what? This is not a ManyToMany
to User
anymore: it's now a OneToMany
to GenusScientist
. Yep, it's now the inverse side of the ManyToOne
relationship we just added. That means we need to change inversedBy
to mappedBy
set to genus
. And of course, targetEntity
is GenusScientist
:
... lines 1 - 14 | |
class Genus | |
{ | |
... lines 17 - 71 | |
/** | |
* @ORM\OneToMany(targetEntity="GenusScientist", mappedBy="genus", fetch="EXTRA_LAZY") | |
*/ | |
private $genusScientists; | |
... lines 76 - 202 | |
} |
You can still keep the fetch="EXTRA_LAZY"
: that works for any relationship that holds an array of items. But, we do need to remove the JoinTable
: annotation: both JoinTable
and JoinColumn
can only live on the owning side of a relationship.
There are more methods in this class - like addGenusScientist()
that are now totally broken. But we'll fix them later. In GenusScientist
, add inversedBy
set to the genusScientists
property on Genus
:
... lines 1 - 10 | |
class GenusScientist | |
{ | |
... lines 13 - 19 | |
/** | |
* @ORM\ManyToOne(targetEntity="Genus", inversedBy="genusScientists") | |
* @ORM\JoinColumn(nullable=false) | |
*/ | |
private $genus; | |
... lines 25 - 70 | |
} |
Finally, open User
: we need to make the exact same changes here.
For studiedGenuses
, the targetEntity
is now GenusScientist
, the relationship is OneToMany
, and it's mappedBy
the user
property inside of GenusScientist
:
... lines 1 - 16 | |
class User implements UserInterface | |
{ | |
... lines 19 - 77 | |
/** | |
* @ORM\OneToMany(targetEntity="GenusScientist", mappedBy="user") | |
*/ | |
private $studiedGenuses; | |
... lines 82 - 241 | |
} |
The OrderBy
doesn't work anymore. Well, technically it does, but we can only order by a field on GenusScientist
, not on User
. Remove that for now.
Tip
You should also add the inversedBy="studiedGenuses"
to the user
property in
GenusScientist
:
... lines 1 - 10 | |
class GenusScientist | |
{ | |
... lines 13 - 25 | |
/** | |
* @ORM\ManyToOne(targetEntity="User", inversedBy="studiedGenuses") | |
* @ORM\JoinColumn(nullable=false) | |
*/ | |
private $user; | |
... lines 31 - 70 | |
} |
It didn't hurt anything, but I forgot that!
Woh! Ok! Step back for a second. Our ManyToMany
relationship is now entirely gone: replaced by 3 entities and 2 classic ManyToOne
relationships. And if you think about it, you'll realize that a ManyToMany
relationship is nothing more than two ManyToOne
relationships in disguise. All along, we could have mapped our original setup by creating a "join" GenusScientist
entity with only genus
and user
ManyToOne
fields. A ManyToMany
relationship is just a convenience layer when that join table doesn't need any extra fields. But as soon as you do need extra, you'll need this setup.
Last step: generate the migration:
./bin/console doctrine:migrations:diff
Tip
If you get a
There is no column with name
id
on tablegenus_scientist
error, this is due to a bug in doctrine/dbal 2.5.5. It's no big deal, as it just affects the generation of the migration file. There are 2 possible solutions until the bug is fixed:
1) Downgrade to doctrine/dbal 2.5.4. This would mean adding the following line to your composer.json file:
"doctrine/dbal": "2.5.4"
Then run composer update
2) Manually rename genus_scientist to something else (e.g. genus_scientist_old)
and then generate the migration. Then, rename the table back. The generated migration
will be incorrect, because it will think that you need to create a genus_scientist
table, but we do not. So, you'll need to manually update the migration code by hand
and test it.
Look in the app/DoctrineMigrations
directory and open that migration:
... lines 1 - 10 | |
class Version20161017160251 extends AbstractMigration | |
{ | |
... lines 13 - 15 | |
public function up(Schema $schema) | |
{ | |
// 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('ALTER TABLE genus_scientist DROP FOREIGN KEY FK_66CF3FA885C4074C'); | |
$this->addSql('ALTER TABLE genus_scientist DROP FOREIGN KEY FK_66CF3FA8A76ED395'); | |
$this->addSql('ALTER TABLE genus_scientist DROP PRIMARY KEY'); | |
$this->addSql('ALTER TABLE genus_scientist ADD id INT AUTO_INCREMENT NOT NULL, ADD years_studied VARCHAR(255) NOT NULL'); | |
$this->addSql('ALTER TABLE genus_scientist ADD CONSTRAINT FK_66CF3FA885C4074C FOREIGN KEY (genus_id) REFERENCES genus (id)'); | |
$this->addSql('ALTER TABLE genus_scientist ADD CONSTRAINT FK_66CF3FA8A76ED395 FOREIGN KEY (user_id) REFERENCES user (id)'); | |
$this->addSql('ALTER TABLE genus_scientist ADD PRIMARY KEY (id)'); | |
} | |
... lines 29 - 32 | |
public function down(Schema $schema) | |
{ | |
// 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('ALTER TABLE genus_scientist MODIFY id INT NOT NULL'); | |
$this->addSql('ALTER TABLE genus_scientist DROP FOREIGN KEY FK_66CF3FA885C4074C'); | |
$this->addSql('ALTER TABLE genus_scientist DROP FOREIGN KEY FK_66CF3FA8A76ED395'); | |
$this->addSql('ALTER TABLE genus_scientist DROP PRIMARY KEY'); | |
$this->addSql('ALTER TABLE genus_scientist DROP id, DROP years_studied'); | |
$this->addSql('ALTER TABLE genus_scientist ADD CONSTRAINT FK_66CF3FA885C4074C FOREIGN KEY (genus_id) REFERENCES genus (id) ON DELETE CASCADE'); | |
$this->addSql('ALTER TABLE genus_scientist ADD CONSTRAINT FK_66CF3FA8A76ED395 FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE'); | |
$this->addSql('ALTER TABLE genus_scientist ADD PRIMARY KEY (genus_id, user_id)'); | |
} | |
} |
So freakin' cool! Because we already have the genus_scientist
join table, the migration does not create any new tables. Nope, it simply modifies it: drops a couple of foreign keys, adds the id
and years_studied
columns, and then re-adds the foreign keys. Really, the only thing that changed of importance is that we now have an id
primary key, and a years_studied
column. But otherwise, the table is still there, just the way it always was.
If you try to run this migration...it will blow up, with this rude error:
Incorrect table definition; there can be only one auto column...
It turns out, Doctrine has a bug! Gasp! The horror! Yep, a bug in its MySQL code generation that affects this exact situation: converting a ManyToMany
to a join entity. No worries: it's easy to fix... and I can't think of any other bug like this in Doctrine... and I use Doctrine a lot.
Take this last line: with ADD PRIMARY KEY id
, copy it, remove that line, and then - after the id
is added in the previous query - paste it and add a comma:
... lines 1 - 10 | |
class Version20161017160251 extends AbstractMigration | |
{ | |
... lines 13 - 15 | |
public function up(Schema $schema) | |
{ | |
// 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('ALTER TABLE genus_scientist DROP FOREIGN KEY FK_66CF3FA885C4074C'); | |
$this->addSql('ALTER TABLE genus_scientist DROP FOREIGN KEY FK_66CF3FA8A76ED395'); | |
$this->addSql('ALTER TABLE genus_scientist DROP PRIMARY KEY'); | |
$this->addSql('ALTER TABLE genus_scientist ADD id INT AUTO_INCREMENT NOT NULL, ADD PRIMARY KEY (id), ADD years_studied VARCHAR(255) NOT NULL'); | |
$this->addSql('ALTER TABLE genus_scientist ADD CONSTRAINT FK_66CF3FA885C4074C FOREIGN KEY (genus_id) REFERENCES genus (id)'); | |
$this->addSql('ALTER TABLE genus_scientist ADD CONSTRAINT FK_66CF3FA8A76ED395 FOREIGN KEY (user_id) REFERENCES user (id)'); | |
} | |
... lines 28 - 31 | |
public function down(Schema $schema) | |
{ | |
// 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('ALTER TABLE genus_scientist MODIFY id INT NOT NULL'); | |
$this->addSql('ALTER TABLE genus_scientist DROP FOREIGN KEY FK_66CF3FA885C4074C'); | |
$this->addSql('ALTER TABLE genus_scientist DROP FOREIGN KEY FK_66CF3FA8A76ED395'); | |
$this->addSql('ALTER TABLE genus_scientist DROP PRIMARY KEY'); | |
$this->addSql('ALTER TABLE genus_scientist DROP id, DROP years_studied'); | |
$this->addSql('ALTER TABLE genus_scientist ADD CONSTRAINT FK_66CF3FA885C4074C FOREIGN KEY (genus_id) REFERENCES genus (id) ON DELETE CASCADE'); | |
$this->addSql('ALTER TABLE genus_scientist ADD CONSTRAINT FK_66CF3FA8A76ED395 FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE'); | |
$this->addSql('ALTER TABLE genus_scientist ADD PRIMARY KEY (genus_id, user_id)'); | |
} | |
} |
MySQL needs this to happen all in one statement.
But now, our migrations are in a crazy weird state, because this one partially ran. So let's start from scratch: drop the database fully, create the database, and then make sure all of our migrations can run from scratch:
./bin/console doctrine:database:drop --force
./bin/console doctrine:database:create
./bin/console doctrine:migrations:migrate
Success!
Now that we have a different type of relationship, our app is broken! Yay! Let's fix it and update our forms to use the CollectionType
.
Hey Petru L.
I believe your problem relies on setting both sides of the relationship, double-check if your setter
or addFile()
method is setting the relationship between objects. That's the first thing I'd check. Let me know if it worked
Cheers!
Hey MolloKhan ,
I'm sorry for the late response, been a bit busy this week. I'm not sure i understand what you mean by 'setting both sides of the relationship'. I'm pasting my code in case it helps:
ProductFile.php ( the join table )
`class ProductFile
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private int $id;
/**
* @ORM\ManyToOne(targetEntity=Product::class, inversedBy="productFiles", cascade={"persist"})
*/
private $products;
/**
* @ORM\ManyToOne(targetEntity=File::class, inversedBy="productFiles", cascade={"persist"})
* @Groups({"product: read", "product: write"})
*/
private $files;
/**
* @ORM\Column(type="integer", nullable=true)
*/
private ?int $position;
/**
* @Gedmo\Timestampable(on="create")
* @ORM\Column(type="datetime")
*/
private \DateTimeInterface $addedAt;
public function getId(): ?int
{
return $this->id;
}
public function getProducts(): ?Product
{
return $this->products;
}
public function setProducts(?Product $products): self
{
$this->products = $products;
return $this;
}
public function getFiles(): ?File
{
return $this->files;
}
public function setFiles(?File $files): self
{
$this->files = $files;
return $this;
}
public function getPosition(): ?int
{
return $this->position;
}
public function setPosition(?int $position): self
{
$this->position = $position;
return $this;
}
public function getAddedAt(): ?\DateTimeInterface
{
return $this->addedAt;
}
public function setAddedAt(\DateTimeInterface $addedAt): self
{
$this->addedAt = $addedAt;
return $this;
}
}`
On the File entity:
`public function addProductFile(ProductFile $productFile): self
{
if (!$this->productFiles->contains($productFile)) {
$this->productFiles[] = $productFile;
$productFile->setFiles($this);
}
return $this;
}`
Also, i don't know why the code formatting is no longer working, even though i have it enclosed in it's tags
I love using Symfony 4's make:entity, but I'm a bit confused as to how to use it to create a Many-to-Many relationship with extra fields. Is this correct?
* Make Genus and User Entities
* Make UserGenus entity, with ManyToOne relationship to User, and ManyToOne relationship to Genus
The related methods then come out to something like $genus->getUserGenuses(). Something doesn't feel right, though, in this setup.
Hey Tac :D
This is 100% correct. Yes, $genus->getUserGenuses()
does sound weird. Unfortunately, once you need an extra field, you can't think about the relationship as "Genus" and "User" anymore. You need to think about it as "Genus" has many "UserGenuses" and "User" has many "UserGenuses". Sometimes, depending on the naming, this feels natural... and sometimes it doesn't. For example, the naming we used here Genus <-> GenusScientist <-> User feels more natural than Genus <-> GenusUser <-> User... even though these are the EXACT same thing, except for calling the middle entity GenusScientist instead of GenusUser.
Cheers!
I'm afraid I didn't get the tutorial for what I needed. I want to make a create/edit form that contains two entities related manytomany with extrafield, and that creates/edits both at the same time, on the fly. After a long time trying, I haven't been able to do it. Do you have a tutorial or do you know where to see an example? Thanks
Hey Jedediah,
We're sorry you didn't answer all your questions with this tutorial! It looks like you're interested in Symfony Forms also, have you seen our tutorials about forms: https://symfonycasts.com/sc... for Sf3 or https://symfonycasts.com/sc... for Sf4? It might bring you more context about forms. Anyway, why you haven't been able to do that? Did you have any errors trying to do that? As I understand you were able to configure ManyToMany with Extra Fields in your project, right? So, the problem in a compound form for them only? We would be glad to help with your question in comments.
Cheers!
Hey, excellent team!
What would be the proper way to implement the following: Let's suppose, via GenusFormType, we need to list all (i have only 10 fixed names) user names by checkboxes + an extra field "numberOfYears" with each of them. Till now, without any extra field that was simpler, I had ManyToMany relation between "Genus" and "User" and in my GenusFormType: "user, EntityType::class, ['class'=>User::class, 'multiple' =>true, 'expanded'=>true,'choice-label'=>'name'] ..." and that was working finely, but after adding an extra field like "numberOfYears", all changes...I created as in this tutorial, "GenusUser" entity with "numberOfYears" property. Then, in my GenusFormType, should i introduce them via CollectionType? Thanks
Hey Alexander,
Yeah, ManyToMany relation is implemented with an auxiliary table behind the scene where Doctrine keep entities IDs. And as soon as you need to add extra data on this auxiliary table - you can't use simple ManyToMany relation anymore, you would need a new entity and 2 relations instead: oneToMany and manyToOne to it. So, in your case you need 3 entities: User, Genus, UserGenus (but you can called it whatever you want to make more sense for you). And so, User relates to UserGenus as OneToMany, and Genus relates to UserGenus as OneToMany as well, so you don't have direct relations between User and Genus, it's done via UserGenus, where you can add whatever extra data you want, like numberOfYears as in your case.
P.S. These screencasts might be helpful for you:
https://symfonycasts.com/sc...
And actually, we even have a screencast about this topic, but for Symfony 3 only, thought it's not important as you just need to configure relations in your entities: https://symfonycasts.com/sc...
I hope it's clear and helps!
Cheers!
Thanks, Victor!
That's exactly i'had done.
Sorry for my re-question, but i'm a newbie in Symfony
I just wanted to know, if there was a Symfony way to show all users via Checkoxes its "own" extra field with each one. That's the particularity of my case.
To resolve this, I've coded hardly in php/html way: input type=checkbox ... etc. Then i retrieve Ids and set them as foreigh keys into "UserGenus" table with so desired extra field data.
But, i'd like so to know Symfony shortcut, if there's any.
Yours
Hey Alexander,
That's great! So, what I said in my previous message is really a "Symfony" way to handle extra fields on manyToMany relation. I'm not sure about your forms, you would probably need 3 forms now: one for User entity, one for UserGenus, and one for Genus - they probably will be nested to each other. And along with CollectionType you can build a one form that will handle all this, for your information, you can specify your custom forms in "entry_type" option of CollectionType like:
$builder->add('emails', CollectionType::class, [
// each entry in the array will be a UserGenus object rendered in "UserGenusType" field
'entry_type' => UserGenusType::class,
]);
I hope this helps!
Cheers!
Shouldn't it be like this...
class Genus {
// Instead: public function addGenusScientist(User $user)
public function addGenusScientist(GenusScientist $genusScientist) {}
// Instead: public function removeGenusScientist(User $user)
public function removeGenusScientist(GenusScientist $genusScientist) {}
}
Hey Mina!
You're totally right :). We leave that broken in this video (we mention it's broken, but only briefly). We fix it a bit later - https://knpuniversity.com/s...
Sorry if that confused you!
Cheers!
I'm a little confused by the mappedby attribute on scientists in Genus
/**
* @ORM\OneToMany(targetEntity="GenusScientist", mappedBy="Genus", fetch="EXTRA_LAZY")
* @ORM\JoinTable(name="genus_scientists")
*/
private $scientists;
Why is it not mapped by User? Isn't that the join table for scientists? Or is it referring to the class that $scientists exists in?
Hey Sara,
It used to be a join table if we had a simple ManyToMany relation between User and Genus, but since we need to store extra data on this join table - we can't use simple ManyToMany relation. That's why we provide a new entity - GenusScientist - that allow as to store extra data. So, we no more have simple ManyToMany but two relations: OneToMany and ManyToOne. In other words, we expand ManyToMany relation into 2 simple OneToMany and ManyToOne that technically is the same as ManyToMany in the database, but now we can add extra data (add extra columns on GenusScientist) to this intermediate table with Doctrine. I hope it clear to you now. I'd also recommend to re-watch this video one more time because yeah, it's kinda complex.
Cheers!
Generating the migration creates two new tables: genus_scientist_genus, genus_scientist_user. How can I avoid that?
GenusScientist.php
/**
* @ORM\ManyToMany(targetEntity="Genus", inversedBy="genusScientists")
* @ORM\JoinColumn(nullable=true)
*/
private $genus;
/**
* @ORM\ManyToMany(targetEntity="User", inversedBy="studiedGenuses")
* @ORM\JoinColumn(nullable=true)
*/
private $user;
Genus.php
/**
* @ORM\OneToMany(targetEntity="GenusScientist", mappedBy="genus")
*/
private $genusScientists;
User.php
/**
* @ORM\OneToMany(targetEntity="GenusScientist", mappedBy="user")
*/
private $studiedGenuses;
Hey Sergiu,
You should use "ManyToOne" relationships instead of "ManyToMany" in GenusScientist class, since you use "OneToMany" in Genus and User. I bet you have an invalid mapping if you do "bin/console doctrine:schema:validate" - it's a nice tip btw ;)
So the result GenusScientist entity mapping should be:
/**
* @ORM\ ManyToOne(targetEntity="Genus", inversedBy="genusScientists")
* @ORM\JoinColumn(nullable=true)
*/
private $genus;
/**
* @ORM\ ManyToOne(targetEntity="User", inversedBy="studiedGenuses")
* @ORM\JoinColumn(nullable=true)
*/
private $user;
Cheers!
Thanks. Copy-pasta error
[Mapping] OK - The mapping files are correct.
[Database] OK - The database schema is in sync with the mapping files.
Ah hah! I got it, finally! The tutorial uses doctrine/dbal 2.5.4, and there is a behavior change in doctrine/dbal 2.5.5! Here is the issue about it: https://github.com/doctrine/dbal/issues/2501
So, it's quite an annoying thing. There are 2 fixes:
1) Downgrade to doctrine/dbal 2.5.4. This would mean adding the following line to your composer.json file:
"doctrine/dbal": "2.5.4"
Then run composer update
2) Manually rename genus_scientist
to something else (e.g. genus_scientist_old
) and then generate the migration. Then, rename the table back. The generated migration will be incorrect, because it will think that you need to create a genus_scientist table, but we do not. So, you'll need to manually update the migration code by hand and test it.
Ultimately, the bug in Doctrine only prevents us from automatically generating the migration file. If you can write that file by hand, or get it partially-generated and then fix it, all is right with the world after.
Thanks for all the information guys - I couldn't reproduce this for a LONG time and you finally gave me enough information to do that!
Cheers!
Woohoo! I know what you mean - there are so many subtle options with relationships... but if you get them right, man, Doctrine relations really kill it.
Cheers and good luck!
Hello
When I type in the console
php bin/console doctrine:migrations:diff
I have the error:
[Doctrine\Common\Annotations\AnnotationException]
[Semantical Error] The annotation "@Doctrine\ORM\Mapping" in property AppBundle\Entity\GenusScient
ist::$genus does not exist, or could not be auto-loaded.
Please, help me understand how can I to fix this?
Hey Nina,
Have you imported namespace "use Doctrine\ORM\Mapping as ORM;" before GenusScientist class declaration? If so, could you show us your GenusScientist::$genus property definition with its annotations?
Cheers!
Yes, I imported namespace "use Doctrine\ORM\Mapping as ORM;" before GenusScientist class declaration.
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="genus_scientist")
*/
class GenusScientist
{
/**
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM|ManyToOne(targetEntity="Genus", inversedBy="genusScientists")
* @ORM|JoinColumn(nullable=false)
*/
private $genus;
I understood my mistake )
I forgot to write
/**
* @ORM\ManyToOne(targetEntity="User", inversedBy="studiedGenuses")
* @ORM\JoinColumn(nullable=false)
*/
private $user;
in the User.php
Great, so everything works fine now?
I noticed that you have a pipe " | " instead of inverted backslash " \ " in GenusScientist::$genus property
Cheers!
Hey,
I have a little error when I want to make my migrations:diff.
"There is no column with name 'id' on table 'genus_scientist' "
But I have this one.
If I rename my old table the migration is ok and create the new table, how can I have like you an update and not a creation ?
Thanks again.
Greg
Hey Greg!
Hmm, when exactly do you get this error? Is it when you run doctrine:migrations:diff
? And are you using MySQL or something different? Oh, and did you remove the ManyToMany on Genus and User (i.e. change them to OneToMany and give each the mappedBy option instead of inversedBy). And last thing :)... do you definitely have the @ORM\Column annotation above your id property in GenusScientist (with two stars to begin the comments - /**
).
I'm listing a bunch of possible tiny things because this error tells me two things:
1) Something is pointing to an "id" column on genus_scientist
. I'm not sure what this is. We added the id
column in the screencast... but nothing really depends on it. Your error seems to suggest that there is either a foreign key or some sort of index that's referring to this.
2) And of course, this error tells me that Doctrine doesn't see the id
column on genus_scientist
(you might have it in your database, but Doctrine doesn't see any evidence for it when it reads all of its mapping annotation metadata).
Let me know if this helps! Cheers!
Hi Ryan
Yes I have this error when I run the doctrine command. I use MySQL and I change the relation in Genus and User by the OneToMany.
I do exactly the same as the tutorial but if I rename my old genus_scientist table the migration is ok.
I will check again this evening.
Thanks again for your really awesome works.
Cheers
> but if I rename my old genus_scientist table the migration is ok
Do you mean that if you manually rename the table in your database, then the migration generates with no errors? If so, that's very interesting - let me know! :)
Cheers!
Yes I renamed it in sequel pro, but I have a migration with a create table not an alter table.
Hey Greg!
I was just looking for a bit more information, because I'm not convinced I've given you enough to solve this yet (and I'm curious about what's going on). As I said earlier, for some reason, Doctrine is looking at your annotation metadata and expecting there to be an "id" column on your genus_scientist table and not finding it. This can happen for a few reasons:
1) You have an Index above one of your entities referring to this column
2) There is a foreign key constraint that references this column (this would be a JoinTable or JoinColumn - did you remove the JoinTable from Genus?)
If none of the pointers are helping you find a possible cause, re-run the command that causes the error with a -vvv flag (so bin/console doctrine:migrations:diff -vvv
) and then paste the output here / take a screenshot. I'm curious to see exactly where the error is coming from.
Cheers!
Hey Ryan
This is the result of the command
https://gist.github.com/Gre...
I hope that is help you to understand.
Cheers.
Hey Greg!
Ah, it does! Well, sort of :). I was thinking about the problem incorrectly, but your gist cleared it up. Basically, when Doctrine tries to calculate what is different in your genus_scientist
table between the database and your annotations metadata, for some reason, it thinks that both the old and new tables have an index on it on the column id
. The question is why? It could be a bug in Doctrine, after all, there is the small bug I mention in the screencast, even when the diff works. If that's that case, I'm not sure why you're seeing it and I'm not (I also haven't heard anyone else mention this yet, but this is also a new screencast - so time will tell). Or, there is something very subtly wrong in your code somewhere. If you are willing, I would love to see your GenusScientist, Genus and User entities, to see if I can spot anything.
Cheers!
Hey Ryan
I will send you my entities this evening for me ( I am french ).
Thanks you again for your time.
Sorry for the delay this is my entities
https://gist.github.com/Gre...
Like Stéphane , doctrine/dbal v2.5.5
Cheers.
Dang! Then, I wonder if it's a Doctrine or MySQL weird version problem. What version of MySQL do you have? And what version of doctrine/dbal? You can run composer info
to find out.
Thanks for letting me know - hopefully we can find out what the issue is :).
Cheers!
I use MySQL Ver 14.14 Distrib 5.7.16, for Linux (x86_64) using EditLine wrapper and doctrine/dbal v2.5.5.
Cheers.
Hello,
One question.
You got 'FOREIGN KEY FK_66CF3FA885C4074C'
Do we need to use indexes: @ORM\Table( indexes={@ORM\Index(name="user_fk_idx"... ??
When and where are indexes important?
Hey Axa!
Normally you don't have to worry about indexes, Doctrine will take care for you. Unless you already have a running app, and you already configured you DB, in that case, I think you will have to specify your indexes for your tables, but I'm not 100% sure about this (I'm just guessing)
Cheers!
Hello Diego,
I am confused.
@ORM\Table(indexes={@ORM\Index(name="name_idx", columns={"name"})})
So, we do not need to do this because Doctrine will add indexes? Or you speak only about foreign keys?
Foreign keys (for relationships) and primary keys should be managed automatically by Doctrine, but if you want to index a field, you will have to do it as you said above. I'm not an expert about how to manage indexes, but maybe if you tell me more about your use case, I'll be able to help you more :)
I only want to ask whether we need to index a field. I did not notice in KNP tutorials that you put index somewhere. Have you used them in your projects or ignored them?
Hey @axa,
Yes, you're right, probably we do not show it in our screencasts. And probably the main reason for that is simplicity. Sometimes you need to add indexes, but it makes sense to do when you need to improve performance, like when you often JOIN or search by those columns, i.e. you can see your website is going to be slow due to some queries, you can debug those slow queries and in some cases you'll find that you just need to add an index to a column and then queries will be fast as they were before. So, this question does not related to Doctrine but to DB in general, Doctrine just gives a way to do it. I mean, when to add indexes you need to know from your DB architecture and from queries you execute in your application.
What about foreign keys, you don't need to add indexes for those columns, because a foreign key can be considered as an index.
But keep in mind, that every index could slow down write performance, so you need carefully add indexes, i.e. add it only when you *really* need them. Otherwise you'll get more bad than good.
Cheers!
// composer.json
{
"require": {
"php": "^7.1.3",
"symfony/symfony": "3.4.*", // v3.4.49
"doctrine/orm": "^2.5", // 2.7.5
"doctrine/doctrine-bundle": "^1.6", // 1.12.13
"doctrine/doctrine-cache-bundle": "^1.2", // 1.4.0
"symfony/swiftmailer-bundle": "^2.3", // v2.6.7
"symfony/monolog-bundle": "^2.8", // v2.12.1
"symfony/polyfill-apcu": "^1.0", // v1.23.0
"sensio/distribution-bundle": "^5.0", // v5.0.25
"sensio/framework-extra-bundle": "^3.0.2", // v3.0.29
"incenteev/composer-parameter-handler": "^2.0", // v2.1.4
"composer/package-versions-deprecated": "^1.11", // 1.11.99.4
"knplabs/knp-markdown-bundle": "^1.4", // 1.9.0
"doctrine/doctrine-migrations-bundle": "^1.1", // v1.3.2
"stof/doctrine-extensions-bundle": "^1.2" // v1.3.0
},
"require-dev": {
"sensio/generator-bundle": "^3.0", // v3.1.7
"symfony/phpunit-bridge": "^3.0", // v3.4.47
"nelmio/alice": "^2.1", // v2.3.6
"doctrine/doctrine-fixtures-bundle": "^2.3" // v2.4.1
}
}
Hey there ,
I'm re phrasing my original question because it was marked as spam for some reason...anyway:
I have two entities: Product & File with a ManyToMany relationships but since i needed extra fields i switched it to two OneToMany relationships with a middle table called ProductFile.
I'm also using apiplatform with symfony 5 and before this change, if i wanted to create a new product and add an existing file to it, the body request looked like this:
`
{
"title": "string",
"description": "string",
"files": [
]
}
`
After the change, it looks like this:
`
{
"title": "string",
"description": "string",
"productFiles": [
]
}
`
Which result in an error:
<blockquote>A new entity was found through the relationship 'App\Entity\Product#productFiles' that was not configured to cascade persist operations for entity: App\Entity\ProductFile@00000000590c148e0000000001d14e6f. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example @ManyToOne(..,cascade={\"persist\"}). If you cannot find out which entity causes the problem implement 'App\Entity\ProductFile#__toString()' to get a clue.</blockquote>
Can you help me with this please? P.S. the code formatting might have some issues? Sorry for that, i can't make it work