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 SubscribeOur question
table has data! And each time we refresh, we got more data! You get a question! You get a question!
Copy the slug from the latest one and then go to /questions/that-slug
to see it. Except... this is not actually that question. The name is kinda right... but that's it. Over in the show()
action, this is because nothing is being loaded from the database. Lame!
Here's our next mission: use the $slug
to query for a row of Question
data and use that to make this page truly dynamic. How? The entity manager that we use to save data can also be used to fetch data.
Start by adding a third argument: EntityManagerInterface $entityManager
. This interface has a bunch of methods on it. But... most of the time, you'll only use three: persist()
and flush()
to save, and getRepository()
when you want to get data.
... lines 1 - 12 | |
class QuestionController extends AbstractController | |
{ | |
... lines 15 - 70 | |
public function show($slug, MarkdownHelper $markdownHelper, EntityManagerInterface $entityManager) | |
{ | |
... lines 73 - 94 | |
} | |
} |
Say $repository = $entityManager->getRepository()
and pass the entity class that we want to query. So Question::class
.
... lines 1 - 12 | |
class QuestionController extends AbstractController | |
{ | |
... lines 15 - 70 | |
public function show($slug, MarkdownHelper $markdownHelper, EntityManagerInterface $entityManager) | |
{ | |
if ($this->isDebug) { | |
$this->logger->info('We are in debug mode!'); | |
} | |
$repository = $entityManager->getRepository(Question::class); | |
... lines 78 - 94 | |
} | |
} |
Whenever you need to get data, you'll first get the repository for an entity. This repository object is really really good at querying from the question
table. And it has several methods to help us.
For example, we want to query WHERE
the slug
column equals the $slug
variable. Do that with $question = $repository->
and... this auto completes a bunch of methods. We want findOneBy()
. Pass this an array of the WHERE statements we need: 'slug' => $slug
. After, dd($question)
.
... lines 1 - 12 | |
class QuestionController extends AbstractController | |
{ | |
... lines 15 - 70 | |
public function show($slug, MarkdownHelper $markdownHelper, EntityManagerInterface $entityManager) | |
{ | |
... lines 73 - 76 | |
$repository = $entityManager->getRepository(Question::class); | |
$question = $repository->findOneBy(['slug' => $slug]); | |
dd($question); | |
... lines 80 - 94 | |
} | |
} |
Ok, let's see what this returns! Refresh and... woohoo! This gives us a Question
object. Doctrine finds the matching row of data and uses that to populate an object, which is beautiful.
The repository has a number of other methods on it. For example, findOneBy()
returns a single object and findBy()
returns an array of objects that match whatever criteria you pass. The findAll()
method returns an array of all Question
objects and there are a few others. So without doing any work, we can easily execute the most basic queries. Now, eventually we will need to do more complex stuff - and for that, we'll write custom queries. We'll see that later.
So when Doctrine finds a matching row, we get back a Question
object. But if we change the slug in the URL to something that does not exist, we get null. So: a Question
object or null.
Let's think: what do we want to do when someone goes to a URL that doesn't match a real question? The answer is: trigger a 404 page! Great! Um... how do we trigger a 404 page in Symfony?
First, this is optional - I'm going to say /**
space and then type Question|null
.
... lines 1 - 12 | |
class QuestionController extends AbstractController | |
{ | |
... lines 15 - 70 | |
public function show($slug, MarkdownHelper $markdownHelper, EntityManagerInterface $entityManager) | |
{ | |
... lines 73 - 77 | |
/** @var Question|null $question */ | |
$question = $repository->findOneBy(['slug' => $slug]); | |
... lines 80 - 98 | |
} | |
} |
This simply helps my editor know that this is a Question
object or null, which will assist auto-completion. And, to be honest, PhpStorm is so smart that... I think it already knew this.
Below, if not $question
, trigger a 404 page by saying throw $this->createNotFoundException()
, which is a method on the parent AbstractController
class. Pass this any message you want:
No question found for slug %s
And pass the $slug
variable.
... lines 1 - 12 | |
class QuestionController extends AbstractController | |
{ | |
... lines 15 - 70 | |
public function show($slug, MarkdownHelper $markdownHelper, EntityManagerInterface $entityManager) | |
{ | |
... lines 73 - 77 | |
/** @var Question|null $question */ | |
$question = $repository->findOneBy(['slug' => $slug]); | |
if (!$question) { | |
throw $this->createNotFoundException(sprintf('no question found for slug "%s"', $slug)); | |
} | |
... lines 83 - 98 | |
} | |
} |
That's it! But notice the throw
. createNotFoundException()
instantiates an exception object - a very special exception object that triggers a 404 page. Most of the time in Symfony, if you throw an exception, it will cause a 500 page. But this special exception maps to a 404.
Let's try it: refresh and... yes! You can see it up here: "404 Not found" with our message.
Two things about this. First: this is the development error page. If we changed the environment to prod
, we would see a much more boring 404 page with no error or stack trace details. We won't talk about it, but the Symfony docs have details about how you can customize the look and feel of your error pages on production.
The second thing I want to say is that the message - no question found for slug - is something that only developers will see. Feel free to make this as descriptive as you want: you don't need to worry about a real user seeing it.
Now that we have a Question
object in our controller, let's use it in our template to render real, dynamic info. That's next.
I follow the steps in this video. But it didn't auto complete methods in my code (01:45). What is the possibly reason for this?
Hey Ngan-Cat,
It depends if you have Symfony Plugin installed or no. In this tutorial Ryan has it installed and enabled, that's why he get that autocomplete. You can take a look at this video to know how to install and enable it: https://symfonycasts.com/screencast/phpstorm/setup#plugins
Otherwise, you can hint your IDE about what class is that object by using @var
annotation above the var, e.g.:
/** @var \App\Repository\QuestionRepository $repository */
$repository = $entityManager->getRepository(Question::class);
$repository-> // now you should have autocomplete here :)
I hope this helps!
Cheers!
How to know which library should I use? Like you used your "\App\Repository\QuestionRepository $repository" seems like out of nowhere..
Hey fUb,
Well, it depends on what repository you need. If you call "getRepository(Question::class)", then I know that the repository behind of that Question entity is QuestionRepository. If you're not sure - you can try to dump the $repository var, e.g. with "dd($repository)" - it will give you a hint about instance of what class is that var.
Cheers!
Hello everybody,
a happy new year to everybody. Especially the ones in the far east of europe.
Could anyone please tell me why this statements throws an error when executed by doctrine but not when directly executed by PHPMyAdmin - on the same server and the same database!
An exception occurred while executing 'UPDATE `timeperiods` SET `cleared`="2021030515 Wildschütz Werbeagentur GmbH T.PDF" WHERE `id`=1846;UPDATE `timeperiods` SET `cleared`="2021030515 Wildschütz Werbeagentur GmbH T.PDF" WHERE `id`=1854;':
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'UPDATE `timeperiods` SET `cleared`="2021030515 Wildschütz Werbeagentur GmbH T.P' at line 1
I also tried to insert a chr(10) OR \r\n right before the second Update. No chance either 8-(.
Thx
Oliver
Hey Oliver-W!
A very warm happy new year to you too - and I echo your sentiments ❤️.
Hmm. What method are you using to execute these 2 queries? This isn't something I've done before, but I'm sure it's "allowed", but it's possible that you need to use the DBAL directly (i.e. a lower level) to allow this.
Cheers!
Hey Ryan,
I am using
$repositorycust = $em->getRepository(Timeperiods::class);
$conn = $em->getConnection();
$query = "UPDATE `timeperiods` SET `cleared`=\"$renr $filename.PDF\" WHERE `id`=" . intval($datum["id"]);
$stmt = $conn->prepare($query);
$stmt->execute();
To make it work, I execute the query for every set of data. Before I concatenated the statements and made one query.
Cheers!
Hey Oliver-W!
Hmm. That's what I would have used too. I'm not sure why concatenating doesn't work. This looks technically possible, though "iffy" - like it's not something that you're really "supposed" to do. So, I would keep your solution of executing 1 query at a time.
Cheers!
Hey Ryan,
or I could use ...where id=1 OR id=2 and so on.
Sometimes live is really hard! Thx for your time and help
Oliver
Hey team SymfonyCasts, you guys are awesome and I have learned a lot from you guys and btw, happy new year.
Now I am trying to build some sort of form generator adhering to the Open/Closed principle. The form will definitely have new fields in the future. So with all these things in mind here is the path that I have taken:
Ok, now the problem I am facing at step 3 is that I came down to this line:
foreach($FormFieldNames as $ffn)
{
$entities[] = $this->em->getRepository('App\Entity\'.$ffn.'::class)->findBy(['isActive' => true);
}
The error I get is: Class 'App\Entity'.$ffn.' does not exist.
Of course hard coding the above variable with entity name works. For example: $entities[] = $this->em->getRepository('App\Entity\Continent'::class)->findBy(['isActive' => true);
But I need to fetch all entities agnostically, meaning the code should not or cannot be aware of the name of the entities. Oh, and btw, if I use getRepositpry(Continent::class), not only I am hard coding, but also I would need to add a use statement on top of the page to inject this entity which I can simply avoid by using the exact path instead of the entity name.
There seems to be an scaping mechanism of some sort in the background which I do not know how to bypass. I tried different concatenations, none worked. Any suggestion or advice in general is very appreciated. Thank you!
Hey Ali_M!
A very happy new year to you too :). Now, let's see about your question!
The error I get is: Class 'App\Entity'.$ffn.' does not exist.
Ok, I'm going to assume that the $ffn
variable is set to the "short" class name of your entity - like the string Product
. If that's the case, the code you need is:
$entities[] = $this->em->getRepository('App\Entity\'.$ffn)->findBy(['isActive' => true);
That should do it :). The ::class
thing is something that you can call on a class to turn it into the full class name of that class. For example:
use App\Entity\Product;
echo Product::class;
// prints the string "App\Entity\Product"
So, ::class
is just a convenience thing when you have a use
statement. But it's not required. For example, just using the full string is identical:
use App\Entity\Product;
dump(Product::class === 'App\Entity\Product');
So, you've basically got it right - there's just not need to add the ::class
. Actually, the fact that 'App\Entity\Continent'::class
works surprised me - but I guess PHP is super friendly. And so, in theory, your other solution may have worked with some tweak, but the ::class
simply isn't needed.
I hope this helps! Also, OCP is great, but, if you want some advice that you didn't ask for, be careful not to over-complicate things too early in a project ;). I typically put things into the database and make them dynamic only if I (or an admin) need the ability to change those values at runtime via an admin interface. For something like Language
, it depends on the purpose of your app, but in many cases, if I want to add a Language
, if that required a code change, I'd be ok with that.
Cheers!
Cheers!
Wonderful... It works just fine now, thanks to you... though I had my 20-minutes of head-scratching trying to get it to work. It was the backslash scaping effect during concatenation causing issues.. I invite you to look at my attempts leading to victory :)
`$c = "Continent"; //refers to an Entity class name
dump(Continent::class === 'App\Entity\Continent'); // true
dump(Continent::class === 'App\Entity\'.$c); // false, syntax error for unclosed parantesis.');//now it's closed!
dump(Continent::class === 'App\Entity\'.$c.'); // false.
dump(Continent::class === "App\Entity\".$c); // false. Unclosed '(' on line blahblah");// now it's closed
dd(Continent::class === "App\Entity\\".$c); // true :)`
As to your valuable advices, you are correct. I do want to create an admin dashboard allowing to turn some of the form fields on & off. So that's why I considered adding them to the DB.
And that "Language" thing, it's not related to translation or locale or things like that. It's just another entity just like Continent, Country, etc. A mere list.
I am aware and actually used the Symfony form type country , however, in this project I need 100% control over my code and to allow the admin to tweak the form render to the fullest. i.e. Should the form show the Country field? Which countries should be hidden in the list? All of these to be controlled by an admin.
As to the OCP, It's in my todo list to watch your tutorial on SOLID soon enough ;)
Appreciate your help.
Hey Ali_M!
Nice job experimenting! Yes, the backslash makes perfect sense - that's an annoying character sometimes :). Good luck on the project - it sounds cool!
Cheers!
Hello,
I am trying to fetching data from repository and I found more solutions.
In this course, you are using entityManagerInterface
In official symfony docummentation,
https://symfony.com/doc/current/doctrine.html#fetching-objects-from-the-database is managerRegistry
used
And I found another one objectManager
...
Could you explain me the differrence between each one? Are there any other options I haven't mentioned?
Cheers, Thomas
Hey Tomas,
Good qustion! Well, ManagerRegistry is something more global in Doctrine, it will allow you to get instance to the entity manager iteself, to other Doctrine features. You can look at that service to see what you can do / get with it. With the EntityManagerInterface you only get the part of Doctrine features, i.e. the actual entity manager only. So, if you don't need the whole Doctrine features and only need the entity manager to be able to persist() and flush() data for exmaple - better to inject EntityManagerInterface, as it
s more precise dependency. If you even don't need those persit() and flush() methods - perfect, you probably even don't need an entity manager at all. Maybe you just need an entity repository? Inject the repo you need, e.g. UserRepository in case you need to get access to User entity data. The more specific services you inject - the better. So, it really depends on your specific case.
About ObjectManager - that's too low-level thing usually, mostly used in case to make unit tests easier to set up because it's easier to mock up it.
In short, depending on what you really need to get in your methods - inject the specific services that give you specific features you need, don't need to inject the global objects that gives you literally everything while you're using only a tiny part of it. This will help you when you will be testing your code as well, it would be easier to mock up :)
I hope it clarifies thing for you a bit!
Cheers!
Hi.
How can I use the
`findBy([
'isPaid' => not is null,
'isPrinted'=>null
]);`
to get the not nullable values.
isPaid is a DateTime field, is not boolean.
thank you.
Hey m3tal
You can pass a DateTime object as a parameter if the field is configured as a Datime or Date. In your case it may be better to write a custom query instead of using the generic `findBy()` method
Cheers!
Hi MolloKhan,
thanks for taking your time.
Finally I have opted for this road of creating a custom building because find method doesnt satisfy my needs.
thank you very much.
Hey there,
It seems to me you forgot to define that environment variable, just add it to your .env
or .env.local
file
Cheers!
// composer.json
{
"require": {
"php": "^7.4.1",
"ext-ctype": "*",
"ext-iconv": "*",
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"doctrine/doctrine-bundle": "^2.1", // 2.1.1
"doctrine/doctrine-migrations-bundle": "^3.0", // 3.0.2
"doctrine/orm": "^2.7", // 2.8.2
"knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
"knplabs/knp-time-bundle": "^1.11", // v1.16.0
"sensio/framework-extra-bundle": "^6.0", // v6.2.1
"sentry/sentry-symfony": "^4.0", // 4.0.3
"stof/doctrine-extensions-bundle": "^1.4", // v1.5.0
"symfony/asset": "5.1.*", // v5.1.2
"symfony/console": "5.1.*", // v5.1.2
"symfony/dotenv": "5.1.*", // v5.1.2
"symfony/flex": "^1.3.1", // v1.17.5
"symfony/framework-bundle": "5.1.*", // v5.1.2
"symfony/monolog-bundle": "^3.0", // v3.5.0
"symfony/stopwatch": "5.1.*", // v5.1.2
"symfony/twig-bundle": "5.1.*", // v5.1.2
"symfony/webpack-encore-bundle": "^1.7", // v1.8.0
"symfony/yaml": "5.1.*", // v5.1.2
"twig/extra-bundle": "^2.12|^3.0", // v3.0.4
"twig/twig": "^2.12|^3.0" // v3.0.4
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.0
"symfony/debug-bundle": "5.1.*", // v5.1.2
"symfony/maker-bundle": "^1.15", // v1.23.0
"symfony/var-dumper": "5.1.*", // v5.1.2
"symfony/web-profiler-bundle": "5.1.*", // v5.1.2
"zenstruck/foundry": "^1.1" // v1.5.0
}
}
For anyone interested in getting a custom 404 or other error page.... I wrote a blog post about it:
https://blog.syntaxseed.com...