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

Mastering Doctrine Relations in Symfony 4!

2:14:20

What you'll be learning

The course is built on Symfony 4, but the principles still apply perfectly to Symfony 5 - not a lot has changed in the world of relations!
// 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
    }
}
// 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
    }
}

In the part 1 of the Doctrine Tutorial we created a freakin' awesome setup: with Doctrine: entities, queries, migrations and fixtures.

But, we're missing a big, giant, huge, important piece! Database relations! And once you've mastered these, you'll be unstoppable! But... a lot of information out there make Doctrine relations look over-complicated. And actually, they're simple and beautiful, especially with some recent changes in Symfony 4:

  • Generating a ManyToOne relationship (association)
  • The annotations for a relation
  • The OneToMany inverse side of a relation
  • Referencing objects across fixture classes
  • The owning versus inverse sides of a relation
  • Doing magic with the ArrayCollection (Criteria)
  • Querying with Joins
  • ManyToMany Relations
  • Pagination!

Help us bring "The Space Bar" app to the galaxy, with, (inter) stellar database relations. Yep, that's the level of humor you can expect!


Your Guides

Ryan Weaver

Buy Access

Join the Conversation?

29
Login or Register to join the conversation
Mohamed K. Avatar
Mohamed K. Avatar Mohamed K. | posted 4 years ago

Will there be symfony 4 tutorial on api's and how to use that with angular 5 or 6 ?

1 Reply

Hey Mohamed K.

At the moment we do not have that course in our plans, but we *do* hear all our customer's suggestions, so, thanks for letting us know what would you like to learn!
You can check our upcoming tutorials here: https://knpuniversity.com/c...

Have a nice day :)

2 Reply

To add more info, we will definitely do something with API's, and relatively soon. I'd like to do a tutorial about APIPlatform, which is AWESOME. But, we may also do something that sticks a bit more to normal Symfony features.

About Angular specifically, we do have a React tutorial planned, and may do some Vue stuff in the future. But, Angular is not currently on my radar. However, fortunately, there's nothing really special about using Symfony & any of the front-end frameworks. For authentication, the easiest thing to do is use normal "form login" authentication, then allow your AJAX requests to use the session cookie. Then, it's all just normal API/AJAX requests.

If you have any other questions, let us know!

Cheers!

3 Reply
Default user avatar

Angular and RxJs Observables are so sweet! Redux and ngRx will be nice too.

1 Reply
Ariel S. Avatar
Ariel S. Avatar Ariel S. | posted 1 year ago

Hi,

Could you tell me if this tutorial is relevant if I use Symfony 5 ?
Thank you very much for your work.

Ariel

Reply

Hey @Ariel!

Excellent question! Yes, definitely :). We will update this soon for Symfony 5, but Doctrine relations have not changed in... really any way that I can think of between Symfony 4 and 5. This tutorial will serve you well. And of course, if you have any doubts, you can ask us any questions.

Cheers!

Reply
Ariel S. Avatar

Hi Ryan,

Thank you for your quick response :)

Ariel

Reply
Matteo S. Avatar
Matteo S. Avatar Matteo S. | posted 1 year ago

Is a Symfony 5 version of this coming soon?

Reply

Hey PHP Fan,

Thank you for your interest in SymfonyCasts tutorials! We're sorry for the delay with releasing this tutorial for Symfony 5 track. We're working on it, most probably it might be our next tutorial, or next after another tutorial I think. But unfortunately, no more accurate estimations yet. You can watch for our upcoming tutorials on this page: https://symfonycasts.com/co... - if anything is changed - it will be displayed there.

Cheers!

Reply

Hello!
Got a question about error "A new entity was found through relationship YY that was not configured to cascade persist operations for entity XX".
I have a Cart (fields: cartProduct entity, ...), cartProduct (fields: product entity, ...) and Product entities. Between cart and cartProduct, I have a OneToMany relationship and between cartProduct and product, a ManytoOne. I got "cascade={"persist"}" set for the first relationship but not for the second one. I can not have product in my cart that did not exist before. The problem is when I save the cart to database, even if the product already exists, it does not recognize it. Did I miss something?

Thx!

Reply

Hey be_tnt!

It sounds like you are definitely understanding the "problem" correctly :).

> The problem is when I save the cart to database, even if the product already exists, it does not recognize it.

Yea... this looks "weird" to me also. If you already have a Product saved into the database, and then you create a new CartProduct and set that Product onto it, you should *not* need to explicitly call persist on that Product (or cascade={"persist"}, which is just the same thing as manually persisting). So, I agree, you should not need to have that second persist - I think something weird is going on.

Here's what I would do to debug:

Add the cascade=persist to the product field on CartProduct... *just* to see what happens. Does it all save normally? Or does this (crazily) create a NEW Product in the database? This will give us a hint about what's going on. According to the error, it sort of seems like Doctrine thinks (for some reason) that this is a NEW Product it should insert.

Based on what this, we can keep debugging :).

Cheers!

Reply

Hey weaverryan ,

I have done what you suggested and I got another error (quite strange btw):

Notice: Undefined index: 000000002e4d2d6e0000000027619249

Not really helpful, isn't it ? ;)

Reply

A little bit more details:

Uncaught PHP Exception ErrorException: "Notice: Undefined index: 000000002e4d2d6e0000000027619249" at /vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php line 2997

this is about these lines:

public function getEntityIdentifier($entity)
{
return $this->entityIdentifiers[spl_object_hash($entity)];
}

Hope this help!

Reply

Hey be_tnt !

Sorry for my slow reply! And woh! Something is *very* strange here! This simply... shouldn't happen. So, if you're still with me, we need to dig further! A few things:

1) Are you doing anything strange like $entityManager->clear() in your app... or are you doing this work in a test? Or, $entityManager->detach(). Everything makes it *feel* like you queried for CartProduct object... but then Doctrine... sorta "forgets" that it knows about it. Specifically, the UnitOfWork keeps track of every entity that it has queried for or saved during a request. The error we're seeing (I believe) is that you've queried for this CartProduct... then when everything saves... it's as if it's forgotten that it queried for this object.

2) Can you post a screenshot of the full stack trace? And maybe, if you can, the code you have for doing all the saving and the annotations for the relationships :).

It definitely smells to me like some very subtle thing is causing lots of weirdness. Things aren't behaving correctly!

Cheers!

Reply

Huh my comments were removed because they were detected as spams. Maybe because of the stack trace. Let me post the message without :)

The Cart class:

/**
* @ORM\OneToMany(
* targetEntity="App\Entity\CartProduct",
* mappedBy="cart",
* )
*/
private $products;


The cart object is created and saved to database based on products saved in session. Below the way I used to save products in session:

$items[$product->getId()] = [
'product' => $product,
'qty' => $request->request->get('add_item_to_cart')['quantity']
];
$this->session->set('shoppingCart/items', $items);


The CartProduct class:

/**
* @ORM\ManyToOne(targetEntity="App\Entity\Cart", inversedBy="products")
* @ORM\JoinColumn(nullable=false)
*/
private $cart;

/**
* @ORM\ManyToOne(targetEntity="App\Entity\Product")
* @ORM\JoinColumn(
* nullable=false
* )
*
*/
private $product;


In the cart controller:

$items = $this->session->get( 'shoppingCart/items' );
foreach ( $items as $item ) {
$product = new CartProduct();
$product->setProduct( $item['product'] );
$product->setQuantity( $item['qty'] );
$em->persist( $product );
$cart->addProducts( $product );
}

$em->persist( $cart );
$em->flush();


The $item['product'] is a Product object.

Hope this helps :)

Reply

be_tnt ,

No idea of what could be the issue here?
Thx !

Reply

Hi weaverryan !

Thx for helping! To answer to your questions:

1) No, nothing special in my code.
2) Let me try to explain what I am trying to do. The user add products to a cart. This is kept in session:

// add the new item in cart
$items[$product->getId()] = [
'product' => $product,
'qty' => $request->request->get('add_item_to_cart')['quantity']
];
$this->session->set('shoppingCart/items', $items);

When the user starts the payment process, this cart is saved to database:

$cart = new Cart();
$cart->setUser( $user );
$items = $this->session->get( 'shoppingCart/items' );
foreach ( $items as $item ) {
$product = new CartProduct();
$product->setProduct( $item['product'] );
$product->setQuantity( $item['qty'] );
$em->persist( $product );
$cart->addProducts( $product );
}

$em->persist( $cart );
$em->flush();

The Cart class:

/**
* @ORM\OneToMany(
* targetEntity="App\Entity\CartProduct",
* mappedBy="cart",
* )
*/
private $products;

The CartProduct class:

/**
* @ORM\ManyToOne(targetEntity="App\Entity\Cart", inversedBy="products")
* @ORM\JoinColumn(nullable=false)
*/
private $cart;

/**
* @ORM\ManyToOne(targetEntity="App\Entity\Product")
* @ORM\JoinColumn(
* nullable=false
* )
*
*/
private $product;

The full stack trace (the original one):

Doctrine\ORM\ORMInvalidArgumentException:
Multiple non-persisted new entities were found through the given association graph:

* A new entity was found through the relationship 'App\Entity\CartProduct#product' that was not configured to cascade persist operations for entity: App\Entity\Product@000000001057f61a000000007afc015d. 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\Product#__toString()' to get a clue.
* A new entity was found through the relationship 'App\Entity\CartProduct#product' that was not configured to cascade persist operations for entity: App\Entity\Product@000000001057f7e8000000007afc015d. 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\Product#__toString()' to get a clue.

at vendor/doctrine/orm/lib/Doctrine/ORM/ORMInvalidArgumentException.php:105
at Doctrine\ORM\ORMInvalidArgumentException::newEntitiesFoundThroughRelationships(array(array(array('fieldName' => 'product', 'joinColumns' => array(array('name' => 'product_id', 'unique' => false, 'nullable' => false, 'onDelete' => null, 'columnDefinition' => null, 'referencedColumnName' => 'id')), 'cascade' => array(), 'inversedBy' => null, 'targetEntity' => 'App\\Entity\\Product', 'fetch' => 2, 'type' => 2, 'mappedBy' => null, 'isOwningSide' => true, 'sourceEntity' => 'App\\Entity\\CartProduct', 'isCascadeRemove' => false, 'isCascadePersist' => false, 'isCascadeRefresh' => false, 'isCascadeMerge' => false, 'isCascadeDetach' => false, 'sourceToTargetKeyColumns' => array('product_id' => 'id'), 'joinColumnFieldNames' => array('product_id' => 'product_id'), 'targetToSourceKeyColumns' => array('id' => 'product_id'), 'orphanRemoval' => false), object(Product)), array(array('fieldName' => 'product', 'joinColumns' => array(array('name' => 'product_id', 'unique' => false, 'nullable' => false, 'onDelete' => null, 'columnDefinition' => null, 'referencedColumnName' => 'id')), 'cascade' => array(), 'inversedBy' => null, 'targetEntity' => 'App\\Entity\\Product', 'fetch' => 2, 'type' => 2, 'mappedBy' => null, 'isOwningSide' => true, 'sourceEntity' => 'App\\Entity\\CartProduct', 'isCascadeRemove' => false, 'isCascadePersist' => false, 'isCascadeRefresh' => false, 'isCascadeMerge' => false, 'isCascadeDetach' => false, 'sourceToTargetKeyColumns' => array('product_id' => 'id'), 'joinColumnFieldNames' => array('product_id' => 'product_id'), 'targetToSourceKeyColumns' => array('id' => 'product_id'), 'orphanRemoval' => false), object(Product))))
(vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php:3445)
at Doctrine\ORM\UnitOfWork->assertThatThereAreNoUnintentionallyNonPersistedAssociations()
(vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php:364)
at Doctrine\ORM\UnitOfWork->commit(null)
(vendor/doctrine/orm/lib/Doctrine/ORM/EntityManager.php:359)
at Doctrine\ORM\EntityManager->flush()
(src/Controller/CartController.php:92)
at App\Controller\CartController->cart(object(CartRepository), object(UserAddressRepository), object(Request), object(EntityManager))
(vendor/symfony/http-kernel/HttpKernel.php:151)
at Symfony\Component\HttpKernel\HttpKernel->handleRaw(object(Request), 1)
(vendor/symfony/http-kernel/HttpKernel.php:68)
at Symfony\Component\HttpKernel\HttpKernel->handle(object(Request), 1, true)
(vendor/symfony/http-kernel/Kernel.php:198)
at Symfony\Component\HttpKernel\Kernel->handle(object(Request))
(public/index.php:25)

Hope this help!

Reply

I have finally changed my way of doing things. Before adding a product in the cart, I get it again from the db (to get the latest information in case for ex the price has changed) and then add it to the cart:

So my code becomes:

$items = $this->session->get( 'shoppingCart/items' );
foreach ( $items as $item ) {
$cartProduct = new CartProduct();
// get the latest information about the product saved in database
$product = $product_repository->findOneBy([
'id' => $item['product'],
'isDeleted' => 0,
'isEnabled' => 1
]);
if (!empty($product)) {
$cartProduct->setProduct( $product );
$cartProduct->setQuantity( $item['qty'] );
$cart->addProducts( $cartProduct );
}
}

$em->persist( $cart );
$em->flush();

Thx!

Reply

Hey be_tnt !

Bahhh, sorry about my slow reply! I was *just* finally working through some things - it's SymfonyCon week, so we're a little extra busy. Anyways, I'm super happy you got it sorted! (And sorry about the SPAM issue - that's lame).

Let me give you a bit of context on "why" this was all happening... in case it's useful :). When you store the Cart object in the session (which is a totally legal thing to do!) something funny happens the next time you "fetch" it out of the cart. Here is the flow:

A) Request 1: you create a Cart object and insert it into the database. It gets id 1!
B) Request 1: you then put that Cart in the session
// 2 minutes go by, user adds another Product to their Cart
C) Request 2: you fetch the Cart out of the session. It is (of course) id 1.

This is enough to describe the issue. When you are on that "second" request (step C), Doctrine has not queried for the Cart object *on that request*. Doctrine keeps track of all the objects it has saved or queried for during a request (so that if you query for the same object twice, it skips making a 2nd query and just gives you back the same object). But this is NOT the same request as steps (A) and (B). And so, when you persist/flush the Cart object, Doctrine "thinks" it's a *new* object and *inserts* it. The key to understand why is this: when Doctrine tries to determine whether an object should be updated in the database versus inserted, it does *not* simply check to see if the object (e.g. Cart) has an "id" or not. Nope, it checks its "identity map" - which is a fancy way of saying - it asks itself: "Have I queried for or saved this object on this request?". If it has, then it knows about it and updates it. If it has not, then it tries to insert it (also, in this situation, it requires you to call persist() - which is the reason for the original error).

Phew! So actually, one solution is to store the Cart id in the session, not the entire object. We do something similar here on SfCasts - we only store the cart "id". We have a CartManager service, however, that helps us get the object. Basically, we all $cartManager->getCart() and IT takes care to read the cart "id" from the session, query for it, and return it. So we avoid the problem and still get a really nice user experience where we don't need to worry about querying for the Cart in the controller.

I hope this helps - even if it wasn't as timely as I would have liked!

Cheers!

1 Reply

Many thanks for this great explanation !!! That was important for me to understand the reason to continue to improve my knowledge on symfony.

1 Reply

I cann't find any information about cascade operation in symfony 4 doctrine tutorial, doest it mean do not use it beacuse it's bad practise or what ? ;)

Reply

Hey bartek

Nope, it doesn't mean it's a bad practice. I usually use "cascade on persist" when I work with collections, so then I don't need to persist new items added to a collection.
You can find more info about cascade operations here: https://www.doctrine-projec...

Cheers!

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

Hi Ryan. You are doing a great job. Thank you.
I am looking for advice: How to store gallery images of possibly a lot of users?
In Database or FS?
Usually the users upload large images and in different formats. How to deal with all this?
Thank you again.
Alex

Reply
Technomad Avatar
Technomad Avatar Technomad | posted 4 years ago

Hey, could you tell something about approximate publishing date? Would be great! Thanx in advance!

Reply

Hey Alexander,

Very rough approximation - it will be started releasing after "Doctrine & the Database" course, probably on the next week. We also want to start releasing ReactJS course very soon, so these two courses on the way to be released.

Cheers!

Reply
Technomad Avatar

Ok, great! Thanx for the quick reply!

Reply
Default user avatar
Default user avatar Alexander Enlund | posted 4 years ago

When are forms going to appear?

Reply

Hey Alexander Enlund

I can't give you a release date yet, but we already have a tutorial about forms, it's made on Symfony3, but nothing serious has changed https://knpuniversity.com/s...

And, if you want to learn more about how to customize your form's rendering, then you may also like to watch this tutorial: https://knpuniversity.com/s...

Cheers!

Reply
Default user avatar
Default user avatar Alexander Enlund | MolloKhan | posted 4 years ago

okay... Thanks for the fast reply and the recommendations!

Reply
Cat in space

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