17.

Smart Model Methods & Making the Design Dynamic

|

Share this awesome video!

|

Adding the .value to the end of the enum to print it works like a charm. But I want to show another, more elegant, solution.

Adding Smarter Model Methods

In Starship, it'll probably be common for us to want to get the string status of a Starship. To make that easier, why not add a shortcut method here called getStatusString()? This will return a string, and inside, return $this->status->value.

46 lines | src/Model/Starship.php
// ... lines 1 - 4
class Starship
{
// ... lines 7 - 40
public function getStatusString(): string
{
return $this->status->value;
}
}

Thanks to this, over in the template, we can shorten to ship.statusString.

54 lines | templates/main/homepage.html.twig
// ... lines 1 - 4
{% block body %}
<main class="flex flex-col lg:flex-row">
// ... lines 7 - 8
<div class="px-12 pt-10 w-full">
// ... lines 10 - 13
<div class="space-y-5">
{% for ship in ships %}
<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 18
<div class="ml-5">
<div class="rounded-2xl py-1 px-3 flex justify-center w-32 items-center bg-amber-400/10">
// ... line 21
<p class="uppercase text-xs text-nowrap">{{ ship.statusString }}</p>
</div>
// ... lines 24 - 29
</div>
</div>
// ... lines 32 - 42
</div>
{% endfor %}
</div>
// ... lines 46 - 50
</div>
</main>
{% endblock %}

Oh, and this is more Twig smartness! There is no property called statusString on Starship! But Twig doesn't care. It sees that there is a getStatusString() method and calls that.

Watch: when we refresh, the page still works. I really like this solution, so I'll copy that... and repeat it up here for the alt attribute.

54 lines | templates/main/homepage.html.twig
// ... lines 1 - 4
{% block body %}
<main class="flex flex-col lg:flex-row">
// ... lines 7 - 8
<div class="px-12 pt-10 w-full">
// ... lines 10 - 13
<div class="space-y-5">
{% for ship in ships %}
<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="/images/status-in-progress.png" 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">
// ... line 21
<p class="uppercase text-xs text-nowrap">{{ ship.statusString }}</p>
</div>
// ... lines 24 - 29
</div>
</div>
// ... lines 32 - 42
</div>
{% endfor %}
</div>
// ... lines 46 - 50
</div>
</main>
{% endblock %}

And while we're fixing this, in show.html.twig, we print the status there too. So I'll make that same change... then close this.

40 lines | templates/starship/show.html.twig
// ... lines 1 - 4
{% block body %}
// ... lines 6 - 11
<div class="md:flex justify-center space-x-3 mt-5 px-4 lg:px-8">
// ... lines 13 - 15
<div class="space-y-5">
<div class="mt-8 max-w-xl mx-auto">
<div class="px-8 pt-8">
<div class="rounded-2xl py-1 px-3 flex justify-center w-32 items-center bg-amber-400/10">
// ... line 20
<p class="uppercase text-xs">{{ ship.statusString }}</p>
</div>
// ... lines 23 - 34
</div>
</div>
</div>
</div>
{% endblock %}

Finishing our Dynamic Template

Ok: let's finish making our homepage template dynamic: it's smooth space sailing from here. For the ship name, {{ ship.name }}, for the captain, {{ ship.captain }}. And down here for the class, {{ ship.class }}.

54 lines | templates/main/homepage.html.twig
// ... lines 1 - 4
{% block body %}
<main class="flex flex-col lg:flex-row">
// ... lines 7 - 8
<div class="px-12 pt-10 w-full">
// ... lines 10 - 13
<div class="space-y-5">
{% for ship in ships %}
<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 18
<div class="ml-5">
// ... lines 20 - 23
<h4 class="text-[22px] pt-1 font-semibold">
<a
// ... lines 26 - 27
>{{ ship.name }}</a>
</h4>
</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">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>
{% endfor %}
</div>
// ... lines 46 - 50
</div>
</main>
{% endblock %}

Oh, and let's fill in the link: {{ path() }} then the name of the route. We're linking to the show page for the ship, so the route is app_starship_show. And because this has an id wildcard, pass id set to ship.id.

54 lines | templates/main/homepage.html.twig
// ... lines 1 - 4
{% block body %}
<main class="flex flex-col lg:flex-row">
// ... lines 7 - 8
<div class="px-12 pt-10 w-full">
// ... lines 10 - 13
<div class="space-y-5">
{% for ship in ships %}
<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 18
<div class="ml-5">
// ... lines 20 - 23
<h4 class="text-[22px] pt-1 font-semibold">
<a
class="hover:text-slate-200"
href="{{ path('app_starship_show', { id: ship.id }) }}"
>{{ ship.name }}</a>
</h4>
</div>
</div>
// ... lines 32 - 42
</div>
{% endfor %}
</div>
// ... lines 46 - 50
</div>
</main>
{% endblock %}

And now, so much better! It looks nice and we can click these links.

Dynamic Image Paths

But... the image is still broken. Earlier, when we copied the images into our assets/ directory, I included three files for the three statuses. Up here, we are "kind of" pointing to the in progress status... but this isn't the right way to reference images in the assets/ directory. Instead, say {{ asset() }} and pass the path relative to the assets/ directory, called the "logical" path.

If we try that now... we're closer. But the "in progress" part needs to be dynamic based on the status. One thing we could try is Twig concatenation: to add ship.status to the string. That is possible, though it's a bit ugly.

Instead, let's revisit the solution we used a minute ago: making all the data about our Starship easily accessible... from the Starship class.

Here's what I mean: add a public function getStatusImageFilename() that returns a string.

55 lines | src/Model/Starship.php
// ... lines 1 - 4
class Starship
{
// ... lines 7 - 45
public function getStatusImageFilename(): string
{
// ... lines 48 - 52
}
}

Let's do all the logic for creating the filename right here. I'll paste in a match function.

This says: check $this->status and if it's equal to WAITING, return this string. If it's equal to IN_PROGRESS return this string and so on.

55 lines | src/Model/Starship.php
// ... lines 1 - 4
class Starship
{
// ... lines 7 - 45
public function getStatusImageFilename(): string
{
return match ($this->status) {
StarshipStatusEnum::WAITING => 'images/status-waiting.png',
StarshipStatusEnum::IN_PROGRESS => 'images/status-in-progress.png',
StarshipStatusEnum::COMPLETED => 'images/status-complete.png',
};
}
}

And exactly like before, because we have a getStatusImageFilename() method, we get to, in Twig, pretend like we have a statusImageFilename property.

54 lines | templates/main/homepage.html.twig
// ... lines 1 - 4
{% block body %}
<main class="flex flex-col lg:flex-row">
// ... lines 7 - 8
<div class="px-12 pt-10 w-full">
// ... lines 10 - 13
<div class="space-y-5">
{% for ship in ships %}
<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 }}">
// ... lines 19 - 30
</div>
// ... lines 32 - 42
</div>
{% endfor %}
</div>
// ... lines 46 - 50
</div>
</main>
{% endblock %}

And now, we got it!

Last Details of Making the Design Dynamic

Final things! Let's fill in some missing links, like this logo should go to the homepage. Right now... it goes nowhere.

Remember, when we want to link to a page, we need to make sure that route has a name. In src/Controller/MainController.php... our homepage does not have a name. Ok, it has an auto-generated name, but we don't want to rely on that.

Add name: set to app_homepage. Or you could use app_main_homepage.

24 lines | src/Controller/MainController.php
// ... lines 1 - 9
class MainController extends AbstractController
{
#[Route('/', name: 'app_homepage')]
public function homepage(StarshipRepository $starshipRepository): Response
// ... lines 14 - 22
}

To link the logo, in base.html.twig... here it is... Use {{ path('app_homepage') }}.

44 lines | templates/base.html.twig
<!DOCTYPE html>
<html>
// ... lines 3 - 13
<body class="text-white" style="background: radial-gradient(102.21% 102.21% at 50% 28.75%, #00121C 42.62%, #013954 100%);">
<div class="flex flex-col justify-between min-h-screen relative">
<div>
<header class="h-[114px] shrink-0 flex flex-col sm:flex-row items-center sm:justify-between py-4 sm:py-0 px-6 border-b border-white/20 shadow-md">
<a href="{{ path('app_homepage') }}">
// ... line 19
</a>
// ... lines 21 - 34
</header>
// ... line 36
</div>
// ... lines 38 - 40
</div>
</body>
</html>

Copy that and repeat it below for another home link.

44 lines | templates/base.html.twig
<!DOCTYPE html>
<html>
// ... lines 3 - 13
<body class="text-white" style="background: radial-gradient(102.21% 102.21% at 50% 28.75%, #00121C 42.62%, #013954 100%);">
<div class="flex flex-col justify-between min-h-screen relative">
<div>
<header class="h-[114px] shrink-0 flex flex-col sm:flex-row items-center sm:justify-between py-4 sm:py-0 px-6 border-b border-white/20 shadow-md">
<a href="{{ path('app_homepage') }}">
// ... line 19
</a>
<nav class="flex space-x-4 font-semibold">
<a class="hover:text-amber-400 pt-2" href="{{ path('app_homepage') }}">
Home
</a>
// ... lines 25 - 33
</nav>
</header>
// ... line 36
</div>
// ... lines 38 - 40
</div>
</body>
</html>

We'll leave these other links for a future tutorial.

Back at the browser, click that logo! All good. The final missing link is over on the show page. This "back" link should also go to the homepage. Open up show.html.twig. And up on top - there it is - I'll paste that same link.

40 lines | templates/starship/show.html.twig
// ... lines 1 - 4
{% block body %}
<div class="my-4 px-8">
<a class="bg-white hover:bg-gray-200 rounded-xl p-2 text-black" href="{{ path('app_homepage') }}">
<svg class="inline text-black" xmlns="http://www.w3.org/2000/svg" height="16" width="14" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path fill="#000" d="M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l160 160c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L109.2 288 416 288c17.7 0 32-14.3 32-32s-14.3-32-32-32l-306.7 0L214.6 118.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-160 160z"/></svg>
Back
</a>
</div>
// ... lines 12 - 38
{% endblock %}

Ok team, the design is done! Congrats! Treat yourself to a tea... or latte... or donut or a walk amongst nature to celebrate. Because this is huge! Our site looks and feels real. I'm thrilled.

Now we can focus on the finer details. Like, when we click this link, the sidebar is supposed to collapse. To handle that, I want to introduce you to my favorite tool for writing JavaScript: Stimulus.