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 SubscribeWe are able to create new QuestionTag
objects with its factory... but when we do that, it creates a brand new Question
object for each new QuestionTag
. That's... not what we want! I want what we had before... where we create our 20 published questions and relate those to a random set of tags.
Delete the return
statement and the QuestionTagFactory
line. Right now, this says:
Create 20 questions. And, for each one, set the
tags
property to 5 randomTag
objects.
The problem is that our Question
entity doesn't have a tags
property anymore: it now has a questionTags
property. Okay. So let's change this to questionTags
. We could set this to QuestionTagFactory::randomRange()
. But that would require us to create those QuestionTag
objects up here... which we can't do because we need the question object to exist first. Well, we could do that, but we would end up with extra questions that we don't really want.
By the way, we're about to see some really cool, really advanced stuff in Foundry. But at the end, I'm also going to show a simpler solution to creating the objects we need.
Anyways, set questionTags
to QuestionTagFactory::new()
. So, to an instance of this factory.
... lines 1 - 14 | |
class AppFixtures extends Fixture | |
{ | |
public function load(ObjectManager $manager) | |
{ | |
TagFactory::createMany(100); | |
$questions = QuestionFactory::createMany(20, function() { | |
return [ | |
'questionTags' => QuestionTagFactory::new(), | |
]; | |
}); | |
... lines 26 - 44 | |
} | |
} |
There is a problem with this... but it's mostly correct. And... it's kind of crazy! This tells Foundry to use this QuestionTagFactory
instance to create a new QuestionTag
object. Normally when we use QuestionFactory
, it creates a new Question
object. But in this case, that won't happen. Because we're calling this from inside the QuestionFactory
logic, the question
attribute that's passed to QuestionTagFactory
will be overridden and set to the Question
object that is currently being created by its factory.
In other words, this will not cause a new, extra Question
to be created in the database. Instead, the new QuestionTag
object will be related to whatever Question is currently being created. Foundry does this by reading the Doctrine relationship and smartly overriding the question
attribute on QuestionTagFactory
.
But... I did say that there was a problem with this. And... we'll see it right now:
symfony console doctrine:fixtures:load
This gives us a weird error from PropertyAccessor
about how the questionTags
attribute cannot be set on Question
. The PropertyAccessor
is what's used by Foundry to set each attribute onto the object. And while it's true that we don't have a setQuestionTags()
method, we do have addQuestionTag()
and removeQuestionTag()
, which the accessor is smart enough to use.
So, the real problem here is simpler: QuestionTagFactory::new()
says that we want to create a single QuestionTag
and set it onto questionTags
. But we need an array. That confused the property accessor. To fix this, add ->many()
.
This "basically" returns a factory instance that's now configured to create multiple objects. Pass 1, 5 to create anywhere from 1 to 5 QuestionTag
objects.
... lines 1 - 14 | |
class AppFixtures extends Fixture | |
{ | |
public function load(ObjectManager $manager) | |
{ | |
TagFactory::createMany(100); | |
$questions = QuestionFactory::createMany(20, function() { | |
return [ | |
'questionTags' => QuestionTagFactory::new()->many(1, 5), | |
]; | |
}); | |
... lines 26 - 44 | |
} | |
} |
Try the fixtures again:
symfony console doctrine:fixtures:load
No errors! And if we SELECT * FROM question
:
symfony console doctrine:query:sql 'SELECT * FROM question'
We only have 25 rows: the correct amount! That's the 20 published... and the 5 unpublished. This proves that the QuestionTagFactory
did not create new question objects like it did before: all the new question tags are related to these 20 questions. We can see that by querying: SELECT * FROM question_tag
symfony console doctrine:query:sql 'SELECT * FROM question_tag'
60 rows seems about right. This is related to question 57, 57, 57, 57... then 56, 56 and then 55. So each question has a random number of question tags.
Unfortunately this line is still creating a new random Tag
each time. Check the tag
table:
symfony console doctrine:query:sql 'SELECT * FROM tag'
We want there to be 100 rows... from the 100 in our fixtures. We don't want extra tags to be created down here. But... we get 160: 100 plus 1 more for each QuestionTag
.
And... this make sense... thanks to the getDefaults()
method.
The fix... is both nuts and simple: pass an array to new()
to override the tag
attribute. Set it to TagFactory::random()
to grab one
existing random Tag
.
... lines 1 - 14 | |
class AppFixtures extends Fixture | |
{ | |
public function load(ObjectManager $manager) | |
{ | |
... lines 19 - 20 | |
$questions = QuestionFactory::createMany(20, function() { | |
return [ | |
'questionTags' => QuestionTagFactory::new([ | |
'tag' => TagFactory::random() | |
])->many(1, 5), | |
]; | |
}); | |
... lines 28 - 46 | |
} | |
} |
Reload the fixtures again:
symfony console doctrine:fixtures:load
And query the tag table:
symfony console doctrine:query:sql 'SELECT * FROM tag'
We're back to 100 tags! But... I made a mistake... and maybe you saw it. Check out the question_tag
table:
symfony console doctrine:query:sql 'SELECT * FROM question_tag'
These last two are both related to question id 82... actually the last 3. And that's fine: each Question
will be related to 1 to 5 question tags. The problem is that all of these are also related to the same Tag
!
In the fixtures, each time a Question
is created, it executes this callback. So it's executed 20 times. But then, when the 1 to 5 QuestionTag
object are created, TagFactory::random()
is only called once... meaning that the same Tag
is used for each of the 1 to 5 question tags.
Yup, this is the same problem we've seen multiple times before... I'm trying to make this mistake a ton of times in this tutorial, so that you never experience it.
Refactor this to use a callback.
... lines 1 - 14 | |
class AppFixtures extends Fixture | |
{ | |
public function load(ObjectManager $manager) | |
{ | |
... lines 19 - 20 | |
$questions = QuestionFactory::createMany(20, function() { | |
return [ | |
'questionTags' => QuestionTagFactory::new(function() { | |
return [ | |
'tag' => TagFactory::random(), | |
]; | |
})->many(1, 5), | |
]; | |
}); | |
... lines 30 - 48 | |
} | |
} |
Then, reload the fixtures:
symfony console doctrine:fixtures:load
And check the question_tag
table:
symfony console doctrine:query:sql 'SELECT * FROM question_tag'
Yes! These last 2 have the same question id... but they have different tag ids. Mission accomplished! And... this is probably the most insane thing that you'll ever do with Foundry. This says:
Create 20 questions. For each question, the
questionTags
property should be set to 1 to 5 newQuestionTag
objects... except where thequestion
attribute is overridden and set to the newQuestion
object. Then, for eachQuestionTag
, select a randomTag
.
Congratulations, you now have a PhD in Foundry!
But... you do not need to make it this complicated! I did this mostly for the pursuit of learning! To show off some advanced stuff you can do with Foundry.
An easier way to do this would be to create 100 tags, 20 published questions and then, down here, use the QuestionTagFactory
to create, for example, 100 QuestionTag
objects where each one is related to a random Tag
and also a random Question
.
... lines 1 - 14 | |
class AppFixtures extends Fixture | |
{ | |
public function load(ObjectManager $manager) | |
{ | |
... lines 19 - 22 | |
QuestionTagFactory::createMany(100, function() { | |
return [ | |
'tag' => TagFactory::random(), | |
'question' => QuestionFactory::random(), | |
]; | |
}); | |
... lines 29 - 47 | |
} | |
} |
Then, above, when we create the Questions... we can just create normal, boring Question
objects... because the QuestionTag
stuff is handled below.
... lines 1 - 14 | |
class AppFixtures extends Fixture | |
{ | |
public function load(ObjectManager $manager) | |
{ | |
TagFactory::createMany(100); | |
$questions = QuestionFactory::createMany(20); | |
QuestionTagFactory::createMany(100, function() { | |
return [ | |
'tag' => TagFactory::random(), | |
'question' => QuestionFactory::random(), | |
]; | |
}); | |
... lines 29 - 47 | |
} | |
} |
If we try this:
symfony console doctrine:fixtures:load
No errors. And if you look inside the question_tag
table:
symfony console doctrine:query:sql 'SELECT * FROM question_tag'
We get 100 question tags, each related to a random Question
and a random Tag
. It's not exactly the same as we had before, but it's probably close enough, and much simpler.
Next: let's fix the frontend and our JOIN to use the refactored QuestionTag
relationship.
Hey Nicolas,
Did you download the course code and start from start/ directory? Or are you applying changes on your personal project or a fresh Symfony version?
Looks like you have a bad mapping in your project, try to run "bin/console doctrine:schema:validate" first and check the output - does it have both OK or mapping is failed? If failed - try to double-check and fix your mapping and try that query one more time.
Cheers!
Thanks Victor,
I indeed downloaded the course and started from scratch - not from a previous course.
Everything was fine until now.
I ran "bin/console doctrine:schema:validate" and the output seems correct:
nicolas@MURCIA MINGW64 /c/xampp/htdocs/cauldron_overflow (master)
$ bin/console doctrine:schema:validate
Mapping
-------
[OK] The mapping files are correct.
Database
--------
[OK] The database schema is in sync with the mapping files.
Yours truly...
Hey Nicolas,
Please, make sure you have $tags property in the Question entity. It probably should be $questionTags property in the query, i.e. try:
/**
* @return Question[] Returns an array of Question objects
*/
public function findAllAskedOrderedByNewest()
{
return $this->addIsAskedQueryBuilder()
->orderBy('q.askedAt', 'DESC')
->leftJoin('q.questionTags', 'tag')
->addSelect('tag')
->getQuery()
->getResult()
;
}
I suppose it will work now. I just downloaded the course code and see that the property is named as "questionTags" instead of just "tags".
Cheers!
Hey Victor,
The right answer to my issue is in fact in the next Chapter (Chapter 25 - JOINing Across Multiple Relationships).
I tested the Frontend too soon, at the end of Chapter 24 - unlike what Ryan did.
I am so sorry for the disturbance, and you can delete my post which might not be very useful to other users.
Thank again for your help, quick and useful.
Hey Nicolas,
Ah, ok, glad you found the solution! And let's keep the thread, it might be still useful for others ;)
Cheers!
At about 1:58 in the video it should be:
"Normally when we use QuestionTagFactory, it creates a new Question object." not QuestionFactory
Hey Ralf,
The sentence "Normally when we use QuestionFactory, it creates a new Question object" is correct, we're talking about QuestionFactory in that sentence, not about QuestionTagFactory. QuestionTagFactory cannot create Question object because it creates QuestionTag object. You're probably misleaded because on the video the QuestionTagFactory text was selected, but we were talking about it in the previous sentence :)
Cheers!
// composer.json
{
"require": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*",
"babdev/pagerfanta-bundle": "^3.3", // v3.3.0
"composer/package-versions-deprecated": "^1.11", // 1.11.99.3
"doctrine/doctrine-bundle": "^2.1", // 2.4.2
"doctrine/doctrine-migrations-bundle": "^3.0", // 3.1.1
"doctrine/orm": "^2.7", // 2.9.5
"knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
"knplabs/knp-time-bundle": "^1.11", // v1.16.1
"pagerfanta/doctrine-orm-adapter": "^3.3", // v3.3.0
"pagerfanta/twig": "^3.3", // v3.3.0
"sensio/framework-extra-bundle": "^6.0", // v6.2.1
"stof/doctrine-extensions-bundle": "^1.4", // v1.6.0
"symfony/asset": "5.3.*", // v5.3.4
"symfony/console": "5.3.*", // v5.3.7
"symfony/dotenv": "5.3.*", // v5.3.7
"symfony/flex": "^1.3.1", // v1.17.5
"symfony/framework-bundle": "5.3.*", // v5.3.7
"symfony/monolog-bundle": "^3.0", // v3.7.0
"symfony/runtime": "5.3.*", // v5.3.4
"symfony/stopwatch": "5.3.*", // v5.3.4
"symfony/twig-bundle": "5.3.*", // v5.3.4
"symfony/validator": "5.3.*", // v5.3.14
"symfony/webpack-encore-bundle": "^1.7", // v1.12.0
"symfony/yaml": "5.3.*", // v5.3.6
"twig/extra-bundle": "^2.12|^3.0", // v3.3.1
"twig/string-extra": "^3.3", // v3.3.1
"twig/twig": "^2.12|^3.0" // v3.3.2
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.0
"symfony/debug-bundle": "5.3.*", // v5.3.4
"symfony/maker-bundle": "^1.15", // v1.33.0
"symfony/var-dumper": "5.3.*", // v5.3.7
"symfony/web-profiler-bundle": "5.3.*", // v5.3.5
"zenstruck/foundry": "^1.1" // v1.13.1
}
}
Either with the first solution or "The Simpler Solutions", I get this error :
<blockquote>[Semantical Error] line 0, col 58 near 'tag WHERE q.askedAt': Error: Class App\Entity\Question has no association named tags</blockquote>
in the Doctrine query :
<blockquote>Doctrine\ORM\Query\QueryException
in C:\xampp\htdocs\cauldron_overflow\vendor\doctrine\orm\lib\Doctrine\ORM\Query\QueryException.php (line 49)
</blockquote>
I checked my code many times but to no avail.
Maybe something is wrong in QuestionRepository.php :
Any idea?
Thanks....