Score:1

How to use dependency injection to load node data?

kz flag

In a basic Controller class, I tried to load node data using the code from this accepted answer. I.e:

$nid = 1;
$node_storage = $this->entityTypeManager()->getStorage('node');
$node = $node_storage->load($nid);

However, this answer does not have sufficient detail to help me. Just putting it inside my very simple controller class, I get a WSOD with this error:

Error: Call to undefined method Drupal\mymodule\Controller\MyModuleController::entityTypeManager() in …

Following the suggestions below the answer by @apaderno, I understood that I I need to declare ControllerBase as the parent of my Controller class.

It now looks like this:

namespace Drupal\mymodule\Controller;

use Drupal\Core\Controller\ControllerBase;

class MyController extends ControllerBase {

  public function mymodule() {
      $nid = 1;
      $node_storage = $this->entityTypeManager()->getStorage('node');
      $entity = $node_storage->load($nid);
      dpm($entity, 'entity');
  }
}

This is still not a working controller. It still produces a WSOD with this error:

Symfony\Component\HttpKernel\Exception\ControllerDoesNotReturnResponseException: The controller must return a "Symfony\Component\HttpFoundation\Response" object but it returned null. Did you forget to add a return statement somewhere in your controller?

But the entity now gets loaded , and the exception is something I know how to fix.

I guess my problem is that I am a fairly inexperienced Drupal 10 developer, and don't know all the things about the Drupal 10 programming environment that it seems that most other Drupal developers take for granted that one already knows.

leymannx avatar
ne flag
Does this answer your question? [Dependency injection to load node data](https://drupal.stackexchange.com/questions/232906/dependency-injection-to-load-node-data)
Free Radical avatar
kz flag
@leymannx No it does not. It is linked on the first line on my question, and while I try to use exactly that code, there is too little detail to help me. I'll try to expand the question to make the problem clearer.
id flag
It is possible the controller class file is in the wrong place or has the wrong name.
apaderno avatar
us flag
@cilefen It could be, but that should be a change that happened after adding `ControllerBase` as parent class, or Drupal could not first complain that `Drupal\mymodule\Controller\MyModuleController::entityTypeManager()` is an undefined method (which means Drupal found the `Drupal\mymodule\Controller\MyModuleController` class).
Kevin avatar
in flag
Controllers have to return a Response object or a render array. You need to use xdebug if you want to step through and debug during development. Devel won't cut it here. You're getting that error because dpm doesn't halt execution for code inspection like xdebug would.
Kevin avatar
in flag
https://www.amazon.com/Drupal-Development-Cookbook-experiences-applications/dp/1803234962/ref=nodl_?dplnkId=4daaae26-b098-4a19-acd6-db2dfbbabeef
Score:2
us flag

entityTypeManager() is a method defined from the ControllerBase class, even in Drupal 10. ControllerBase::$entityTypeManager is one of its properties, but I would use ControllerBase::entityTypeManager() because it initializes that property, if it was not already initialized.

protected function entityTypeManager() {
  if (!isset($this->entityTypeManager)) {
    $this->entityTypeManager = $this->container()->get('entity_type.manager');
  }
  return $this->entityTypeManager;
}

If that is not one of the parent classes for the controller, I would do as the documentation for that class says (if the controller class does not contain trivial code) and use the ContainerInjectionInterface to implement the create() method necessary to inject the required dependencies.

For reference, this is the suggestion given in the ControllerBase documentation. (Emphasis is mine.)

Controllers that use this base class have access to a number of utility methods and to the Container, which can greatly reduce boilerplate dependency handling code. However, it also makes the class considerably more difficult to unit test. Therefore this base class should only be used by controller classes that contain only trivial glue code. Controllers that contain sufficiently complex logic that it's worth testing should not use this base class but use ContainerInjectionInterface instead, or even better be refactored to be trivial glue code.

If you want to have available a entityTypeManager() method without extending that class, I would use code similar to the following one.

use Drupal\Core\Entity\EntityTypeManager;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\ContainerInterface;

class Controller implements ContainerInjectionInterface {

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManager
   */
  protected $entityTypeManager;

  /**
   * Constructs a new Controller object.
   *
   * @param \Drupal\Core\Entity\EntityTypeManager $entityTypeManager
   *   The entity type manager.
   */
  public function __construct(EntityTypeManager $entityTypeManager) {
    $this->entityTypeManager = $entityTypeManager;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static($container->get('entity_type.manager');
  }

  /**
   * Retrieves the entity type manager.
   *
   * @return \Drupal\Core\Entity\EntityTypeManager
   *   The entity type manager.
   */
  public function entityTypeManager() {
    return $this->entityTypeManager;
  }

}

Otherwise, to simply use the ControllerBase class, I would take the BookController code as example.

use Drupal\book\BookExport;
use Drupal\book\BookManagerInterface;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Link;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Url;
use Drupal\node\NodeInterface;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

/**
 * Controller routines for book routes.
 */
class BookController extends ControllerBase {

  /**
   * Constructs a BookController object.
   *
   * @param \Drupal\book\BookManagerInterface $bookManager
   *   The book manager.
   * @param \Drupal\book\BookExport $bookExport
   *   The book export service.
   * @param \Drupal\Core\Render\RendererInterface $renderer
   *   The renderer.
   */
  public function __construct(BookManagerInterface $bookManager, BookExport $bookExport, RendererInterface $renderer) {
    $this->bookManager = $bookManager;
    $this->bookExport = $bookExport;
    $this->renderer = $renderer;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('book.manager'),
      $container->get('book.export'),
      $container->get('renderer'));
  }

  /**
   * Prints a listing of all books.
   *
   * @return array
   *   A render array representing the listing of all books content.
   */
  public function bookRender() {
    $book_list = [];
    foreach ($this->bookManager->getAllBooks() as $book) {
      $book_list[] = Link::fromTextAndUrl($book['title'], $book['url']);
    }
    return [
      '#theme' => 'item_list',
      '#items' => $book_list,
      '#cache' => [
        'tags' => $this->entityTypeManager()->getDefinition('node')->getListCacheTags(),
      ],
    ];
  }

}

BookController adds its own dependencies, but it can still use $this->entityTypeManager() to get the entity type manager.

As for the exception thrown in line 135, this is the code that causes that exception.

$entity_types = $this->getEntityTypes();
$parameter_definitions = $route->getOption('parameters') ?: [];

$result = FALSE;

if (is_array($controller)) {
  [$instance, $method] = $controller;
  $reflection = new \ReflectionMethod($instance, $method); // Line 135
}
else {
  $reflection = new \ReflectionFunction($controller);
}

For some reason, PHP does not found the \Drupal\mymodule\Controller\MyController class, for example because the class namespace is wrong. (There could be other reasons for which PHP autoload cannot load that class, but I could not recall them at the moment.)

Free Radical avatar
kz flag
Thanks for answering! My controller do not extend *any* parent classes. I would appreciate it if you could expand your answer a bit to spell out how to use `ContainerInjectionInterface` to get access to the method in my class.
apaderno avatar
us flag
If you want to use a `entityTypeManager()` method without using the `ControllerBase` class, I can show the code I would use, yes.
Free Radical avatar
kz flag
Thanks for the help. I want to have access to the `entityTypeManager()` method inside my Controller. I am too inexperienced to have an opinion about whether to do it with or without the `ControllerBase` class. I only want to see some code that implements the recommended way to do this.
leymannx avatar
ne flag
@FreeRadical In controllers `$this->entityTypeManager` is already there. Simply scan the `core/` directory for living examples.
apaderno avatar
us flag
@leymannx Yes. There are also controllers that inject their own dependencies and still use `$this->entityTypeManager()`, like `BookController`.
Free Radical avatar
kz flag
@leymannx Thank you for helping out!. It is not trivial to copy code snippets from core. I am looking at "NodeController.php", but when I try to copy what I think are the relevant bits into MyController, I get "ReflectionException: Class "\Drupal\mymodule\Controller\MyModuleController" does not exist in ReflectionMethod->__construct()".
apaderno avatar
us flag
Clearly, controllers can use `entityTypeManager()` if `ControllerBase` is one of the parent classes. Controllers are not required to have a parent class or implement a specific interface, so `ControllerBase` needs to be set as parent class, to get that method.
apaderno avatar
us flag
@FreeRadical The relevant parts are the `extends ControllerBase` part in the class definition and the `use Drupal\Core\Controller\ControllerBase;` line. That is sufficient for your class to use `$this->entityTypeManager()`. For the rest, your class can implement `create()` if more dependency are necessary, but that is "optional."
apaderno avatar
us flag
@FreeRadical The `use` line is simply `use Drupal\Core\Controller\ControllerBase;`. There is just a `ControllerBase` class implemented by Drupal core. (Yes, I copied the wrong line, in my previous comment. `use Drupal\Core\Entity\EntityTypeManager;` is not necessary, to use `$this->entityTypeManager()`.)
Jaypan avatar
de flag
I answered this question on DO: https://www.drupal.org/forum/support/module-development-and-code-questions/2023-05-20/error-using-dependency-injection#comment-15059307
apaderno avatar
us flag
Between the *Call to undefined method Drupal\mymodule\Controller\MyModuleController::entityTypeManager()* error and the *Class "\Drupal\mymodule\Controller\MyController" does not exist in ReflectionMethod->__construct()* error, something else is changed. Adding a parent class does not cause the second error. I assume the module has been uninstalled before changing its code (which is should be done, when testing a module code).
Free Radical avatar
kz flag
@apaderno No, the module has so far not been uninstalled and reinstalled. As I said: I am not an experienced developer. Thanks for providing this extra information! When doing it I find that there is another bug in my class (i.e. not related to this question). After fixing this bug, uninstalling and reinstalling, it works.
I sit in a Tesla and translated this thread with Ai:

mangohost

Post an answer

Most people don’t grasp that asking a lot of questions unlocks learning and improves interpersonal bonding. In Alison’s studies, for example, though people could accurately recall how many questions had been asked in their conversations, they didn’t intuit the link between questions and liking. Across four studies, in which participants were engaged in conversations themselves or read transcripts of others’ conversations, people tended not to realize that question asking would influence—or had influenced—the level of amity between the conversationalists.