Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Join Entity App Refactoring

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

In some ways, not much just changed. Before, we had a genus_scientist table with genus_id and user_id columns. And... we still have that, just with two new columns:

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

But, in our app, a ton just changed. That's my nice way of saying: we just broke everything!

Collection of GenusScientists, not Users

For example, before, genusScientists was a collection of User objects, but now it's a collection of GenusScientist objects:

... lines 1 - 14
class Genus
{
... lines 17 - 71
/**
* @ORM\OneToMany(targetEntity="GenusScientist", mappedBy="genus", fetch="EXTRA_LAZY")
*/
private $genusScientists;
... lines 76 - 202
}

The same thing is true on User:

... lines 1 - 16
class User implements UserInterface
{
... lines 19 - 77
/**
* @ORM\OneToMany(targetEntity="GenusScientist", mappedBy="user")
*/
private $studiedGenuses;
... lines 82 - 241
}

Wherever our code was using the studiedGenuses property - to get the collection or change it - well, that code is done broke.

Let's clean things up! And see some cool stuff along the way.

First, because we just emptied our database, we have no data. Open the fixtures file and temporarily comment-out the genusScientists property:

AppBundle\Entity\Genus:
genus_{1..10}:
... lines 3 - 8
# genusScientists: '3x @user.aquanaut_*'
... lines 10 - 38

We can't simply set a User object on genusScientists anymore: this now accepts GenusScientist objects. We'll fix that in a second.

But, run the fixtures:

./bin/console doctrine:fixtures:load

While that's working, go find GenusController and newAction(). Let's once again use this method to hack together and save some interesting data.

First, remove the two addGenusScientist lines:

... lines 1 - 13
class GenusController extends Controller
{
... lines 16 - 18
public function newAction()
{
... lines 21 - 40
$genus->addGenusScientist($user);
$genus->addGenusScientist($user); // duplicate is ignored!
... lines 43 - 52
}
... lines 54 - 146
}

These don't make any sense anymore!

How can we add a new row to our join table? Just create a new entity: $genusScientist = new GenusScientist(). Then, set $genusScientist->setGenus($genus), $genusScientist->setUser($user) and $genusScientist->setYearsStudied(10). Don't forget to $em->persist() this new entity:

... lines 1 - 6
use AppBundle\Entity\GenusScientist;
... lines 8 - 14
class GenusController extends Controller
{
... lines 17 - 19
public function newAction()
{
... lines 22 - 42
$genusScientist = new GenusScientist();
$genusScientist->setGenus($genus);
$genusScientist->setUser($user);
$genusScientist->setYearsStudied(10);
$em->persist($genusScientist);
... lines 48 - 57
}
... lines 59 - 151
}

There's nothing fancy going on anymore: GenusScientist is a normal, boring entity.

Using the new Collections

In your browser, try it: head to /genus/new. Genus created! Click the link to see it! Explosion! That's no surprise: our template code is looping over genusScientists and expecting a User object. Silly template! Let's fix that and the fixtures next.

Leave a comment!

10
Login or Register to join the conversation
Default user avatar

Wouldn't it be better to have a composite primary key for the GenusScientist entity, consisting of two foreign keys (genus_id, user_id), since this way, with surrogate primary key (id), theoretically we can end up with the same two combinations of foreign keys (genus_id, user_id) under two different primary keys (id)?

Reply

Hey @Mirko

That's a good point and it actually depends on your data constraints, if you have decided that you don't want to have duplicated combinations then you can set up unique constraints at the DB level. Check this out: https://www.doctrine-projec...

Cheers!

Reply
Default user avatar

Ok, so I can put a unique constraint on the genus_id-user_id combination if I want to. But, as I understand, GenusScientist table (entity) must not have the same combination of genus_id and user_id more than once, since this table acts like an intermediary table in many-to-many relationship between Genus and User.
My question is, if we make a combination of genus_id and user_id a primary key, then we don't need the surrogate id nor do we need to put a unique constraint on genus_id-user_id combination, since primary key is unique by default?

Reply

Ohh, you have a good point and probably from a DB point of view that would be the way to go but to be honest I haven't done that on Doctrine, so, I'm not totally sure if it's supported or not (Maybe yes?). If you find more info about it please let me know :)

Cheers!

Reply
Default user avatar

Yes, I managed to do it with Doctrine by following this: https://www.doctrine-projec...
Down below are the annotations I used for the GenusScientist entity's primary key. For the remainder of the tutorial I just needed to do some minor adjustments, but otherwise everything went well.

/**
* @ORM\Id
* @ORM\GeneratedValue(strategy="NONE")
* @ORM\ManyToOne(targetEntity="Genus", inversedBy="genusScientist")
* @ORM\JoinColumn(nullable=false)
*/
private $genus;

/**
* @ORM\Id
* @ORM\GeneratedValue(strategy="NONE")
* @ORM\ManyToOne(targetEntity="User", inversedBy="studiedGenuses")
* @ORM\JoinColumn(nullable=false)
*/
private $user;

2 Reply

Cool! Thanks for sharing it :)

Reply
Krzysztof K. Avatar
Krzysztof K. Avatar Krzysztof K. | posted 5 years ago

Do we have to keep field: id as a primary key for GenusScientist? What implictaions will be here if I will remove it and add composite primary key?

Reply

Hey Krzysztof K.

Very interesting question! Honestly, I haven't had the need (or obligation) of using a composite primary key for my entities, but I found a nice article about how to do it: https://www.doctrine-projec...

I hope it helps. Cheers!

1 Reply
Krzysztof K. Avatar
Krzysztof K. Avatar Krzysztof K. | MolloKhan | posted 5 years ago | edited

MolloKhan thanks :) I will do some tests to see hoe it works, maybe I will comeback here with more questions.

Reply

Excellent, I'm ready! (cracking knuckles)

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