# Dependency injection

Checks for classes that call `\Drupal` where they can take a service through dependency injection, for entity storages that a class takes or keeps instead of asking the entity type manager, and for the properties that `DependencySerializationTrait` can't restore.

---

## drupal/global-drupal-call

- **Level:** warning
- **Source:** phpstan-drupal
- **Ports:** phpstan-drupal's `GlobalDrupalDependencyInjectionRule` (partly)

phpstan-drupal reports a `\Drupal::` call in a class that can take its services through dependency injection. Such a class gets a service through its constructor, or through `create()` for a form, a controller or a plugin.

**Reports:**

- a call to a method of `\Drupal` in an instance method of a class that implements `ContainerInjectionInterface` or `ContainerFactoryPluginInterface`. Examples are `\Drupal::service()` and `\Drupal::state()`;
- the same call in an entity handler (`EntityHandlerInterface`) or a container deriver (`ContainerDeriverInterface`), whose static factory gets the container;
- the same call in a class that a services file registers as a service, by its `class:` key or by an id that names the class. The check reads the [services files](<https://project.pages.drupalcode.org/mago/analyzer/services/index.md#the-service-index>) named after core, a module or a profile, so a class registered only in `drush.services.yml` isn't reported;
- the same call in an instance method of a hook class, which is a class with a `#[Hook]` attribute on the class or on a method that it declares. Drupal registers a hook class as an autowired service;
- the same call in a `BlockBase` plugin, which can take services once it implements `ContainerFactoryPluginInterface` and adds `create()`;
- a call on a subclass or an instance of `Drupal` in those methods.

**Not reported:**

- a static method, which doesn't have injected services to use;
- a constructor with a parameter that accepts null. Drupal's deprecation policy adds a new service to a constructor as a nullable parameter, and the constructor falls back to `\Drupal::service()` for callers that don't pass it yet;
- a call in a [deprecation scope](<https://project.pages.drupalcode.org/mago/analyzer/deprecations/index.md#deprecation-scopes>), such as deprecated code and the callables passed to `DeprecationHelper::backwardsCompatibleCall()`. A deprecated method keeps its lookup until it is removed, and a callable for newer core can use a service that doesn't exist in older core;
- a call inside an anonymous class;
- any other class, such as a plain object built with `new`;
- test code (any file under a `tests` directory).

> [!NOTE]
>
> **Example**
>
> modules/example/src/Hook/ExampleHooks.php
>
> ```
> // Reported.
> namespace Drupal\example\Hook;
>
> use Drupal\Core\Hook\Attribute\Hook;
>
> class ExampleHooks {
>
>   #[Hook('cron')]
>   public function cron(): void {
>     \Drupal::state()->set('example.last_cron', time());
>   }
>
> }
>
> // Fine.
> namespace Drupal\example\Hook;
>
> use Drupal\Core\Hook\Attribute\Hook;
> use Drupal\Core\State\StateInterface;
>
> class ExampleHooks {
>
>   public function __construct(
>     protected StateInterface $state,
>   ) {}
>
>   #[Hook('cron')]
>   public function cron(): void {
>     $this->state->set('example.last_cron', time());
>   }
>
> }
> ```

> [!NOTE]
>
> **Compared with phpstan-drupal**
>
> - phpstan-drupal checks every class except tests, entities, typed data, stream wrappers, Nightwatch test setup classes, enums and traits. It also reports the plugins and other classes left alone by the check.
> - A service called on purpose through `\Drupal`, such as a lazy lookup that breaks a circular dependency, is reported by both tools. Mark it with `@mago-expect analysis:drupal/global-drupal-call`.
> - phpstan-drupal also reports the fallback in a constructor with a nullable parameter, a call in deprecated code or in a callable passed to `DeprecationHelper::backwardsCompatibleCall()`, and a call in an anonymous class.
> - phpstan-drupal checks a static call on `\Drupal` itself. The check also takes a call on a subclass of `Drupal`.
> - phpstan-drupal skips a class that implements PHPUnit's `Test`, and the check skips any file under a `tests` directory.

---

## drupal/static-entity-load

- **Level:** warning
- **Source:** Coder's `DrupalPractice` standard
- **Ports:** `DrupalPractice.Objects.GlobalClass.GlobalClass` (partly)

Coder reports a static `load()` or `loadMultiple()` call on an entity class in a class that can take its services through dependency injection. `Node::load()` gets the storage from `\Drupal`, so such a class takes the entity type manager instead, and loads through `getStorage()`.

**Reports:**

- a static `load()` or `loadMultiple()` call on an entity class, such as `Node::load()`, in an instance method of a class that can take services. These are the classes that [`drupal/global-drupal-call`](<#drupalglobal-drupal-call>) checks: a class that implements `ContainerInjectionInterface` or `ContainerFactoryPluginInterface`, an entity handler, a container deriver, a service, a hook class and a `BlockBase` plugin. An entity class is any class that implements `EntityInterface`, from core, contrib or custom code;
- the same call with the class name written in full, such as `\Drupal\taxonomy\Entity\Term::loadMultiple()`.

**Not reported:**

- a static method, a constructor with a parameter that accepts null, a call in a [deprecation scope](<https://project.pages.drupalcode.org/mago/analyzer/deprecations/index.md#deprecation-scopes>), a call inside an anonymous class and test code, as for `drupal/global-drupal-call`;
- a call in an entity class, such as `static::load()`, since services aren't injected into an entity;
- a first-class callable, such as `Node::load(...)`;
- a call on an object, such as `$this->storage->load()` under a docblock that types the property as an entity class. PHP runs a static method called on an object too, but only the `Node::load()` form is a static load;
- any other class, such as a plain object built with `new`.

> [!NOTE]
>
> **Example**
>
> modules/example/src/Form/ExampleForm.php
>
> ```
> // Reported.
> namespace Drupal\example\Form;
>
> use Drupal\Core\Form\FormBase;
> use Drupal\user\Entity\User;
>
> class ExampleForm extends FormBase {
>
>   protected function accountName(int $uid): string {
>     return (string) User::load($uid)?->label();
>   }
>
> }
>
> // Fine.
> namespace Drupal\example\Form;
>
> use Drupal\Core\Entity\EntityTypeManagerInterface;
> use Drupal\Core\Form\FormBase;
>
> class ExampleForm extends FormBase {
>
>   public function __construct(
>     protected EntityTypeManagerInterface $entityTypeManager,
>   ) {}
>
>   protected function accountName(int $uid): string {
>     return (string) $this->entityTypeManager->getStorage('user')->load($uid)?->label();
>   }
>
> }
> ```

> [!NOTE]
>
> **Compared with Coder**
>
> - Coder knows 46 core entity classes, such as `Node`, `User` and `Term`, and the classes of its `classes` setting. The check takes every entity class, so it also reports the entity classes of contrib and custom modules.
> - Coder reads a class name that a `use` statement imports, and misses a name written in full, such as `\Drupal\taxonomy\Entity\Term::load()`.
> - Coder checks a class whose `extends` names one of 12 base classes, such as `FormBase`, a class whose own `implements` list names a container interface, and a class that the module's services file lists. The check takes the same classes as `drupal/global-drupal-call`, which adds subclasses, entity handlers, hook classes and the services of every services file.
> - Coder stops reading the file at the first call in a class that can't take services, so it misses a call in a later class of the same file.
> - Coder also reports a first-class callable, a call in a constructor with a nullable parameter, in deprecated code or in an anonymous class, and test code.
> - A deliberate lookup, such as one that breaks a circular dependency, is reported by both tools. Mark it with `@mago-expect analysis:drupal/static-entity-load`.

---

## drupal/entity-storage-injection

- **Level:** warning
- **Source:** phpstan-drupal
- **Ports:** phpstan-drupal's `EntityStorageDirectInjectionRule`

phpstan-drupal reports an entity storage that a constructor takes. Another module can swap the storage handler of an entity type, so code asks the entity type manager for the storage where it uses it.

**Reports:**

- a constructor parameter typed as an entity storage. An entity storage type is `EntityStorageInterface`, or a class or an interface that extends it and whose name ends in `Storage` or `StorageInterface`.

**Not reported:**

- an entity handler's own storage. A handler's `createInstance()` gets its storage from the entity type manager, so a `$storage` parameter, or `$storage_controller` as in core's views data, of a class that implements `EntityHandlerInterface` is left alone. Any other storage that the handler takes is reported;
- as many storage parameters as the parent's constructor takes, under any name, since the class has to pass them on;
- a type whose name doesn't end in `Storage` or `StorageInterface`, even when it extends `EntityStorageInterface`.

> [!NOTE]
>
> **Example**
>
> modules/example/src/ExampleLister.php
>
> ```
> // Reported.
> namespace Drupal\example;
>
> use Drupal\Core\Entity\EntityStorageInterface;
>
> class ExampleLister {
>
>   public function __construct(
>     protected EntityStorageInterface $nodeStorage,
>   ) {}
>
> }
>
> // Fine.
> namespace Drupal\example;
>
> use Drupal\Core\Entity\EntityTypeManagerInterface;
>
> class ExampleLister {
>
>   public function __construct(
>     protected EntityTypeManagerInterface $entityTypeManager,
>   ) {}
>
> }
> ```

> [!NOTE]
>
> **Compared with phpstan-drupal**
>
> - phpstan-drupal also reports an entity handler's own `$storage` parameter.
> - phpstan-drupal takes any type that is an `EntityStorageInterface`, whatever its name.

---

## drupal/entity-storage-property

- **Level:** warning
- **Source:** phpstan-drupal
- **Ports:** phpstan-drupal's `EntityStoragePropertyAssignmentRule`

phpstan-drupal reports an entity storage that the object keeps in a property, for the same reason as [`drupal/entity-storage-injection`](<#drupalentity-storage-injection>).

**Reports:**

- a property whose declared type, or `@var` type, is an entity storage. The storage types are the same as for `drupal/entity-storage-injection`;
- such a property in a trait.

**Not reported:**

- a promoted constructor parameter, which is reported by `drupal/entity-storage-injection`;
- an entity handler's own `$storage` or `$storage_controller` property. In a trait, `$storage` is left alone when every class that uses the trait is an entity handler.

> [!NOTE]
>
> **Example**
>
> modules/example/src/ExampleLister.php
>
> ```
> // Reported.
> namespace Drupal\example;
>
> use Drupal\Core\Entity\EntityStorageInterface;
> use Drupal\Core\Entity\EntityTypeManagerInterface;
>
> class ExampleLister {
>
>   protected EntityStorageInterface $nodeStorage;
>
>   public function __construct(EntityTypeManagerInterface $entity_type_manager) {
>     $this->nodeStorage = $entity_type_manager->getStorage('node');
>   }
>
> }
>
> // Fine.
> namespace Drupal\example;
>
> use Drupal\Core\Entity\EntityTypeManagerInterface;
>
> class ExampleLister {
>
>   public function __construct(
>     protected EntityTypeManagerInterface $entityTypeManager,
>   ) {}
>
> }
> ```

> [!NOTE]
>
> **Compared with phpstan-drupal**
>
> - phpstan-drupal reports an assignment of a storage to a property of `$this`. So it also reports an untyped property with no `@var` tag that a storage is assigned to, and not a typed property that's never assigned. The check reads the type of the property.
> - phpstan-drupal leaves alone a property declared by a parent class whose constructor takes a storage, such as `EntityListBuilder::$storage`. The check leaves alone an entity handler's own `$storage`.
> - phpstan-drupal takes any type that is an `EntityStorageInterface`, whatever its name.

---

## drupal/logger-from-factory

- **Level:** error
- **Source:** phpstan-drupal
- **Ports:** phpstan-drupal's `LoggerFromFactoryPropertyAssignmentRule`

phpstan-drupal reports a logger channel that a constructor takes from the logger factory and keeps on the object, in a class that uses `DependencySerializationTrait`. The trait serializes a service by its id. A channel from the factory doesn't have an id, so the trait can't restore the property. Inject the channel service itself, such as `logger.channel.example`.

**Reports:**

- a `get()` call on a `LoggerChannelFactoryInterface` whose result is assigned straight to a property of `$this`, in the constructor of a class that uses `DependencySerializationTrait`. The class can use the trait itself, through a parent class or through another trait.

**Not reported:**

- a channel fetched outside the constructor, which is fetched again after the object is unserialized;
- a channel kept in a local variable, and a value computed from a channel;
- a call inside an anonymous class.

> [!NOTE]
>
> **Example**
>
> modules/example/src/ExampleQueue.php
>
> ```
> // Reported.
> namespace Drupal\example;
>
> use Drupal\Core\DependencyInjection\DependencySerializationTrait;
> use Drupal\Core\Logger\LoggerChannelFactoryInterface;
> use Drupal\Core\Logger\LoggerChannelInterface;
>
> class ExampleQueue {
>
>   use DependencySerializationTrait;
>
>   protected LoggerChannelInterface $logger;
>
>   public function __construct(LoggerChannelFactoryInterface $logger_factory) {
>     $this->logger = $logger_factory->get('example');
>   }
>
> }
>
> // Fine.
> namespace Drupal\example;
>
> use Drupal\Core\DependencyInjection\DependencySerializationTrait;
> use Drupal\Core\Logger\LoggerChannelInterface;
>
> class ExampleQueue {
>
>   use DependencySerializationTrait;
>
>   public function __construct(
>     protected LoggerChannelInterface $logger,
>   ) {}
>
> }
> ```

> [!NOTE]
>
> **Compared with phpstan-drupal**
>
> phpstan-drupal reports the same assignment, and also in the constructor of an anonymous class that uses the trait.

---

## drupal/dependency-serialization-property

- **Level:** error
- **Source:** phpstan-drupal
- **Ports:** phpstan-drupal's `DependencySerializationTraitPropertyRule`

phpstan-drupal reports a property that `DependencySerializationTrait` can't restore. The trait's `__sleep()` lists the properties that `get_object_vars()` sees in the class that composes the trait, and its `__wakeup()` writes them back by name. Neither method reaches a private property of another class, and before PHP 8.4 `__wakeup()` can't write a readonly property declared below the composing class.

A class composes the trait when its body uses the trait, or a trait that uses it, since a trait method runs in the scope of the class that uses it. The check looks at every class that uses the trait, itself, through another trait or through a parent class.

**Reports:**

- a private property of a class that uses `DependencySerializationTrait`, the composing class included. When a subclass is serialized, `__sleep()` would have to name that property in its mangled form;
- before PHP 8.4, a readonly property whose type can hold an object, declared below the composing class. A type of scalars and arrays can't. PHP 8.4 lets the parent's scope write it. As in phpstan-drupal, any readonly property of such a type counts, although `__wakeup()` writes only the properties that held a service or an entity storage, so a readonly value object such as core's `Component::$metadata` is reported too;
- a private property that a trait in the `Drupal\` namespace, named in the class body, brings into the class. It is reported at the class, since the trait's file serves other classes;
- in a class that composes the trait itself, below parents that don't, each private property of those parents, and before PHP 8.4 each of their readonly properties of such a type. These are reported at the class, once per property, naming the parent. The trait's `__sleep()` never lists them.

Promoted constructor parameters count. The classes that compose the trait are read from the PHP files under the Drupal root. Without core on disk, core's three bases stand in: `FormBase`, `PluginBase` and `EntityHandlerBase`. So core's forms, plugins and entity handlers count, and controllers, plugin forms and views plugins, which don't use the trait, don't.

**Not reported:**

- a static property, which `get_object_vars()` doesn't list;
- a property of a parent or a trait outside the `Drupal\` namespace, such as Symfony's session handler base, PHPUnit's `TestCase` or Prophecy's trait, since the module can't change them. A property that a trait in the `Drupal\` namespace brings into a parent counts as the parent's, and one that it brings into the class itself as the class's own;
- the parents' properties in a class with a `__sleep()` of its own, or with a `__serialize()`, which PHP calls in place of the trait's `__sleep()`.

> [!NOTE]
>
> **Example**
>
> modules/example/src/ExampleQueue.php
>
> ```
> // Reported.
> namespace Drupal\example;
>
> use Drupal\Core\DependencyInjection\DependencySerializationTrait;
> use Drupal\Core\State\StateInterface;
>
> class ExampleQueue {
>
>   use DependencySerializationTrait;
>
>   public function __construct(
>     private StateInterface $state,
>   ) {}
>
> }
>
> // Fine.
> namespace Drupal\example;
>
> use Drupal\Core\DependencyInjection\DependencySerializationTrait;
> use Drupal\Core\State\StateInterface;
>
> class ExampleQueue {
>
>   use DependencySerializationTrait;
>
>   public function __construct(
>     protected StateInterface $state,
>   ) {}
>
> }
> ```

> [!NOTE]
>
> **Compared with phpstan-drupal**
>
> - phpstan-drupal doesn't report the private and readonly properties of the parents of a class that composes the trait.
> - phpstan-drupal also reports a private static property.
