Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

Service Objects

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.

Symfony is really two parts... and we've already learned one of them.

The first part is the route and controller system. And I hope you're feeling pretty comfortable: create a route, it executes a controller function, we return a response.

The second half of Symfony is all about the many "useful objects" that are floating around inside Symfony. For example, when we render a template, what we're actually doing is taking advantage of a twig object and asking it to render. The render() method is just a shortcut to use that object. There is also a logger object, a cache object and many more, like a database connection object and an object that helps make HTTP requests to other APIs.

Basically... every single thing that Symfony does - or that we do - is actually done by one of these useful objects. Heck, even the router is an object that figures out which route matches the current request.

In the Symfony world - well, really, in the object-oriented programming world - these "objects that do work" are given a special name: services. But don't let that confuse you: when you hear "service", just think:

Hey! That's an object that does some work - like a logger object or a database object that makes queries.

Listing All Services

Inside CommentController, let's log something. To do that work, we need the "logger" service. How can we get it?

Find your terminal and run:

php bin/console debug:autowiring

Say hello to one of the most important bin/console commands. This gives us a list of all the service objects in our app. Well, ok, this isn't all of them: but it is a full list of all the services that you are likely to need.

Even in our small app, there's a lot of stuff in here: there's something called Psr\Log\LoggerInterface, there's stuff for caching and plenty more. As we install more bundles, this list will grow. More services, means more tools.

To find which service allows us to "log" things, run:

php bin/console debug:autowiring log

This returns a bunch of things... but ignore all of these down here for now and focus on the top line. This tells us that there is a logger service object and its class implements some Psr\Log\LoggerInterface. Why is that important? Because if you want the logger service, you ask for it by using this type-hint. It's called "autowiring".

Autowiring the Logger Service

Here's how you get a service from inside a controller. Add a third argument to your method - though the argument order doesn't matter. Say LoggerInterface - auto-complete the one from Psr\Log\LoggerInterface - and $logger.

... lines 1 - 4
use Psr\Log\LoggerInterface;
... lines 6 - 9
class CommentController extends AbstractController
{
... lines 12 - 14
public function commentVote($id, $direction, LoggerInterface $logger)
{
... lines 17 - 28
}
}

This added a use statement above the class for Psr\Log\LoggerInterface, which matches the type-hint that debug:autowiring told us to use. Thanks to this type-hint, when Symfony renders our controller, it will know that we want the logger service to be passed to this argument.

So... yea: there are now two types of arguments that you can add to your controller method. First, you can have an argument whose name matches a wildcard in your route. And second, you can have an argument whose type-hint matches one of the class or interface names listed in debug:autowiring. CacheInterface is another type-hint we could use to get a caching service.

Using the Logger Service

So... let's use this object! What methods can we call on it? I have no idea! But because we properly type-hinted the argument, we can say $logger-> and PhpStorm tells us exactly what methods it has. Let's use $logger->info() to say "Voting up!". Copy that and say "Voting down!" on the else.

... lines 1 - 9
class CommentController extends AbstractController
{
... lines 12 - 14
public function commentVote($id, $direction, LoggerInterface $logger)
{
... lines 17 - 19
if ($direction === 'up') {
$logger->info('Voting up!');
... line 22
} else {
$logger->info('Voting down!');
... line 25
}
... lines 27 - 28
}
}

Testing time! Refresh the page and... let's click up, down, up. It... at least doesn't look broken.

Hover over the AJAX part of the web debug toolbar and open the profiler for one of these requests. The profiler has a "Logs" section , which is the easiest way to see the log entries for a single request. There it is! "Voting up!". You could also find this in the var/log/dev.log file.

The point is: Symfony has many, many useful objects, I mean "services". And little-by-little, we're going to start using more of them... each time by adding a type-hint to tell Symfony which service we need.

Autowiring & Using the Twig Service

Let's look at one other example. The first service that we used in our code was the Twig service. We used it... kind of "indirectly" by saying $this->render(). In reality, that method is a shortcut to use the Twig service behind the scenes. And that should not surprise you. Like I said, everything that's done in Symfony is actually done by a service.

As a challenge, let's pretend that the render() function doesn't exist. Gasp! In the homepage() controller, comment-out the render() line.

So... how can we use the Twig service directly to render a template? I don't know! We could definitely find some documentation about this... but let's see if we can figure it out by ourselves with the help of the debug:autowiring command:

php bin/console debug:autowiring twig

And, voilà! There is apparently a class called Twig\Environment that we can use as a "type-hint" to get a Twig service. In our controller, add Environment and hit tab to add the use statement on top. I'll call the argument $twigEnvironment.

... lines 1 - 7
use Twig\Environment;
... line 9
class QuestionController extends AbstractController
{
... lines 12 - 14
public function homepage(Environment $twigEnvironment)
{
... lines 17 - 21
//return $this->render('question/homepage.html.twig');
}
... lines 24 - 40
}

Inside, add $html = $twigEnvironment->. Once again, without reading any documentation, thanks to the fact that we're coding responsibly and using type-hints, PhpStorm shows us all the methods on this class. Hey! This render() method looks like it might be what we need! Pass the same template name as before.

... lines 1 - 9
class QuestionController extends AbstractController
{
... lines 12 - 14
public function homepage(Environment $twigEnvironment)
{
// fun example of using the Twig service directly!
$html = $twigEnvironment->render('question/homepage.html.twig');
... lines 19 - 21
//return $this->render('question/homepage.html.twig');
}
... lines 24 - 40
}

When you use twig directly, instead of returning a Response object, it returns a string with the HTML. No problem: finish with return new Response() - the one from HttpFoundation - and pass $html.

... lines 1 - 5
use Symfony\Component\HttpFoundation\Response;
... lines 7 - 9
class QuestionController extends AbstractController
{
... lines 12 - 14
public function homepage(Environment $twigEnvironment)
{
// fun example of using the Twig service directly!
$html = $twigEnvironment->render('question/homepage.html.twig');
return new Response($html);
//return $this->render('question/homepage.html.twig');
}
... lines 24 - 40
}

This is now doing the exact same thing as $this->render(). To prove it, click the homepage link. It still works.

Now in reality, other than being a "great exercise" to understand services, there's no reason to do this the long way. I just want you to understand that services are really the "things" doing the work behind the scenes. And if you want to do something - like log or render a template - what you really need is to find out which service does that work. Trust me, this is the key to unlocking your full potential in Symfony.

Let's put the old, shorter code back, and comment out the longer example.

... lines 1 - 9
class QuestionController extends AbstractController
{
... lines 12 - 14
public function homepage(Environment $twigEnvironment)
{
/*
// fun example of using the Twig service directly!
$html = $twigEnvironment->render('question/homepage.html.twig');
return new Response($html);
*/
return $this->render('question/homepage.html.twig');
}
... lines 26 - 42
}

Ok, you've almost made it through the first Symfony tutorial. You rock! As a reward, we're going to finish with something fun: an introduction into a system called Webpack Encore that will allow you to do crazy things with your CSS and JavaScript.

Leave a comment!

5
Login or Register to join the conversation
Default user avatar
Default user avatar Boran Alsaleh | posted 1 year ago

Hi when we send a request to our Controller for example index(LoggerInterface $logger) , does symfony create a new logger object at this moment and inject it to the controller or is this object already created before “e.g in kernel “and just we are using an existing object from symfony behind the seen could you please explain me that & how symfony is creating this obj inject it into our . Thanks in Advance

Reply

Hey Boran,

Good question! All the autowiring things are related to Symfony's Dependency Injection Container. That container knows everything about creating those objects, i.e. how to create that Logger object, etc. So the container is responsible for the creation of them. And yes, if we're talking about LoggerInterface - it will either created a new instance of it if it has not been created yet, or just will pass the instance that was already created before. For example, if you have an event listener that is called earlier than that controller, and that listener also requires the same LoggerInterface object - the same object will be passed to the controller. But if no code requires the logger before the controller - a new instance will be created and passed.

In shorts, learn more about Symfony's Dependency Injection and Dependency Injection Container to know more about how it works.

I hope this helps!

Cheers!

Reply
Peter K. Avatar
Peter K. Avatar Peter K. | posted 1 year ago

We had to inject loggerinterface but we are not injecting environment. Why is it coded this way? Both objects are services but one of them is not injected. Why accessing approach is not consistent? In other words why it is not designed that way that I can just type $this->info(). There seems to be more of them.

'router' => '?'.RouterInterface::class,
'request_stack' => '?'.RequestStack::class,
'http_kernel' => '?'.HttpKernelInterface::class,
'serializer' => '?'.SerializerInterface::class,
'session' => '?'.SessionInterface::class,
'security.authorization_checker' => '?'.AuthorizationCheckerInterface::class,
'twig' => '?'.Environment::class,
'doctrine' => '?'.ManagerRegistry::class,
'form.factory' => '?'.FormFactoryInterface::class,
'security.token_storage' => '?'.TokenStorageInterface::class,
'security.csrf.token_manager' => '?'.CsrfTokenManagerInterface::class,
'parameter_bag' => '?'.ContainerBagInterface::class,
'message_bus' => '?'.MessageBusInterface::class,
'messenger.default_bus' => '?'.MessageBusInterface::class,

Why one of them is not logger => '?'.LoggerInterface::class?

Reply

Hey Peter,

Those are just different approaches that allow you to achieve same results. Controllers are special, and allow you to use method injection instead of constructor injection - this gives you some more flexibility as on the request only those services will be booted (created) that are used directly in the specific controller's action. If you would use construction injection - most probably it would mean that you would use only some of those methods in a specific action, and some other in another specific action... but they all will be booted in the constructor no matter what action is hit. So in some cases it makes sense to use constructor injection when you want to share services across the whole class, and in some cases - use method injection to share some services across the specific method only, i.e. make them to be booted and available in a specific method only.

Anyway, you can choose whatever method you like more, or whatever you're comfortable with - we just try to show you all possible options you have in our tutorials ;)

Cheers!

Reply
Cat in space

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

This tutorial also works great for Symfony 6!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.3.0 || ^8.0.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "easycorp/easy-log-handler": "^1.0.7", // v1.0.9
        "sensio/framework-extra-bundle": "^6.0", // v6.2.1
        "symfony/asset": "5.0.*", // v5.0.11
        "symfony/console": "5.0.*", // v5.0.11
        "symfony/debug-bundle": "5.0.*", // v5.0.11
        "symfony/dotenv": "5.0.*", // v5.0.11
        "symfony/flex": "^1.3.1", // v1.17.5
        "symfony/framework-bundle": "5.0.*", // v5.0.11
        "symfony/monolog-bundle": "^3.0", // v3.5.0
        "symfony/profiler-pack": "*", // v1.0.5
        "symfony/routing": "5.1.*", // v5.1.11
        "symfony/twig-pack": "^1.0", // v1.0.1
        "symfony/var-dumper": "5.0.*", // v5.0.11
        "symfony/webpack-encore-bundle": "^1.7", // v1.8.0
        "symfony/yaml": "5.0.*" // v5.0.11
    },
    "require-dev": {
        "symfony/profiler-pack": "^1.0" // v1.0.5
    }
}