Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Finding & Using the Services from a Bundle

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.

We just installed KnpTimeBundle. Hooray! Um... but... uh... what does that mean? What did doing that give us?

The number one thing that a bundle gives us is... services! What services does this bundle give us? Well, we could, of course, read the documentation, blah, blah. Well, ok, you should do that... but, come on! Let's venture ahead recklessly and learn by exploring!

In the last tutorial, we learned about a command that shows us all of the services in our app: debug:autowiring:

php bin/console debug:autowiring

For example, if we search for "logger", there's apparently a service called LoggerInterface. We also learned that we can autowire any service in this list into our controller by using its type. By using this LoggerInterface type - which is actually Psr\Log\LoggerInterface - Symfony knows to pass us this service. Then, down here, we call methods on it like $logger->info().

We installed KnpTimeBundle a moment ago, so let's search for "time":

php bin/console debug:autowiring time

And... hey! Look at this! We have a new DateTimeFormatter service! That's from the new bundle and I bet that's what we're looking for. Let's go use it in our controller.

Using the New DateTimeFormatter Service

The type-hint we need is Knp\Bundle\TimeBundle\DateTimeFormatter. Ok! In VinylController, find browse(), then add the new argument.

By the way, the order of the arguments does not matter... except when it comes to optional arguments. I made the $slug argument optional and you typically need your optional arguments at the end of the list. So I'll add DateTimeFormatter right here and hit "tab" to add the use statement on top.

We can name the argument anything we want, like $sherlockHolmes or $timeFormatter:

... lines 1 - 4
use Knp\Bundle\TimeBundle\DateTimeFormatter;
... lines 6 - 10
class VinylController extends AbstractController
{
... lines 13 - 31
public function browse(DateTimeFormatter $timeFormatter, string $slug = null): Response
{
... lines 34 - 45
}
... lines 47 - 71
}

To use this, loop over the mixes - foreach ($mixes as $key => $mix):

... lines 1 - 4
use Knp\Bundle\TimeBundle\DateTimeFormatter;
... lines 6 - 10
class VinylController extends AbstractController
{
... lines 13 - 31
public function browse(DateTimeFormatter $timeFormatter, string $slug = null): Response
{
... lines 34 - 36
foreach ($mixes as $key => $mix) {
... line 38
}
... lines 40 - 45
}
... lines 47 - 71
}

then, on each, add a new ago key: $mixes[$key]['ago'] =... and this is where we need the new service. How do we use the DateTimeFormatter? I have no idea! But we used its type, so PhpStorm should tell us what methods it has. Type $timeFormatter->... and ok! It has 4 public methods.

The one we want is formatDiff(). Pass it the "from" time... which is $mix['createdAt']:

... lines 1 - 4
use Knp\Bundle\TimeBundle\DateTimeFormatter;
... lines 6 - 10
class VinylController extends AbstractController
{
... lines 13 - 31
public function browse(DateTimeFormatter $timeFormatter, string $slug = null): Response
{
... lines 34 - 36
foreach ($mixes as $key => $mix) {
$mixes[$key]['ago'] = $timeFormatter->formatDiff($mix['createdAt']);
}
... lines 40 - 45
}
... lines 47 - 71
}

That's all we need! We're looping over these $mixes, taking the createdAt key, which is a DateTime object, passing it to the formatDiff() method, which should return a string in the "ago" format. To see if this is working, below, dd($mixes):

... lines 1 - 4
use Knp\Bundle\TimeBundle\DateTimeFormatter;
... lines 6 - 10
class VinylController extends AbstractController
{
... lines 13 - 31
public function browse(DateTimeFormatter $timeFormatter, string $slug = null): Response
{
... lines 34 - 36
foreach ($mixes as $key => $mix) {
$mixes[$key]['ago'] = $timeFormatter->formatDiff($mix['createdAt']);
}
dd($mixes);
... lines 41 - 45
}
... lines 47 - 71
}

Let's try it! Spin over, refresh... and let's open it up. Yes! Look at that: "ago" => "7 months ago"... "ago" => "18 days ago"... It works. So remove that dump:

... lines 1 - 10
class VinylController extends AbstractController
{
... lines 13 - 31
public function browse(DateTimeFormatter $timeFormatter, string $slug = null): Response
{
... lines 34 - 36
foreach ($mixes as $key => $mix) {
$mixes[$key]['ago'] = $timeFormatter->formatDiff($mix['createdAt']);
}
return $this->render('vinyl/browse.html.twig', [
... lines 42 - 43
]);
}
... lines 46 - 70
}

And now that each mix has a new ago field, in browse.html.twig, replace the mix.createdAt|date code with mix.ago:

... 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.ago }}</span>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
... lines 45 - 46

And now... much better.

So: we had a problem... and knew it needed to be solved by a service... because services do work. We didn't have a service that did what we needed yet, so we went out, found one, and installed it. Problem solved! Symfony itself has a ton of different packages, and each of them gives us several services. But sometimes you'll need a third party bundle like this one to get the job done. Typically, you can just search online for the problem you're trying to solve, plus "Symfony bundle", to find it.

Using the ago Twig Filter

In addition to the nice DateTimeFormatter service that we just used, this bundle also gave us another service. But, this isn't a service that we're meant to use directly, like in the controller. Nope! This service is meant to be used by Twig itself... to power a brand new Twig filter! That's right! You can add custom functions, filters... or anything to Twig.

To see the new filter, let's try another useful debugging command:

php bin/console debug:twig

This prints a list of all of the functions, filters, and tests in Twig, along with the one global Twig variable we have. If you go up to Filters, there's a new one called "ago"! That was not there before we installed KnpTimeBundle.

So, all of the work we did in our controller is perfectly fine ... but it turns out that there's an easier way to do all of this. Delete the foreach... remove the DateTimeFormatter service... and, though it's optional, clean up the extra use statement on top:

... 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
}

In browse.html.twig, we don't have an ago field anymore... but we still have a createdAt field. Instead of piping this into the date filter, pipe it to ago:

... 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|ago }}</span>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
... lines 45 - 46

That's all we need! Back over on the site refresh and... we get the exact same result.

By the way, we won't do it in this tutorial, but by the end, you'll be able to easily follow the documentation to create your own custom Twig functions and filters.

Ok, so our app does not have a database yet... and it won't until the next episode. But to make things more interesting, let's get our mixes data by making an HTTP call to a special GitHub repository. That's 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
    }
}