Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Querying for a Single Entity for a "Show" Page

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

Our users really need to be able to click on a mix and navigate to a page with more information about it... like eventually its track list! So let's make that possible! Let's create a page to display just one mix's details.

Creating the new Route & Controller

Head over to src/Controller/MixController.php. After the new action, add public function show() with the [#Route()] attribute above. The URL for this will be... how about /mix/{id}, where id will be the ID of that mix in the database. Below, add the corresponding $id argument. And... just to see if this is working, dd($id).

... lines 1 - 10
class MixController extends AbstractController
{
... lines 13 - 33
#[Route('/mix/{id}')]
public function show($id): Response
{
dd($id);
}
}

Coolio! Spin over and go to, how about, /mix/7. Awesome! Our route and controller are hooked up!

Querying for a Single Object

Ok, now that we have the ID, we need to query for the one VinylMix in the database matching that. And we know how to query: via the repository. Add a second argument to the method type-hinted with VinylMixRepository and call it $mixRepository. Now replace the dd() with $mix = $mixRepository-> and, for the first time, we're going to use the find() method. It's dead simple: it finds a single object using the primary key. So pass it $id. To make sure this is working, dd($mix).

... lines 1 - 5
use App\Repository\VinylMixRepository;
... lines 7 - 11
class MixController extends AbstractController
{
... lines 14 - 35
public function show($id, VinylMixRepository $mixRepository): Response
{
$mix = $mixRepository->find($id);
dd($mix);
}
}

We don't know which IDs we actually have in our database right now, so as a workaround, go to /mix/new to create a new mix. In my case, it has ID 16. Cool: go to /mix/16 and... hello VinylMix id: 16! The important thing to notice is that this returns a VinylMix object. Unless you do something custom, Doctrine always gives us back either a single object or an array of objects, depending on which method you call.

Rendering the Template

Now that we have the VinylMix object, let's render a template and pass that in. Do that with return $this->render() and call the template mix/show.html.twig. The template path could be anything, but since we're inside MixController, the directory mix makes sense. And since we're in the show action, show.html.twig also makes sense. Consistency is a great way to make friends with your fellow teammates!

Pass in a variable called mix set to the VinylMix object $mix.

... lines 1 - 35
public function show($id, VinylMixRepository $mixRepository): Response
{
... lines 38 - 39
return $this->render('mix/show.html.twig', [
'mix' => $mix,
]);
}
... lines 44 - 45

All right, let's go create that template. In templates/, add a new directory called mix/... and inside of that, a new file called show.html.twig. Pretty much every template is going to start the same way. Begin by saying {% extends 'base.html.twig' %}.

{% extends 'base.html.twig' %}
... lines 2 - 8

As a reminder, base.html.twig has several blocks in it. The most important one down here is block body. That's what we'll override with our content. At the top, there's also a block title, which allows us to control the title of the page. Let's override both.

Say {% block title %}{% endblock %} and, in between, {{ mix.title }} Mix. Then override {% block body %} with {% endblock %} below. Inside, just to get started, add an <h1> with {{ mix.title }}.

... lines 1 - 2
{% block title %}{{ mix.title }} Mix{% endblock %}
{% block body %}
<h1>{{ mix.title }}</h1>
{% endblock %}

When we try that... hello page! This is super simple - the <h1> isn't even in the right place - but it's working. Now we can add some pizzazz.

Making the Page All Fancy Looking

I'm going to head back to my template and paste in a bunch of new content. You can copy this from the code block on this page. The top of this is exactly the same: it extends base.html.twig and the block title looks like it did before. But then, in the body, we have a bunch of new markup, we print the mix title... and down here, I have a few TODOs for us where we'll print out more details.

... lines 1 - 4
{% block body %}
<div class="container">
<h1 class="d-inline me-3">{{ mix.title }}</h1>
<div class="row mt-5">
<div class="col-12 col-md-4">
<svg width="100%" height="100%" viewBox="0 0 496 496" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
... lines 12 - 32
</svg>
</div>
<div class="col-12 col-md-8 ps-5">
TODO: print track count, genre and description
</div>
</div>
</div>
{% endblock %}

If you refresh now... nice! We even have the cute little record SVG... which you probably recognize from the homepage. That's awesome... except that duplicating this entire SVG in both templates is... not so awesome. Let's fix that duplication.

Avoiding Duplication with a Template Partial

Select all of this <svg> content, copy it, and over in the mix/ directory, create a new file called _recordSvg.html.twig. Paste that here!

<svg width="100%" height="100%" viewBox="0 0 496 496" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-1">
<stop stop-color="#C380F3" offset="0%"></stop>
<stop stop-color="#4A90E2" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Mixed-Vinyl" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group">
<g id="record-vinyl" fill="#000000" fill-rule="nonzero">
<path d="M248,144 C190.562386,144 144,190.562386 144,248 C144,305.437614 190.562386,352 248,352 C305.437614,352 352,305.437614 352,248 C352,190.562386 305.437614,144 248,144 L248,144 Z M248,272 C234.745166,272 224,261.254834 224,248 C224,234.745166 234.745166,224 248,224 C261.254834,224 272,234.745166 272,248 C272,261.254834 261.254834,272 248,272 Z M248,0 C111,0 0,111 0,248 C0,385 111,496 248,496 C385,496 496,385 496,248 C496,111 385,0 248,0 Z M248,376 C177.307552,376 120,318.692448 120,248 C120,177.307552 177.307552,120 248,120 C318.692448,120 376,177.307552 376,248 C376,281.947711 362.514324,314.505012 338.509668,338.509668 C314.505012,362.514324 281.947711,376 248,376 Z" id="Shape"></path>
</g>
<g id="record-vinyl" transform="translate(144.000000, 144.000000)" fill="url(#linearGradient-1)" fill-rule="nonzero">
<path d="M104,0 C46.562386,0 0,46.562386 0,104 C0,161.437614 46.562386,208 104,208 C161.437614,208 208,161.437614 208,104 C208,46.562386 161.437614,0 104,0 L104,0 Z M104,128 C90.745166,128 80,117.254834 80,104 C80,90.745166 90.745166,80 104,80 C117.254834,80 128,90.745166 128,104 C128,117.254834 117.254834,128 104,128 Z" id="Shape"></path>
</g>
<circle id="Oval" stroke="#979797" cx="248" cy="248" r="235"></circle>
<circle id="Oval" stroke="#979797" cx="248" cy="248" r="215"></circle>
<circle id="Oval" stroke="#979797" cx="248" cy="248" r="195"></circle>
<circle id="Oval" stroke="#979797" cx="248" cy="248" r="175"></circle>
<circle id="Oval" stroke="#979797" cx="248" cy="248" r="155"></circle>
</g>
</g>
</svg>

The reason I prefixed the name with _ is to indicate that this is a template partial. That means it's a template that doesn't include a whole page - just part of a page. The _ is optional... and just something that's done as a common convention: it doesn't change any behavior.

Thanks to this, we can go into show.html.twig and {{ include('mix/_recordSvg.html.twig) }}.

... lines 1 - 4
{% block body %}
<div class="container">
... lines 7 - 8
<div class="col-12 col-md-4">
{{ include('mix/_recordSvg.html.twig') }}
</div>
... lines 12 - 16
</div>
{% endblock %}

Let's go do the same thing in the homepage template: templates/vinyl/homepage.html.twig. This is the same SVG here, so we'll include that same template.

... lines 1 - 4
{% block body %}
<div class="container">
... lines 7 - 8
<div class="col-12 col-md-4">
{{ include('mix/_recordSvg.html.twig') }}
</div>
... lines 12 - 34
</div>
{% endblock %}

Nice! If we go check the homepage... it still looks great! And if we head back to the mix page and refresh... that looks great too!

To finish the template, let's fill in the missing details. Add an <h2> with class="mb-4", and inside, say {{ mix.trackCount }} songs, followed by a <small> tag with (genre: {{ mix.genre }})... and below this, a <p> tag with {{ mix.description }}.

... lines 1 - 4
{% block body %}
... lines 6 - 7
<div class="row mt-5">
... lines 9 - 11
<div class="col-12 col-md-8 ps-5">
<h2 class="mb-4">{{ mix.trackCount }} songs <small>(genre: {{ mix.genre }})</small></h2>
<p>{{ mix.description }}</p>
</div>
</div>
... line 17
{% endblock %}

And now... this is starting to come to life! We don't have a track list yet... because that's another database table we'll create in a future tutorial. But it's a nice start.

Linking to the Show Page

To complete the new feature, when we're on the /browse page, we need to link each mix to its show page. Open templates/vinyl/browse.html.twig and scroll down to where we loop. Ok: change the <div> that surrounds everything to an <a> tag. Then... break this onto multiple lines and add href="". As you can see, PhpStorm was clever enough to update the closing tag to an a automatically.

To link to a page in Twig, we use the path() function and pass the name of the route. What... is the name of the route to our show page? The answer is... it doesn't have one! Ok, Symfony auto-generates a name... but we don't want to rely on that. As soon as we want to link to a route, we should give that route a proper name. How about app_mix_show.

... lines 1 - 11
class MixController extends AbstractController
{
... lines 14 - 34
#[Route('/mix/{id}', name: 'app_mix_show')]
public function show($id, VinylMixRepository $mixRepository): Response
... lines 37 - 47
}

Copy that, head back to browse.html.twig and paste.

But this time, just pasting the route name isn't going to be enough! Check out this sweet error:

Some mandatory parameters are missing ("id") to generate a URL for route "app_mix_show".

That makes sense! Symfony is trying to generate the URL to this route, but we need to tell it what wildcard value to use for {id}. We do that by passing a second array argument with {}. Inside set id to mix.id.

... lines 1 - 2
{% block body %}
... lines 4 - 28
{% for mix in mixes %}
<div class="col col-md-4">
<a href="{{ path('app_mix_show', {
id: mix.id
}) }}" class="mixed-vinyl-container p-3 text-center">
... lines 34 - 42
</a>
... line 44
{% endfor %}
... lines 46 - 48
{% endblock %}

And now... the page works! And we can click any of these to hop in and see more details.

Okay, we've got the happy path working! But what if no mix can be found for a given ID? Next: let's talk 404 pages and learn how we can be even lazier by getting Symfony to query for the VinylMix object for us.

Leave a comment!

2
Login or Register to join the conversation
wh Avatar
wh Avatar wh | posted 7 months ago | edited

why (at -0:37) you say "array" but use an object syntax?

Reply

Hey Wh,

Actually, that's just a Twig syntax for associative arrays. For indexed arrays you're using ['a', 'b', 'c'] but for associative ones: {'a': 'b', 'c': 'd'}. So, it looks like a JS object but technically it's a simple PHP array :)

Cheers!

1 Reply
Cat in space

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

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.7", // v3.7.0
        "doctrine/doctrine-bundle": "^2.7", // 2.7.0
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.12", // 2.12.3
        "knplabs/knp-time-bundle": "^1.18", // v1.19.0
        "pagerfanta/doctrine-orm-adapter": "^3.6", // v3.6.1
        "pagerfanta/twig": "^3.6", // v3.6.1
        "sensio/framework-extra-bundle": "^6.2", // v6.2.6
        "stof/doctrine-extensions-bundle": "^1.7", // v1.7.0
        "symfony/asset": "6.1.*", // v6.1.0
        "symfony/console": "6.1.*", // v6.1.2
        "symfony/dotenv": "6.1.*", // v6.1.0
        "symfony/flex": "^2", // v2.2.2
        "symfony/framework-bundle": "6.1.*", // v6.1.2
        "symfony/http-client": "6.1.*", // v6.1.2
        "symfony/monolog-bundle": "^3.0", // v3.8.0
        "symfony/proxy-manager-bridge": "6.1.*", // v6.1.0
        "symfony/runtime": "6.1.*", // v6.1.1
        "symfony/twig-bundle": "6.1.*", // v6.1.1
        "symfony/ux-turbo": "^2.0", // v2.3.0
        "symfony/webpack-encore-bundle": "^1.13", // v1.15.1
        "symfony/yaml": "6.1.*", // v6.1.2
        "twig/extra-bundle": "^2.12|^3.0", // v3.4.0
        "twig/twig": "^2.12|^3.0" // v3.4.1
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
        "symfony/debug-bundle": "6.1.*", // v6.1.0
        "symfony/maker-bundle": "^1.41", // v1.44.0
        "symfony/stopwatch": "6.1.*", // v6.1.0
        "symfony/web-profiler-bundle": "6.1.*", // v6.1.2
        "zenstruck/foundry": "^1.21" // v1.21.0
    }
}
userVoice