This course is archived!
This tutorial is built using Drupal 8.0. The fundamental concepts of Drupal 8 - like services & routing - are still valid, but newer versions of Drupal *do* have major differences.
10.
How to Get a Service in the Controller
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.
21 Comments
It's good to know that you can also use the "controller_service_name:action" in your routing.yml where "controller_service_name" is a service defined in your service.yml. This way you don't have to implement the create() method anymore.
Hey Robert,
Yeah, `controller_service_name:action` notation is a little-known fact. Thanks for sharing it!
Cheers!
I've also just added it to the Drupal 8 docs on routing so it should be easier to find from now on. I would suggest also mentioning it in the code examples here as the create() method seems really crazy to me as I don't want any binding with my framework but the create() does require the container itself.
Hey Robert!
I agree! I think we should at least add a note about this. Is your change to the Drupal 8 docs already live? It would be a great place to link to in that note.
Cheers!
You can find it at https://www.drupal.org/docs... under "_controller".
Personally I even think the article should be rewritten to show this as the primary method and then show the create() as an alternative method but that's up to you of course :)
Note added! Thank you! https://knpuniversity.com/s...
Drupal ControllerBase is no longer included in Drupal 9.2.6. I'm using PHPStorm and seems like they have a few controllers. Ahhh it's ContainerInfoController. Are we getting any updates on the Drupal tutorials or is it too niche?
Hey Brian,
Thank you for sharing this with others! Unfortunately, this course is based on Drupal 8, so Drupal 9 may have some BC breaks like you mentioned. We will think if we could add some notes about changes in Drupal 9 here, or if it would require a separate tutorial. Unfortunately, no plans for a new course about Drupal 9 in the nearest future.
Cheers!
When a service is registered with controller , all of its instances are stored in the same memory locations , then how can I assign unique values to each of the instances?
Hey @Akshit!
Yes, you're correct: this is a key feature of a "service" in the container: one specific "service" will always be the same object in memory, no matter where you try to access it from. So, if you fetch the
dino_roar.roar_generatorservice form 10 different places, you will always get the same object in memory.Of course, you already understand this. So, to your question:
Typically, a service should hold "data" - it shouldn't be stateful. There are really (in a well designed application, though there certainly are exceptions) 2 types of classes:
A) Stateful "model" classes: classes that don't do a lot of work... but that just mostly hold data. A good example would be a Dinosaur class. A Dinosaur class would probably mostly hold data - e.g.
$dino->setHeight()or$dino->setType(), but it probably wouldn't do a lot of real "work" - like$dino->sendEmailToAllDinosYouAte(). One property of this type of class (versus the other) is that it makes sense to have multiple instance of this class: it makes sense to have 10 Dinosaur objects at once... if you are dealing with 10 dinosaurs.B) Service classes: classes that don't hold a lot of data, but do work. RoarGenerator is a perfect example... as well as anything in the container. One property of these is that you only need one instance of them at any time. For example, if you want to roar 10 times, you don't need 10 RoarGenerator: you just need 1 RoarGenerator and you call ->roar() on it 10 times.
Now, some caveats :).
1) "Service classes don't hold data". This is true... but they can hold configuration: internal values that control how they do their work. For example, it would be perfectly ok to be able to control the RoarGenerator's volume, perhaps via a
$roarGenerator->setVolume()method.2) "One property of service classes is that you only need one instance of them at any time". This is true again... except that it IS ok to have multiple instances of the same service if each of them has different configuration. For example, imagine that you have a constructor argument to RoarGenerator called
$volume. You might then choose to register 2 services in the container for this one class:dino_roar.roar_generator_loud(where you pass 10 for the volume) anddino_roar.roar_generator_quite(where you pass 3 for the volume)And... sorry for the LONG-winded answer. Let me know if it helps clarify.
Cheers!
Adding the logger gives me this error:
TypeError: Argument 2 passed to Drupal\dino_roar\Controller\RoarController::__construct() must be an instance of Drupal\dino_roar\Controller\LoggerChannelFactoryInterface, instance of Drupal\Core\Logger\LoggerChannelFactory given, called in /home/bscdev/new_html/web/modules/custom/dino_roar/src/Controller/RoarController.php on line 35 in Drupal\dino_roar\Controller\RoarController->__construct() (line 16 of modules/custom/dino_roar/src/Controller/RoarController.php).It seems to want <i>Drupal\dino_roar\Controller\LoggerChannelFactoryInterface</i>
But I seem to be giving it <i>Drupal\Core\Logger\LoggerChannelFactory</i> and I'm not sure why.
Hey Sean C.
Looks like you miss
usestatement forLoggerChannelFactoryInterface. I updated code block on this page, so check this out, there is correct use statement ate the top.TIP. You can always check complete file code with show all lines button on the code block header, and of course write a comment here if we are missing something ;)
Cheers!
Thanks Vladimir! That was, indeed, the thing I was missing.
Should mention how the Create method is invoked??? It isn't a magic method, so... who called it?
Hey Thomas W.!
Great question :). This is a total "Drupalism". Basically, when the controller system was added to Drupal, here was the thought process:
A) By default, Drupal will instantiate the controller class automatically. But, when it does this, no constructor arguments will be passed to the constructor... because how would Drupal know what to pass you?
B) Having the controller class instantiated automatically with no constructor args is fine for many people. But to allow people to pass constructor args who want to, Drupal needed to "invent" some way of doing this. The way they decided to do it was: (A) check if the controller class implements a
ContainerInjectionInterface(whichControllerBasedoes) - this interface requires thatcreate()method. And (B) if the controller class does implement it, call that method to instantiate the controller object instead of instantiating it manually.A better explanation might be in code :). Here is some "fake code" that is more-or-less what Drupal does in its core:
I hope that helps. This is total, dark, hidden logic, which is why I love to explain it. You're 100% correct to wonder "who calls this method?"
Cheers!
Great explanation. Thanks! and nice "fake code" too.
I saw an issue that they were going to add this, I'm glad they did. It was actually added to 8.1, not 8.0 (it looks like they added it to 8.0 for a moment, then realized that adding a new feature breaks semver - great decision).
Anyways, for others reading this, yes! In 8.1, you'll be able to use $this->getLogger('main') to get the "main" channel logger from a controller that extends ControllerBase.
Thanks for the note!
Why make a static create() method? In other tutorials, lines like...
$roarGenerator = $container->get('dino_roar.roar_generator');
... would be in the roar() method of RoarController.
I'm guessing it has to do with testing? create() centralizes instantiation of services used in all the methods of RoarController (assuming programmers follow the standard of using create()). Makes it easier to identify the stub services (stub isn't the right name, forget the correct name) needed for testing. Just guessing, since I don't know what I'm talking about.
BTW, from a learning design PoV, I like the fact that you address emotional issues in this series, such as the noob fear induced by terms like dependency injection container. Can be tricky, though. It's easy for students to infer that anyone using the term dependency injection container is intentionally begin snooty. The inference will often be incorrect; that's just the way devs talk to each other. The inference can complicate the social dynamics of a dev team.
Kieran
Hey Kieran!
The static create() thing is tricky, and it's a total Drupal-ism - it's how they chose to give you access to the Container in controllers. I actually find it fairly difficult to explain - it's a very odd - and edge-case version of dependency injection :). But, that makes it all the more interesting!
But, first, you probably don't exactly see $container->get('...') in the roar() method in other tutorials, because - in Drupal 8 - you don't have access to the $container object in a random controller function. This is a "problem" in general with OO code - unless you "cheat" and use global/static variables, you simply don't have access to an object from within another object unless it is somehow passed to you as an argument (either as an argument to your controller or an argument to the __construct() function of your object).
The most important thing for testable and predictable code (because using global variables are very not predictable) is that you don't use global/static variables. If we take that as a truth ("I will not use static/globals"), everything that follows is a collection of strategies to get access to the objects/configuration that we need. *Usually*, we choose to pass our "dependencies" through the __construct() method. But technically speaking, that is just one strategy.
Most of the time in Symfony/Drupal, *we* are in control of *how* are objects are instantiated. For example, if you have a RoarGenerator class (like the one in this tutorial), we can control what arguments are passed to its __construct() method via the YAML file. This allows us to not use globals/static, but still get access to whatever we need (yay!).
But controllers are a special case for DI, because it is the *one* place in Drupal or Symfony where you do *not* control *how* your object is instantiated (the reasons for why that is true are not too important). Somewhere deep and dark, Drupal/Symfony creates your Controller object for you, and it passes *no* arguments to __construct(). Oh no! This puts us in a bad place: how can we get access to outside objects if we cannot control the arguments to our __construct() method? Basically, we can't! So, Drupal added a second option. They said, if your Controller class has a create() method, then instead of creating your Controller object with new __construct args (i.e. new RoarController()) - it will call the create() method, pass you the Container, and allow *you* to choose your __construct arguments. In short, this is a one-off strategy for allowing you to control how your Controller is instantiated. All other objects are controlled in this way via the YAML file.
Phew! Now, I don't know if that helped explain things or confused further :). And thanks for the note about the learning design - your point is well-understood. I like to downplay very technical terms to help shake the fear that sometimes becomes attached to ideas that are fundamentally *not* complex. However, I see your point :).
Cheers!
hi Ryan (again.. not stalking honest).
is there a reason why we shouldnt use?
\Drupal::getContainer()->get('xxx');
seems a lot simpler than using create?
Yo Matt!
I don't think I talk about it here enough (or at all), but my opinion is almost always that you should feel ok avoiding create() (or create-like functions, there are a few other times when Drupal has a thing like this) and use \Drupal::getContainer(). BUT, the key is: *only* do this when you're in these classes that force this pattern on you - e.g. controllers. AND, of course, ideally, put as much logic into services, so that your controllers stay very "skinny". For me, places like your controller *should* have access to the container - I was a bit surprised when Drupal decided to make developers jump through this hoop.
So, tl;dr YES - but only use \Drupal when you're in a Controller (or the other few spots with this pattern).
Cheers!
"Houston: no signs of life"
Start the conversation!