Routing with Doctrine Inheritance
Lucky you! You found an early release chapter - it will be fully polished and published shortly!
This Chapter isn't quite ready...
Rest assured, the gnomes are hard at work
completing this video!
Let's create a dedicated show page for our starships and check out how routing works with Doctrine inheritance entities. We first need a new controller, so hope over to the terminal and run:
symfony console make:controller Starship
No need for any tests. Alright, jump over to your IDE, and find our shiny new
StarshipController. Start by renaming the method to show(). Next, in the
#[Route] attribute, change the path to /starship/{id}, and name it app_starship_show:
Finally, we're going to use the magic of the Doctrine entity value resolver to
inject the Starship for the passed id. Add Starship $ship as an argument to the show() method:
| // ... lines 1 - 9 | |
| final class StarshipController extends AbstractController | |
| { | |
| ('/starship/{id}', name: 'app_starship_show') | |
| public function index(Starship $ship): Response | |
| // ... lines 14 - 20 | |
| } |
For now, just dd($ship) to see it in action:
| // ... lines 1 - 9 | |
| final class StarshipController extends AbstractController | |
| { | |
| // ... line 12 | |
| public function index(Starship $ship): Response | |
| { | |
| dd($ship); | |
| // ... lines 16 - 19 | |
| } | |
| } |
Linking to the Show Page
Now, head over to our generic teaser template. Find the placeholder link (the one with
a # as the href). Replace that with {{ path('app_starship_show') }} and
{id: ship.id} as the parameters:
| <div class="bg-[#16202A] rounded-2xl pl-5 py-5 pr-11 flex flex-col min-[1174px]:flex-row min-[1174px]:justify-between"> | |
| <div class="flex justify-center min-[1174px]:justify-start"> | |
| // ... line 3 | |
| <div class="ml-5"> | |
| // ... lines 5 - 8 | |
| <h4 class="text-[22px] pt-1 font-semibold"> | |
| <a | |
| // ... line 11 | |
| href="{{ path('app_starship_show', {id: ship.id}) }}" | |
| > | |
| // ... lines 14 - 15 | |
| </a> | |
| </h4> | |
| // ... lines 18 - 21 | |
| </div> | |
| </div> | |
| // ... lines 24 - 34 | |
| </div> |
Refresh our homepage, and click the first freighter. Woo, this dump shows us this
Freighter instance. Go back to the homepage, and click the last mining freighter.
Now the dump shows us a MiningFreighter instance. Even though we type-hinted
Starship, Doctrine knows to instantiate the correct entity class.
If this all works exactly as you expected, you're right, there really isn't anything special about routing and Doctrine inheritance.
Understanding Type Hinting
But take note: if you head back to StarshipController::show(), and change the
type hint from Starship to Scout:
| // ... lines 1 - 10 | |
| final class StarshipController extends AbstractController | |
| { | |
| // ... line 13 | |
| public function index(Scout $ship): Response | |
| // ... lines 15 - 21 | |
| } |
You'll end up with a 404 error when
you refresh the page. Why? Because there is no scout with this ID.
So, to make sure any starship can be found on this route, your type hint
should be Starship, the topmost entity you want to find for this route:
| // ... lines 1 - 9 | |
| final class StarshipController extends AbstractController | |
| { | |
| // ... line 12 | |
| public function index(Starship $ship): Response | |
| // ... lines 14 - 20 | |
| } |
Getting the Show Template Ready
Let's get this controller's template ready. The maker bundle created this
index.html.twig file in our templates/starship directory. Rename it to show.html.twig.
Open it up, and replace the title block with {{ ship.name }}:
| // ... lines 1 - 2 | |
| {% block title %}{{ ship.name }}{% endblock %} | |
| // ... lines 4 - 42 |
Clear out all the boilerplate in the body block.
For the sake of simplicity, copy the contents of our teaser template and paste in the show template's body block.
| // ... lines 1 - 4 | |
| {% block body %} | |
| <div class="bg-[#16202A] rounded-2xl pl-5 py-5 pr-11 flex flex-col min-[1174px]:flex-row min-[1174px]:justify-between"> | |
| <div class="flex justify-center min-[1174px]:justify-start"> | |
| <img class="h-[83px] w-[84px]" src="{{ asset(ship.statusImageFilename) }}" alt="Status: {{ ship.statusString }}"> | |
| <div class="ml-5"> | |
| <div class="rounded-2xl py-1 px-3 flex justify-center w-32 items-center bg-amber-400/10"> | |
| <div class="rounded-full h-2 w-2 bg-amber-400 blur-[1px] mr-2"></div> | |
| <p class="uppercase text-xs text-nowrap">{{ ship.statusString }}</p> | |
| </div> | |
| <h4 class="text-[22px] pt-1 font-semibold"> | |
| <a | |
| class="hover:text-slate-200" | |
| href="{{ path('app_starship_show', {id: ship.id}) }}" | |
| > | |
| {{ ship.type|replace({'_': ' '})|title }} | |
| {{ ship.name }} | |
| </a> | |
| </h4> | |
| <div> | |
| Arrived at: {{ ship.arrivedAt|ago }} | |
| </div> | |
| {% block extra %}{% endblock %} | |
| </div> | |
| </div> | |
| <div class="flex justify-center min-[1174px]:justify-start mt-2 min-[1174px]:mt-0 shrink-0"> | |
| <div class="border-r border-white/20 pr-8"> | |
| <p class="text-slate-400 text-xs text-right">Captain</p> | |
| <p class="text-xl">{{ ship.captain }}</p> | |
| </div> | |
| <div class="pl-8 w-[100px]"> | |
| <p class="text-slate-400 text-xs">Class</p> | |
| <p class="text-xl">{{ ship.class }}</p> | |
| </div> | |
| </div> | |
| </div> | |
| {% endblock %} |
In a real world app, the show template would likely be quite different, offering more details and a different layout.
Back in StarshipController::show(), remove the dd(), change the template to
starship/show.html.twig, and pass ship as a parameter.
| // ... lines 1 - 9 | |
| final class StarshipController extends AbstractController | |
| { | |
| // ... line 12 | |
| public function index(Starship $ship): Response | |
| { | |
| return $this->render('starship/show.html.twig', [ | |
| 'ship' => $ship, | |
| ]); | |
| } | |
| } |
Now, head back to our app and refresh the page. Of course, this looks like the teaser but this is indeed our separate show page.
Implementing Twig Template Inheritance
Just like we did with the teasers, we can use Twig template
inheritance with our show template. This needs a different approach though, since
we're rendering the template within a controller, not with the include() function in
another Twig template.
First, copy the teaser directory to show inside the templates/starship directory.
These will be the custom, entity-specific show templates. Open show/freighter.html.twig,
and change the extends to starship/show.html.twig:
| {% extends 'starship/show.html.twig' %} | |
| {% block extra %} | |
| <div>Cargo Capacity: {{ ship.cargoCapacity }}</div> | |
| {% endblock %} |
In show/mining_freighter.html.twig,
change the extends to starship/show/freighter.html.twig:
| {% extends 'starship/show/freighter.html.twig' %} | |
| {% block extra %} | |
| {{ parent() }} | |
| <div>Laser Power: {{ ship.laserPower }}</div> | |
| {% endblock %} |
We're ready to use these templates, but we need to figure out which one to render.
At first, you might think you could do the same thing with render() and
just change it to an array. But render() is strictly type hinted to a
string for the view. So... we have to get our hands a little dirty.
In StarshipController::show(), let's make some breathing room and
inject the Twig environment service with Environment, the one
from the Twig namespace, $twig:
| // ... lines 1 - 10 | |
| final class StarshipController extends AbstractController | |
| // ... lines 12 - 13 | |
| public function index( | |
| // ... line 15 | |
| Environment $twig, | |
| ): Response { | |
| // ... lines 18 - 26 | |
| } | |
| } |
Next, create a $template variable. For this, we'll use string interpolation to
build the template name: starship/show/{$ship->getType()}.html.twig.
| // ... lines 1 - 10 | |
| final class StarshipController extends AbstractController | |
| // ... lines 12 - 13 | |
| public function index( | |
| // ... lines 15 - 16 | |
| ): Response { | |
| $template = "starship/show/{$ship->getType()}.html.twig"; | |
| // ... lines 19 - 26 | |
| } | |
| } |
This is the template name for the specific starship type.
If this template doesn't exist, we want to fallback to the generic show template.
So, if (!$twig->getLoader()->exists($template)), and inside, set $template to starship/show.html.twig:
| // ... lines 1 - 10 | |
| final class StarshipController extends AbstractController | |
| // ... lines 12 - 13 | |
| public function index( | |
| // ... lines 15 - 16 | |
| ): Response { | |
| // ... lines 18 - 19 | |
| if (!$twig->getLoader()->exists($template)) { | |
| $template = 'starship/show.html.twig'; | |
| } | |
| // ... lines 23 - 26 | |
| } | |
| } |
Finally, replace the string in the render() method with our new $template variable:
| // ... lines 1 - 10 | |
| final class StarshipController extends AbstractController | |
| // ... lines 12 - 13 | |
| public function index( | |
| // ... lines 15 - 16 | |
| ): Response { | |
| // ... lines 18 - 23 | |
| return $this->render($template, [ | |
| 'ship' => $ship, | |
| ]); | |
| } | |
| } |
This is very similar to what Twig's include() function does for us when you pass an array.
Jump over to our app and refresh... Cool, since this is a mining freighter, we now see the cargo capacity and laser power. Go to the homepage and click the first normal freighter. Nice, we just see the cargo capacity.
You can hover over the Twig icon in the web debug toolbar to see the template used. Sure enough,
starship/show/freighter.html.twig.
Go to the homepage again, and click a scout. Cool, no extra details, and the toolbar shows the
generic starship/show.html.twig template is being rendered.
Our home-grown dynamic template system is working!
Up next, we'll explore how associations work with Doctrine Inheritance, and what considerations you need to keep in mind when using them.
Comments
"Houston: no signs of life"
Start the conversation!