I am working from this tutorial and I want to load a node entity in a Controller class.
The following construct (copied almost verbatim):
$entity = \Drupal::entityTypeManager()->getStorage('node')->load(1);
works as expected.
The same tutorial says:
Avoid using the static
Entity::load()method in object oriented code. Instead use dependency injection to inject the entity type manager and load the entity with$this->entityTypeManager->getStorage($entity_type)->load($entity_id). This ensures that the code is properly decoupled and can be unit tested.
Based on that, I tried the following variant (i.e. without parenthesises after entityTypeManager), which is an almost verbatim copy of what is suggested:
$entity = $this->entityTypeManager->getStorage('node')->load(1);However, this produces the following error:
Error: Call to a member function getStorage() on null in Drupal\mymodule\Controller\MyModuleController->mymethod() (…).
Edit: After reading the answer by apaderno, I've changed my Controller class to look like this:
use Drupal\Core\Controller\ControllerBase;
class MyController extends ControllerBase {
public function mymodule() {
$entity = $this->entityTypeManager()->getStorage('node')->load(1);
}
}This works!
Edit 2: However. I was too rash in assuming that the line I first copied from the tutorial was wrong. The comment by Berdir (posted 21 May 2023) sets the record straight.
Calling the method, using entityTypeManager() is not really using DI. It's a handy shorthand because ControllerBase makes this method available to thin Controllers without actually using DI. To have DI, one needs to go through the steps outlined by Jaypan.
DI is a lot more complex than I first thought, and the comments below from both Jaypan and apaderno has been very illuminating for me.
Comments
You didn't show where you are
You didn't show where you are using this code. There are two possibilities - you're using it statically, in a hook or a static class method, or you're using it in a non-static OOP method.
If you are in a hook or a static method,
$thisdoes not exist, so you will use:However if you are working from within a non-static method of a class,
$thiswill be available, and you will need dependency injection.Dependency Injection
Any function declared in a class without the
statickeyword will have access to the$thisobject, and can use dependency injection. Setting up dependency injection requires three things:$this1) Declare the dependency
Declaring dependencies is done in one of two ways, either in the
[MODULE].services.ymlfile, for classes that are services, or in thecreate()method, for classes that are not services. In this example, the dependency to be injected is theentity_type.managerservice, and one of the following methods will be used:1A) Declaring a dependency for a service:
This is done in the
argumentsof the[MODULE].services.ymlfile:The Drupal registry needs to be cleared after declaring services, as they are cached and will only be picked up after a cache clear.
1B) Declaring a dependency for a class that is not a service
This is done in the
create()method of the class:2) Create a property to hold the injected dependency
Next, a property is declared that will store the injected dependency:
This sets up
$this->entityTypeManagerPropertyfor the class. However, at this point, the value of the property isNULL, as it has only been declared, with no value yet assigned to it. That will happen in step 3.As an aside, I would usually call the property
$entityTypeManager, however for the purposes of clarity for this tutorial, I am using$entityTypeManagerPropertyto visibly differentiate from the injected dependency which I will call$entityTypeManagerInjectedin the next step. In a normal class, I would name both the property and the injected dependency$entityTypeManager. This makes for clarity and consistency day-to-day coding, but makes it harder to differentiate what the differences are when learning, which is why I've used these names in this example.3) Store the injected dependency in the class property when the class is loaded
Finally, the injected dependency is set as the value of the property, when the class is loaded. This is done in the
__construct()method of the class, which is a "magic" method called every time this class is instantiated into memory (aka NOT called statically);Using the injected dependency
Now that the dependency has been injected
$this->entityTypeManagerPropertycontains the injected dependency, and can be used in any non-static method in the class as follows:$thisContact me to contract me for D7 -> D10/11 migrations.
I've added the context
Sorry about not providing context about where I am using this code. It is used in a very simple Controller class (i.e. inside a non-static method inside a class). I have expanded the OP to show what this class looks like.
- gisle
---
The Call to a member function
getStorage()onnullin Drupal\mymodule\Controller\MyModuleController->mymethod() error is correct, since the code is using a property ($this->entityTypeManager) that has not been yet set.ControllerBase::entityTypeManager()setsControllerBase::$entityTypeManagerto the correct value.The correct way to get the entity type manager is calling the method, not accessing the property.
For procedural code, or for static methods, the following line loads the node whose ID is 1.
For a non-static method implemented in a class that extends
ControllerBase, the line to use is the following one.Yes, this works!
However, it looks like the tutorial I've been following is wrong on how to get theentityTypeManagerinjected.Edit: I was rash to say that the tutorial was wrong – striking out that misinformation from this comment.
I agree with what apaderno says in the comment below, that the
- gisle
---
Either that documentation page does not give details sufficient to understand the example code, or the example code is not much detailed.
$this->entityTypeManager->getStorage($entity_type)->create()would create problems whenever that line is used in a class that extendsControllerBaseor in a class that does not have parent classes, if that class does not initialize$this->entityTypeManagercorrectly. In the first case, PHP would complain about calling a method for a variable that containsNULL; in the second case, PHP would probably complain about a not existing property.Your post (@Gisle) and the
Your post (@Gisle) and the tutorial and comments in this thread are very timely, at least for me. I'm a "trainee" maintainer for the contrib Juicebox module (and believe me I would happily leave the work to more experienced Drupal programmers if such could be found) and have been beating my head about one remaining PHPCS warning re. Dependency Injection. I've been trying to model my solution around other contrib modules that avoid the dependency injection warning as well as one of the core modules that uses the same service I'm trying to use but avoids the warnings. The thread where this is discusssed is:
https://www.drupal.org/project/juicebox/issues/3359786
I'm burned out from hacking at this for an hour this morning but will return to this Forum thread to see if there is a solution in it for my situation. Anyone who has the patience to contribute insights to the thread linked above is welcome to do so meanwhile.
There was also a long Forum thread over in Slack re. dependency injection that didn't yield any obvious solutions to my problem.
I'm sure that using dependency injection is the preferred technical approach but it sure seems a long way around just including some code that would generate an "absolute string" version of a url. That said, it has to be done, I know.
I'm sure that using
It actually doesn't "have" to be done, in that you can call the services statically. But using dependency injection accomplishes three tasks:
1) Follows a proper OOP paradigm
2) Prevents dependency loops
3) Allows for unit testing of code
Dependency injection is 'cleaner', but not necessarily required.
Contact me to contract me for D7 -> D10/11 migrations.
Yes, the current non
Yes, the current non-dependency injection code works just fine. It just generates a PHPCS warning that I would like to eliminate going forward. Personally, I would also like to improve my skills so I can do it the right way.
But coming into the code for the Juicebox module "cold" and not knowing all the ins and outs of Drupal it's hard to know:
- which use statements to use ... various modules that "use" the file_url_generator service seem to use different combinations of use statements
- how to set up the construct and other statements that seem to be needed PRIOR to actually injecting the service.
I think I outlined those
I think I outlined those things in my first post in this thread.
Contact me to contract me for D7 -> D10/11 migrations.
Your first post was about the
Your first post was about the clearest thing I've seen on dependency injection. It will take me a few times through and some practice to digest it. I'm also looking at:
https://symfony.com/doc/current/service_container.html
and subsequent articles in that documentation.
I don't fully understand why programs such as:
core/modules/image/src/Entity/ImageStyle.php
don't have dependency injection problems when the JuiceboxFormatter.php program does. That just means I have to work though the code and documentation step by step a few more times.
If someone can point out exactly where and how ImageStyle.php makes it possible for lines 252 and 253:
/** @var \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator */
$file_url_generator = \Drupal::service('file_url_generator');
to inject the file_url_generator service properly maybe I can copy that approach. And learn from it.
Thanks
Just in case this helps
Just in case this helps someone else.
In the contrib Juicebox module I am working on a couple of community members and the senior maintainer have more or less resolved the dependency injection problems. The solution involved modifying the module's services.yml file. This in turn involved modifying the arguments in the module services file. After completing this, the maintainer wrote a
juicebox_post_update_dependency_injection_cache_clear()program to be invoked when Juicebox was updated via composer. Fine except if you are developing and updating your code manually via, say PHPstorm and running something like Wampserver.
After much gnashing of teeth, what I found out was that Drupal caches the a services (arguments) in a table named cache_container. A cache clear from within Drupal nor a Drush cr doesn't clean out this table. So your construct statement is likely to fail by referencing an obsolete service (argument).
In the bowels of the Internet I found a solution:
from drush run drush ev "drupal_flush_all_caches();"
then do a normal drush cr.
normal cache clears, flushing browser cache and even a standalone drush cr are not sufficient.
I was in a situation where the code worked fine on my hosted production server which I had updated by the "normal" composer require route but not on my local Wampserver where I had just updated the code directly in PHPstorm. Same code: different results because of caching.