PHPStan 2.0 is released! Upgrade today and buy the PHPStan elephpant!
Menu

Why Is instanceof *Type Wrong and Getting Deprecated?

February 8, 2023 · 8 min read

This one is for developers of custom rules and other extension types. PHPStan can be extended by writing your own code. Many users take advantage of that and write custom rules for their proprietary code. Some of the most notable public extensions are phpstan-doctrine and Larastan. PHPStan also serves as the foundation for tools like Rector. The points and changes described below are important for developers of all those packages.

The upcoming release of PHPStan 1.10 is going to deprecate using instanceof *Type for many Type interface implementations. We have many good reasons to do so. If you rewrite your code based on the guidelines below, you’re going to have edge cases solved for free, and therefore have less bugs. By doing the same thing in PHPStan itself we’ve also fixed many bug reports.

StringType #

Let’s say you’re writing a custom rule and you want to check something about strings. If you follow dump-driven development [1], you would write a piece of code to test your rule against, and in that rule you would var_dump() the type representing an expression to figure out how to implement the rule:

public function doFoo(string $input): void {
    // yeah, the type of expr in PhpParser\Node\Stmt\Echo_ is PHPStan\Type\StringType
    echo $input;
}

In the rule we will not continue unless the type is StringType:

if (!$type instanceof StringType) {
  return [];
}

// ...

Looks reasonable, right? But PHPStan 1.10 with bleeding edge enabled or with phpstan-deprecation-rules installed will report the following error thanks to a new rule:

Doing instanceof PHPStan\Type\StringType is error-prone and deprecated. Use Type::isString() instead.

Why is it error-prone? Because not all strings are represented with just a StringType instance. Many strings, like non-empty-string, are actually an IntersectionType of StringType and some other accessory type. So in case of some strings, your straightforward check with instanceof StringType wouldn’t work as expected.

For a reliable check there’s a new method PHPStan\Type\Type::isString(): TrinaryLogic. Why does it return TrinaryLogic? Because it’s useful to know that:

  • This can’t be a string (e.g. an integer, an object)
  • This might be a string (e.g. mixed, int|string)
  • This is a string (e.g. string, non-empty-string, 'a')

In most situations you would be interested in the last case with $type->isString()->yes() but it’s useful to be aware of all possibilities.

ConstantStringType #

Another example of a deprecated check is instanceof ConstantStringType which represents a literal string like 'lorem ipsum'. The common pattern for working with it is:

if (!$type instanceof ConstantStringType) {
  return [];
}

// do something with $type->getValue() (which is 'lorem ipsum')

PHPStan 1.10 wants you to use Type::getConstantStrings(): list<ConstantStringType>. It’s plural because it wants you to handle unions of different strings. So after the refactoring your code is going to be able to handle 'lorem ipsum'|'dolor sit amet' as well:

if (count($type->getConstantStrings()) === 0) {
    return []; // no constant strings
}

foreach ($type->getConstantStrings() as $constantString) {
    // do something with each value
}

ArrayType #

Doing instanceof ArrayType comes with the same gotchas as StringType described above. When you have a non-empty-array type or a list, they’re actually an IntersectionType.

You can replace instanceof ArrayType with Type::isArray(): TrinaryLogic.

ObjectType #

Doing instanceof ObjectType breaks in many situations. Objects can be part of intersection types, objects usually come in unions, object can be represented by a completely different class when it’s a static type or $this.

Use Type::isObject(): TrinaryLogic to find out if the type is an object, or Type::getObjectClassNames(): list<string> to get a list of class names for a given object type.

You can also use Type::isSuperTypeOf(): TrinaryLogic to find out whether an object is of a specific class or its subclass:

// is YES for DateTimeInterface, DateTime, DateTimeImmutable
$isDateTime = (new ObjectType(DateTimeInterface::class))->isSuperTypeOf($type);

Learn more about isSuperTypeOf() in the documentation »

CallableType #

Many things in PHP can be a callable, not just the callable typehint. Doing instanceof CallableType will miss out on many of these types:

  • A string with a function name like 'date' is a callable
  • An array with an object and a method like [$this, 'method'] is a callable
  • An array with a class name and a static method like [Foo::class, 'method'] is a callable
  • A Closure is a callable
  • An object of a class with an __invoke() method is a callable

If you ask Type::isCallable(): TrinaryLogic it will account for all of these types. And you can get the actual callable signatures with Type::getCallableParametersAcceptors(): ParametersAcceptor[] as well.

ConstantBooleanType #

Surely true or false doesn’t get any more complicated and asking instanceof ConstantBooleanType always works, right!?

Let me break that for you:

/**
 * @template T
 * @param T $b
 * @return T
 */
public function sayHello($b)
{
    if ($b === true) {
        // T&true = IntersectionType
        \PHPStan\dumpType($b);
    }
}

It’s an IntersectionType because we need to carry that $b is still the template type T while at the same time we narrowed it down to true.

This is why you should use Type::isTrue(): TrinaryLogic and Type::isFalse(): TrinaryLogic.


This isn’t an exhaustive list of all types where doing instanceof *Type is deprecated in PHPStan 1.10, just the most notable examples. For a complete list please refer to the actual rule source code. This list is going to grow bigger in future PHPStan releases.

Don’t worry, this is currently just being deprecated. If you have instanceof *Type in your code, it’s still going to work for the scenarios where it has already worked. But as you can see, switching to the new methods can be very beneficial.

We’re deprecating it now so that we have free hands to do more changes in future major PHPStan releases. Not relying on instanceof to work will allow us to stop using inheritance in the typesystem. So for example ConstantStringType might no longer extend StringType in PHPStan 2.0.

Once we deprecate and eradicate $type instanceof *Type from PHPStan itself and all 3rd party PHPStan extensions as well, we’ll finally be able to decouple types from each other, which will allow us to do cool stuff, e.g. support all template type bounds automatically [2], or make all types subtractable automatically [3] as well.


  1. If you’ve just started working with the AST and with PHPStan’s typesystem, you don’t have much choice than to start orientating yourself by var_dump-ing what’s going on. ↩︎

  2. Right now each bound (like @template T of int) requires to have a custom class like TemplateIntegerType, so that both $type instanceof IntegerType and $type instanceof TemplateType pass. ↩︎

  3. Only few selected types are currently subtractable, like mixed or object. Playground example ↩︎

Theme
A
© 2016–2024 Ondřej Mirtes