Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

New Bundle, New Service: KnpTimeBundle

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

On our site, you can create your own vinyl mix. (Or you'll eventually be able to do this... right now, this button doesn't do anything). But another great feature of our site is the ability to browse other user's mixes.

Now that I'm looking at this, it might be useful if we could see when each mix was created.

If you don't remember where in our code this page was built, you can use a trick. Down on the web debug toolbar, hover over the 200 status code. Ah, ha! This shows us that the controller behind this page is VinylController::browse.

Cool! Go open up src/Controller/VinylController.php. Here is the browse action:

... lines 1 - 9
class VinylController extends AbstractController
{
... lines 12 - 29
#[Route('/browse/{slug}', name: 'app_browse')]
public function browse(string $slug = null): Response
{
$genre = $slug ? u(str_replace('-', ' ', $slug))->title(true) : null;
$mixes = $this->getMixes();
return $this->render('vinyl/browse.html.twig', [
'genre' => $genre,
'mixes' => $mixes,
]);
}
... lines 41 - 65
}

By the way, I did update the code a little bit since episode one... so make sure you've got a fresh copy if you're coding along with me.

This method calls $this->getMixes()... which is a private function I created down at the bottom:

... lines 1 - 9
class VinylController extends AbstractController
{
... lines 12 - 41
private function getMixes(): array
{
// temporary fake "mixes" data
return [
[
'title' => 'PB & Jams',
'trackCount' => 14,
'genre' => 'Rock',
'createdAt' => new \DateTime('2021-10-02'),
],
[
'title' => 'Put a Hex on your Ex',
'trackCount' => 8,
'genre' => 'Heavy Metal',
'createdAt' => new \DateTime('2022-04-28'),
],
[
'title' => 'Spice Grills - Summer Tunes',
'trackCount' => 10,
'genre' => 'Pop',
'createdAt' => new \DateTime('2019-06-20'),
],
];
}
}

This returns a big array of fake data that represents the mixes we're going to render on the page. Eventually, we'll get this from a dynamic source, like a database.

Printing Dates in Twig

Notice that each mix has a createdAt date field. We get these mixes up in browse()... and pass them as a mixes variable into vinyl/browse.html.twig. Let's jump into that template.

Down here, we use Twig's for loop to loop over mixes. Simple enough!

... lines 1 - 3
<div class="container">
... lines 5 - 25
<div>
<h2 class="mt-5">Mixes</h2>
<div class="row">
{% for mix in mixes %}
<div class="col col-md-4">
<div class="mixed-vinyl-container p-3 text-center">
<img src="https://via.placeholder.com/300" data-src="https://via.placeholder.com/300" alt="Square placeholder img">
<p class="mt-2"><strong>{{ mix.title }}</strong></p>
<span>{{ mix.trackCount }} Tracks</span>
|
<span>{{ mix.genre }}</span>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
... lines 43 - 44

Let's also now print the "created at" date. Add a |, another <span> and then say {{ mix.createdAt }}.

There's just one problem. If you look at createdAt... it's a DateTime object. And you can't just print DateTime objects... you'll get a big error reminding you... that you can't just print DateTime objects. Cruel world!

Fortunately, Twig has a handy date filter. We talked about filters briefly in the first episode: we using them by adding a | after some value and then the name of the filter. This particular filter also takes an argument, which is the format the date should be printed. To keep things simple, let's use Y-m-d, or "year-month-day".

... lines 1 - 3
<div class="container">
... lines 5 - 25
<div>
<h2 class="mt-5">Mixes</h2>
<div class="row">
{% for mix in mixes %}
<div class="col col-md-4">
<div class="mixed-vinyl-container p-3 text-center">
... lines 32 - 35
<span>{{ mix.genre }}</span>
|
<span>{{ mix.createdAt|date('Y-m-d') }}</span>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
... lines 45 - 46

Head over and refresh and... okay! We can now see when each was created, though the format isn't very attractive. We could do more work to spruce this up... but it would be way cooler if we could print this out in the "ago" format.

You've probably seen it before.... like for comments on a blog post... they say something like "posted three months ago" or "posted 10 minutes ago".

So... the question is: How can we convert a DateTime object into that nice "ago" format? Well, that sounds like work to me and, as I said earlier, work in Symfony is done by a service. So the real question is: Is there a service in Symfony that can convert DateTime objects to the "ago" format? The answer is... no. But there is a third party bundle that can give us that service.

Installing KnpTimeBundle

Go to https://github.com/KnpLabs/KnpTimeBundle. If you look at this bundle's documentation, you'll see that it gives us a service that can do that conversion. So... let's get it installed!

Scroll to the composer require line, copy that, spin over to our terminal, and paste.

composer require knplabs/knp-time-bundle

Cool! This grabbed knplabs/knp-time-bundle... as well as symfony/translation: Symfony's translation component, which is a dependency of KnpTimeBundle. Near the bottom, it also configured two recipes. Let's see what those did. Run:

git status

Awesome! Any time you install a third party package, Composer will always modify your composer.json and composer.lock files. This also updated the config/bundles.php file:

<?php
return [
... lines 4 - 11
Knp\Bundle\TimeBundle\KnpTimeBundle::class => ['all' => true],
];

That's because we just installed a bundle - KnpTimeBundle - and its recipe handled that automatically. It also looks like the translation recipe added a config file and a translations/ directory. The translator is needed to use KnpTimeBundle... but we won't need to work with it directly.

So... what did installing this new bundle give us? Services of course! Let's find and use those next!

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "knplabs/knp-time-bundle": "^1.18", // v1.19.0
        "symfony/asset": "6.1.*", // v6.1.0-RC1
        "symfony/console": "6.1.*", // v6.1.0-RC1
        "symfony/dotenv": "6.1.*", // v6.1.0-RC1
        "symfony/flex": "^2", // v2.1.8
        "symfony/framework-bundle": "6.1.*", // v6.1.0-RC1
        "symfony/http-client": "6.1.*", // v6.1.0-RC1
        "symfony/monolog-bundle": "^3.0", // v3.8.0
        "symfony/runtime": "6.1.*", // v6.1.0-RC1
        "symfony/twig-bundle": "6.1.*", // v6.1.0-RC1
        "symfony/ux-turbo": "^2.0", // v2.1.1
        "symfony/webpack-encore-bundle": "^1.13", // v1.14.1
        "symfony/yaml": "6.1.*", // v6.1.0-RC1
        "twig/extra-bundle": "^2.12|^3.0", // v3.4.0
        "twig/twig": "^2.12|^3.0" // v3.4.0
    },
    "require-dev": {
        "symfony/debug-bundle": "6.1.*", // v6.1.0-RC1
        "symfony/maker-bundle": "^1.41", // v1.42.0
        "symfony/stopwatch": "6.1.*", // v6.1.0-RC1
        "symfony/web-profiler-bundle": "6.1.*" // v6.1.0-RC1
    }
}