Traits¶
Mago analyzes a trait on its own, without the classes that use it. The analyzer types the calls
from a trait to the methods of those classes, the properties of those classes that the trait
reads, and the fluent methods kept in traits by core. The
check reports a self:: or static:: call from a static method of a trait to an instance method.
Calls from traits¶
Mago analyzes a trait once, on its own. A call on $this to a method that the trait doesn't declare
is then a missing method, and returns mixed, even when every class that uses the trait has the
method. PHPStan checks the body of a trait in each class that uses it instead. The analyzer finds
the classes that use the trait, and when every one of them has the method, it types the call from
their declarations.
trait RedirectTrait {
protected function redirectUrl(): Url {
// Typed from the getEntity() of the forms that use the trait.
return $this->getEntity()->toUrl();
}
}
Typed:
- the result, as the union of the return types of the classes.
staticand$thismean the class that uses the trait, so they stay the trait's$this, and a call chained on the result is typed the same way; - a parameter, only when every class gives it the same type, so an argument is checked where the classes agree;
self::name()andstatic::name()in an instance method, which reach an instance method through$this. In a static method, the call isdrupal/trait-non-static-call;- a call to a generic method that gets a class name for each of its templates, as
Foo::classor as a string that names a class.$this->createMock(Foo::class)in a test trait is aMockObject&Foo, and$this->getMockBuilder(Foo::class)aMockBuilder<Foo>.
Not typed:
- a call to a method that one of the classes lacks, which keeps the report;
- a call in an unused trait, or in one that doesn't declare a method of its own, since the classes are found through one of its methods;
- any other call to a generic method, such as
createMock($class), which returnsmixed; - a call to a method whose documented return type names something that can't be resolved by Mago,
such as a stray
*after the class name in a docblock. A call on the class returnsmixedthere too.
The search runs once per trait, and looks only at the classes that use the trait without inheriting it from another class that uses it. A subclass has every method and property of its parent, and PHP keeps the return type of an override within the one that it overrides, so those classes answer for their subclasses too. A test trait that thousands of test classes use through a few base classes costs a few method lookups.
Properties¶
A property read on the $this of a trait, such as $this->container, is typed when every class
that uses the trait declares it with the same type:
trait SchemaDumpTrait {
protected function dump(): string {
// Typed from the $container of the test classes that use the trait.
return $this->container->get('example.schema')->dump();
}
}
Typed:
$this->name, as the type that the classes give the property, natively or in a@vartag.
Not typed:
- a property that the classes declare with different types, or that one of them leaves without a
type or documents as
mixed. The union of the types would report a call once for every class whose type doesn't have the method; - a property that one of the classes only has through a
@propertytag; - a static read,
self::$nameorstatic::$name; - a write, which isn't checked, since the classes may accept different values;
- a property of a trait outside
core/lib,core/tests, and thesrcandtests/src/Traitsdirectories of the extensions under the Drupal root. The traits are read from disk when the workers start, so a trait added later is typed after the next start.
Whether the property is typed or not, the report of a missing property is dropped when every class
that uses the trait has the property, declared or through a @property tag:
- a static read needs a declared static property in every class, since PHP throws for an instance or a magic one;
$this->nameneeds a property that isn't static.
Mago doesn't record a reference for the read, so a private property, or a protected one in a final
class, that only the trait reads is still reported as unused-property.
Fluent setters¶
Core documents @return $this on an interface, and keeps the body in a trait with a bare
{@inheritdoc}. A trait doesn't have a parent to inherit from, so on a concrete class the call is
typed mixed, and the rest of the chain with it. The analyzer types the call as the receiver for
the 42 methods of 16 core traits whose bodies only ever return $this.
AccessResult::allowed()->addCacheableDependency($entity)->andIf($other); // AccessResultAllowed
$node->setPublished()->setOwnerId(1); // Node
$form_state->setValue('a', 1)->setValue('b', 2); // FormState
A typed chain lets Mago check calls that stay unchecked on mixed, so it can show mistakes. For
example, setReason() is declared on the neutral and forbidden results, not on AccessResult, and
core calls it after an isAllowed() check that doesn't say anything about the type.
drupal/trait-non-static-call¶
- Level: error
- Source: PHP
A static method doesn't have $this, so self::name() or static::name() in it can't reach an
instance method, and PHP throws an Error. Mago analyzes a trait on its own, and reports a static
call to an instance method only from outside the class (invalid-static-method-access).
Reports:
- a
self::name()orstatic::name()call to an instance method, in a static method of a trait. The call counts when every class that uses the trait has the method and one of them declares it as not static. A call in a closure inside the static method counts too.
Not reported:
- a call to a method that the trait declares itself;
- the same call in a class, which isn't reported by Mago either;
- a call in an unused trait.
Example
namespace Drupal\example;
/**
* Labels things.
*/
final class ExampleLabel {
use LabelTrait;
/**
* Returns the label.
*/
public function label(): string {
return 'Example';
}
/**
* Returns the default label.
*/
public static function defaultLabel(): string {
return 'Example';
}
}
// Reported.
namespace Drupal\example;
/**
* Prints labels.
*/
trait LabelTrait {
/**
* Prints the label.
*/
public static function printLabel(): void {
echo self::label();
}
}
// Fine.
namespace Drupal\example;
/**
* Prints labels.
*/
trait LabelTrait {
/**
* Prints the label.
*/
public static function printLabel(): void {
echo self::defaultLabel();
}
}
Compared with phpstan-drupal
phpstan-drupal doesn't have a rule for this. PHPStan reports the call as method.staticCall,
since it checks the body of a trait in each class that uses it.