Skip to content

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 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, 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).
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());
  }

}
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 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, 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.
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();
  }

}
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.
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,
  ) {}

}
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.

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.
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,
  ) {}

}
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.
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,
  ) {}

}
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().
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,
  ) {}

}
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.