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
\Drupalin an instance method of a class that implementsContainerInjectionInterfaceorContainerFactoryPluginInterface. 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 indrush.services.ymlisn'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
BlockBaseplugin, which can take services once it implementsContainerFactoryPluginInterfaceand addscreate(); - a call on a subclass or an instance of
Drupalin 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
testsdirectory).
Example
// 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
\Drupalitself. The check also takes a call on a subclass ofDrupal. - phpstan-drupal skips a class that implements PHPUnit's
Test, and the check skips any file under atestsdirectory.
drupal/static-entity-load¶
- Level: warning
- Source: Coder's
DrupalPracticestandard - 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()orloadMultiple()call on an entity class, such asNode::load(), in an instance method of a class that can take services. These are the classes thatdrupal/global-drupal-callchecks: a class that implementsContainerInjectionInterfaceorContainerFactoryPluginInterface, an entity handler, a container deriver, a service, a hook class and aBlockBaseplugin. An entity class is any class that implementsEntityInterface, 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 theNode::load()form is a static load; - any other class, such as a plain object built with
new.
Example
// 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,UserandTerm, and the classes of itsclassessetting. 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
usestatement imports, and misses a name written in full, such as\Drupal\taxonomy\Entity\Term::load(). - Coder checks a class whose
extendsnames one of 12 base classes, such asFormBase, a class whose ownimplementslist names a container interface, and a class that the module's services file lists. The check takes the same classes asdrupal/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 inStorageorStorageInterface.
Not reported:
- an entity handler's own storage. A handler's
createInstance()gets its storage from the entity type manager, so a$storageparameter, or$storage_controlleras in core's views data, of a class that implementsEntityHandlerInterfaceis 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
StorageorStorageInterface, even when it extendsEntityStorageInterface.
Example
// 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
$storageparameter. - 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
@vartype, is an entity storage. The storage types are the same as fordrupal/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
$storageor$storage_controllerproperty. In a trait,$storageis left alone when every class that uses the trait is an entity handler.
Example
// 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@vartag 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 aLoggerChannelFactoryInterfacewhose result is assigned straight to a property of$this, in the constructor of a class that usesDependencySerializationTrait. 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
// 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'sComponent::$metadatais 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'sTestCaseor Prophecy's trait, since the module can't change them. A property that a trait in theDrupal\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
// 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.