Skip to content

How-to guideFor module developers, theme developersDrupal 11

Replace deprecated and static API calls

Two kinds of call age a module. The first is the \Drupal:: static accessor used inside a class, where a constructor-injected service belongs instead. The second is a function or method that core has deprecated and will remove in a future major version. Both keep working today, which is exactly why they are easy to leave in place until the version that drops them breaks your site. This page finds and replaces both.

Class code that reaches services through injected properties rather than \Drupal::service(), and a module with no calls to APIs scheduled for removal in the Drupal version you target. The behavior does not change; what changes is that the code is testable, and it will survive the next major release.

Replace static service calls with injection

Section titled “Replace static service calls with injection”
  1. Find the static calls inside your classes: grep -rn "\\\\Drupal::" web/modules/custom/my_module/src. Each hit in a service, controller, form, plugin, or hook class is a candidate. Entity classes and static methods (such as baseFieldDefinitions() and create() factories) are expected exceptions: the container does not build entities, and a static method has no instance to inject into. The same call in a .module, .install, .theme, or theme-settings.php file is not: those are procedural files where injection does not exist, and the static accessor is the correct form there.

  2. Inject the service through the constructor instead, and use the property. The pattern depends on how the class is built:

    • A service (defined in *.services.yml) lists the dependency as a constructor argument, or relies on autowiring by type.
    • A hook class under src/Hook/ is autowired automatically: just type-hint the constructor.
    • A controller, form, block, or other plugin receives services through a create() factory method (from ContainerInjectionInterface or ContainerFactoryPluginInterface), which hands them to the constructor.

    The body change is the same in every case. Before, a static call buried in a method:

    public function build(): array {
    $count = \Drupal::entityTypeManager()
    ->getStorage('node')
    ->getQuery()
    ->accessCheck()
    ->count()
    ->execute();
    // ...
    }

    After, the same manager injected once and reused:

    public function __construct(
    private readonly EntityTypeManagerInterface $entityTypeManager,
    ) {}
    public function build(): array {
    $count = $this->entityTypeManager
    ->getStorage('node')
    ->getQuery()
    ->accessCheck()
    ->count()
    ->execute();
    // ...
    }

    Services and dependency injection shows the full wiring for each class type, including the create() factory.

  3. Rebuild so the container picks up changed constructors or service definitions: ddev drush cache:rebuild.

  1. Run static analysis that knows Drupal’s deprecations. PHPStan with the phpstan-drupal and deprecation-rules extensions (both pulled in by drupal/core-dev) reports every call to an API core has marked @deprecated, with the version it is removed in and the replacement to use. Point it at your module:

    Terminal window
    ddev exec vendor/bin/phpstan analyse web/modules/custom/my_module
  2. Read each report against its change record. A deprecation notice names its replacement; the linked change record on drupal.org explains the move. For example, reading a theme setting used to mean the global theme_get_setting() function, now replaced by a service:

  3. Apply the replacements and rerun the analysis until it reports no deprecations for the Drupal version you target. Commit the analysis command to your CI so a future core update cannot reintroduce one unnoticed.

  • grep -rn "\\\\Drupal::" web/modules/custom/my_module/src returns hits only in entity classes and static methods.
  • ddev exec vendor/bin/phpstan analyse web/modules/custom/my_module reports no deprecation errors.
  • The module behaves exactly as before: the page, block, or form you changed still renders and saves.

Procedural code has no container to inject from, so \Drupal::service() stays the right form in .install update functions, .theme preprocess functions, theme-settings.php, Drush scripts, and one-off drush php:eval commands. Converting those to injection is not possible and not the goal. The rule is narrow: static accessors inside a container-built class are the thing to replace.

If you do not want the whole of drupal/core-dev in a project, ddev composer require --dev mglaman/phpstan-drupal phpstan/phpstan-deprecation-rules phpstan/extension-installer installs the same deprecation checks on their own. For a report in the admin UI, install the Upgrade Status module, which runs the same analysis per project.

drupal-rector rewrites many deprecated calls for you, using the same deprecation data. Run it, review the diff, then rerun PHPStan to catch what it left.

Was this page helpful?

Drupal is a registered trademark of Dries Buytaert.