Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

ManyToMany with Extra Fields

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 $12.00

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

Login Subscribe

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

Creating the GenusScientist Join Entity

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!

Updating the Existing Relationships

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!

The Truth About ManyToMany

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.

Generating (and Fixing) the Migration

Last step: generate the migration:

./bin/console doctrine:migrations:diff

Tip

If you get a

There is no column with name id on table genus_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.

Leave a comment!

46
Login or Register to join the conversation
Petru L. Avatar
Petru L. Avatar Petru L. | posted 2 years ago | edited

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": [

"/api/files/681ba8e1-382f-4c7e-83e0-0ec89a73a28d"

]
}
`

After the change, it looks like this:

`
{
"title": "string",
"description": "string",
"productFiles": [

{
  "files": "/api/files/681ba8e1-382f-4c7e-83e0-0ec89a73a28d"
}

]
}
`

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

Reply

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!

Reply
Petru L. Avatar
Petru L. Avatar Petru L. | MolloKhan | posted 2 years ago | edited

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

Reply
Tac-Tacelosky Avatar
Tac-Tacelosky Avatar Tac-Tacelosky | posted 3 years ago

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.

Reply

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!

1 Reply
Ruben Avatar

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

Reply

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!

Reply

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

Reply

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!

Reply

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

Reply

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!

Reply

Thanks a lot Victor, that's clear and i'm eager to try it!
Have a good day

Reply
Default user avatar

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) {}
}

Reply

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!

Reply
Sara Avatar

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?

Reply
Osagie Avatar

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!

Reply
Sergiu P. Avatar
Sergiu P. Avatar Sergiu P. | posted 5 years ago | edited

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;
Reply

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!

Reply
Sergiu P. Avatar
Sergiu P. Avatar Sergiu P. | Victor | posted 5 years ago | edited

Thanks. Copy-pasta error


[Mapping]  OK - The mapping files are correct.
[Database] OK - The database schema is in sync with the mapping files.
Reply

Great! Sorry for no code blocks in the recent chapters - I'll add it soon

Reply
weaverryan Avatar weaverryan | SFCASTS | posted 5 years ago | edited

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!

Reply

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!

Reply
Default user avatar

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?

Reply

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!

1 Reply
Default user avatar

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;

Reply
Default user avatar

I understood my mistake )
I forgot to write
/**
* @ORM\ManyToOne(targetEntity="User", inversedBy="studiedGenuses")
* @ORM\JoinColumn(nullable=false)
*/
private $user;

in the User.php

Reply

Great, so everything works fine now?
I noticed that you have a pipe " | " instead of inverted backslash " \ " in GenusScientist::$genus property

Cheers!

Reply

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

Reply

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!

Reply

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

Reply

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

Reply

Yes I renamed it in sequel pro, but I have a migration with a create table not an alter table.

Reply

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!

Reply

Hey Ryan

This is the result of the command

https://gist.github.com/Gre...

I hope that is help you to understand.

Cheers.

Reply

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!

Reply

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.

Reply

Hey Ryan,
For information, I have the same error than Greg also.

Reply

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!

Reply

I use MySQL Ver 14.14 Distrib 5.7.16, for Linux (x86_64) using EditLine wrapper and doctrine/dbal v2.5.5.
Cheers.

Reply
Default user avatar

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?

Reply

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!

Reply
Default user avatar

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?

Reply

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

Reply
Default user avatar

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?

Reply

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!

Reply
Cat in space

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

This course is built on Symfony 3, but most of the concepts apply just fine to newer versions of Symfony.

What PHP libraries does this tutorial use?

// 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
    }
}
userVoice