Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

ManyToMany & Fixtures

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. These genuses are coming from our fixtures, but, sadly, the fixtures don't relate any scientists to them... yet. Let's fix that!

The fixtures.yml creates some Genus objects and some User objects, but nothing links them together:

AppBundle\Entity\Genus:
genus_{1..10}:
name: <genus()>
subFamily: '@subfamily_*'
speciesCount: <numberBetween(100, 100000)>
funFact: <sentence()>
isPublished: <boolean(75)>
firstDiscoveredAt: <dateTimeBetween('-200 years', 'now')>
... lines 9 - 22
AppBundle\Entity\User:
user_{1..10}:
email: weaverryan+<current()>@gmail.com
plainPassword: iliketurtles
roles: ['ROLE_ADMIN']
avatarUri: <imageUrl(100, 100, 'abstract')>
user.aquanaut_{1..10}:
email: aquanaut<current()>@example.org
plainPassword: aquanote
isScientist: true
firstName: <firstName()>
lastName: <lastName()>
universityName: <company()> University
avatarUri: <imageUrl(100, 100, 'abstract')>

How can we do that? Well, remember, the fixtures system is very simple: it sets each value on the given property. It also has a super power where you can use the @ syntax to reference another object:

AppBundle\Entity\Genus:
genus_{1..10}:
... line 3
subFamily: '@subfamily_*'
... lines 5 - 37

In that case, that other object is set on the property.

Setting data on our ManyToMany is no different: we need to take a Genus object and set an array of User objects on the genusScientists property. In other words, add a key called genusScientists set to [] - the array syntax in YAML. Inside, use @user.aquanaut_1. That refers to one of our User objects below. And whoops, make sure that's @user.aquanaut_1. Let's add another: @user.aquanaut_5:

AppBundle\Entity\Genus:
genus_{1..10}:
... lines 3 - 8
genusScientists: ['@user.aquanaut_1', '@user.aquanaut_5']
... lines 10 - 38

It's not very random... but let's try it! Find your terminal and run:

./bin/console doctrine:fixtures:load

Ok, check out the /genus page. Now every genus is related to the same two users.

Smart Fixtures: Using the Adder!

But wait... that should not have worked. The $genusScientists property - like all of these properties is private. To set them, the fixtures library uses the setter methods. But, um, we don't have a setGenusScientists() method, we only have addGenusScientist():

... lines 1 - 14
class Genus
{
... lines 17 - 174
public function addGenusScientist(User $user)
{
if ($this->genusScientists->contains($user)) {
return;
}
$this->genusScientists[] = $user;
}
... lines 183 - 195
}

So that's just another reason why the Alice fixtures library rocks. Because it says:

Hey! I see an addGenusScientist() method! I'll just call that twice instead of looking for a setter.

Randomizing the Users

The only way this could be more hipster is if we could make these users random. Ah, but Alice has a trick for that too! Clear out the array syntax and instead, in quotes, say 3x @user.aquanaut_*:

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

Check out that wonderful Alice syntax! It says: I want you to go find three random users, put them into an array, and then try to set them.

Reload those fixtures!

./bin/console doctrine:fixtures:load

Then head over to your browser and refresh. Cool, three random scientists for each Genus. Pretty classy Alice, pretty classy.

Leave a comment!

18
Login or Register to join the conversation
Default user avatar
Default user avatar Vince Liem | posted 5 years ago

Hi, Since I have made a ManyToMany relationship. I end up having a JSON file that fetches EVERYTHING in my database, because everything is connected to each other. Making my app very slow.

How do I create a repository that doesn't include everything?

For example. I've made a product category which contains several products. And a customer Category which contains several customers. Now I have a ManyToMany relationship between product category and customer category, to find out which customers are connected to which products.

Now If I want a page that contains all the product categories and related products and I end up using ->findAll in the product category repository. It goes all the way deep to the customer details. And it's being copied multiple times. Any way to prevent this?

2 Reply

Hey Vince!

Hmm, a few things. First, when you query for an entity (e.g. a list of ProductCategory objects), Doctrine *only* fetches the data for the ProductCategory objects - it does not automatically go and fetch the related data (of course, you can change your query with a JOIN to explicitly tell it to select the extra data, but it doesn't happen automatically). However, as soon as you reference the related data (e.g. $productCategory->getProducts()), Doctrine queries for that data then. In other words, Doctrine only queries for data that you're actually using.

But, how are you turning your object into JSON? Are you using Symfony's serializer, or the JMS Serializer? If so, both, by default, serialize *all* properties. In other words, even though you only queried for (for example) one ProductCategory (and Doctrine *only* fetched its data), when you serialize this to JSON, the serializer activates all the related properties causing everything to be lazy-loaded to create the gigantic JSON. Obviously, that's not cool :). To fix that, just choose *which* properties you do and don't want to serialize. With JMS Serializer, use @ExclusionPolicy and @Expose. With Symfony's serializer, use the serialization @Group annotation.

Let me know if this helps! Cheers!

2 Reply
Default user avatar

Ow yeah. I forgot to mention serializer. Anyways, Thanks for the helpful info

Reply
Default user avatar
Default user avatar Connect James | posted 5 years ago

Hi Ryan,

I have products which are related to each other as a Many to Many relationship creating a table called related_products, fixtures do not seem to like it as the products are not yet created that I am already asking to make a relation in between them. How can solve this problem?

Reply

Yo James!

Hmm, I'm not sure - I'm a little surprised that it doesn't work. The only issue I can find on it is this: https://github.com/nelmio/a... - which makes it sound like it should work. What does your code look like and what error do you receive?

Cheers!

Reply
Default user avatar
Default user avatar Connect James | weaverryan | posted 5 years ago

I think I figured it out, and I need a middle table so one to many as my related products need to be assigned to a certain position.
I have another table which actually is staying as a many to many table which is in between products and categories. Many product can be found in many categories and the inverse. I followed the whole process to add a many to many relationship but when loading fixtures I encounter this error:

[Doctrine\ORM\ORMInvalidArgumentException]
Expected value of type "Doctrine\Common\Collections\Collection|array" for a
ssociation field "AppBundle\Entity\Category#$categoryProducts", got "AppBun
dle\Entity\Product" instead.

Do you see what is the problem?

Thanks

Reply

Hey Connect,

I suppose you're trying to set a single Product entity, *but* your setCategoryProducts() method expects an array, i.e. ArrayCollection. Do you have an adder method, i.e. addCategoryProduct(Product $product) ? We have a nice note about our addGenusScientist() method in this article which says:
<blockquotes>
> Hey! I see an addGenusScientist() method! I'll just call that twice instead of looking for a setter
</blockquotes>

Please, double check you have the similar adder method in your entity.

Cheers!

Reply
Default user avatar
Default user avatar Connect James | Victor | posted 5 years ago

I think it is all done good on my entity file:

/**
* @ORM\ManyToMany(targetEntity="Category", inversedBy="categories")
* @ORM\JoinTable(name="category_product")
*/
private $categoryProducts;

public function __construct()
{
$this->categoryProducts = new ArrayCollection();
}

then

public function addCategoryProduct(Product $product)
{
if ($this->categoryProducts->contains($product)) {
return;
}

$this->categoryProducts[] = $product;
}

/**
* @return ArrayCollection|Product[]
*/
public function getCategoryProducts()
{
return $this->categoryProducts;
}

I can't see an error :/

Reply
Default user avatar

Sorry stupid mistake from my side with * @ORM\ManyToMany(targetEntity="Category", inversedBy="categories") at the place of * @ORM\ManyToMany(targetEntity="Product", inversedBy="categories")

Reply

Oh, I see. Good catch!

Btw, I use "$ bin/console doctrine:schema:validate" to validate mapping - it helps a lot.

Cheers!

Reply
Default user avatar
Default user avatar Mohammad Althayabeh | posted 5 years ago | edited

Hello there! Thanks for this nice tutorial.
When I try to
genusScientists: ['@user.aquanaut_1','@user.aquanaut_2'] OR genusScientists: '@user.aquanaut_*'

I GET Catchable Fatal Error: Argument 1 passed to AppBundle\Entity\Genus::setGenusScientists() must be an instance of AppBundle\Entity\User, array given..

Reply

Hey Mohammad,

Could you show a signature of your setGenusScientists() method? Actually, we don't have this method in this screencast, but we have "public function addGenusScientist(User $user)" instead. I believe this should work: "genusScientists: '3x @user.aquanaut_*'" - see the last code block in this screencast, but it's a bit weird that you have setter for collection, which requires a single User. Adder fits much better here.

Cheers!

Reply
Default user avatar
Default user avatar Xi Wang | posted 5 years ago

Hi Ryan,

How do we handle the loading of image file fixtures (or any up-loadable files)? For example I have a product entity which contains images files, and they are uploaded and handled via a doctrine listener.

Is it possible for us to mimic this process in Alice/Faker? Rather than creating dummy urls that points to lorempixel.

Thanks.

Reply

Hey Xi Wang!

I haven't done this before - and it depends on your setup - but you'll probably want to use a custom processor: https://github.com/hauteloo.... Basically, when a specific entity is being saved, the processor is called. Here you can handle your logic - e.g. copy some fixture file into an uploads/ directory and set the filename on the entity. Or, upload a file to S3 and set its filename on the entity.

Let me know if that works out well for you!

Cheers!

Reply
Default user avatar
Default user avatar Xi Wang | weaverryan | posted 5 years ago | edited

Yo Ryan! weaverryan

Thanks heaps! I totally forgot about this 'hook' although I saw it somewhere here loooong time ago! And you're awesome as always! :)

Now, when making decisions on using those extra layer bundles - how do we weigh the pros and cons in a project management point of view? I mean do we really need hauntlook/AliceBundle on top of theofidry/AliceDataFixtures and then in the dark deep core it is just nelmio/alice and faker?

I can do it the old fashion way as below, and I'm really curious about the trade-offs other than few console commands or config options (and some of these are really poorly documented through out the versions and quite difficult to keep up with).

Cheers!


class LoadFixtures implements FixtureInterface
{
    public function load(ObjectManager $manager)
    {
        $uploadDir = __DIR__ . '/../../../web/uploads';
        $fileUploader = new FileUploader($uploadDir);

        $processors[] = new ImageFileProcessor($fileUploader);

        Fixtures::load(
            __DIR__ . '/fixtures.yml',
            $manager,
            [
                'providers' => [$this]
            ],
            $processors
        );
    }
Reply

Hey Xi Wang!

Great question :). Honestly... over the past few years, AliceBundle has gotten more complex, and now there are a bunch of dependencies. I know the author - he's got a good idea in mind... but for me, *just* to be able to load fixtures, I really don't like the new complexity. So going forward, *if* I use Alice, I'm still using it in this simpler way (like you posted). More long term, I *may* stop using Alice entirely... but I think we (in Symfony) should make a few improvements to the DoctrineFixturesBundle to make a few things easier. What's *really* great about Alice is how easy it is to (A) add a bunch of items in a loop, (B) reference items together and (C) use Faker. This can all be done in DoctrineFixturesBundle, but it's much less elegant. I hope we can make it more elegant in the future.

tl;dr is this: I like using Alice in the more manual, but simpler way that you described. But if you run into too many issues, I'd recommend using DoctrineFixturesBundle manually.

Cheers!

Reply
Peter Avatar
Peter Avatar Peter | posted 5 years ago | edited

Sexiest Tag setting ever
<br /> '<numberBetween(0,4)>x @tags_*'<br />

Reply

Hey Peter,

Most of the time, it's just enough `@tags_*`, but yeah, sometimes it could be useful!

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