Type System
PHPStan’s type system is a collection of classes implementing the common PHPStan\Type\Type
interface to inform the analyser about relationships between types, and their behaviour.
To retrieve the type of an AST expression, you need to call the getType()
method on the Scope object.
Each type that we can encounter in PHP language and in PHPDocs has an implementation counterpart in PHPStan:
Show table of PHPStan\Type\Type
implementations
Type | PHPStan class |
---|---|
mixed | PHPStan\Type\MixedType |
Foo (object of class Foo ) | PHPStan\Type\ObjectType |
Foo<T> | PHPStan\Type\Generic\GenericObjectType |
object | PHPStan\Type\ObjectWithoutClassType |
array | PHPStan\Type\ArrayType |
int | PHPStan\Type\IntegerType |
Integer interval | PHPStan\Type\IntegerRangeType |
float | PHPStan\Type\FloatType |
null | PHPStan\Type\NullType |
string | PHPStan\Type\StringType |
class-string | PHPStan\Type\ClassStringType |
class-string<T> | PHPStan\Type\Generic\GenericClassStringType |
static | PHPStan\Type\StaticType |
$this | PHPStan\Type\ThisType |
void | PHPStan\Type\VoidType |
callable | PHPStan\Type\CallableType |
iterable | PHPStan\Type\IterableType |
never | PHPStan\Type\NeverType |
Enum case (Foo::LOREM) | PHPStan\Type\Enum\EnumCaseObjectType |
In some cases PHPStan knows about the literal value of an expression. These classes implement the PHPStan\Type\ConstantType
interface:
Type | PHPStan class |
---|---|
Array shapes | PHPStan\Type\Constant\ConstantArrayType |
true and false | PHPStan\Type\Constant\ConstantBooleanType |
Integers | PHPStan\Type\Constant\ConstantIntegerType |
Floats | PHPStan\Type\Constant\ConstantFloatType |
Strings | PHPStan\Type\Constant\ConstantStringType |
Some types exist to only consist of other types (learn more about union vs. intersection types:
Type | PHPStan class |
---|---|
Union types | PHPStan\Type\UnionType |
Intersection types | PHPStan\Type\IntersectionType |
Some advanced types are implemented by combining different types in an intersection type:
Type | First PHPStan class | Second PHPStan class |
---|---|---|
non-empty-string | PHPStan\Type\StringType | PHPStan\Type\Accessory\AccessoryNonEmptyStringType |
numeric-string | PHPStan\Type\StringType | PHPStan\Type\Accessory\AccessoryNumericStringType |
callable-string | PHPStan\Type\StringType | PHPStan\Type\CallableType |
literal-string | PHPStan\Type\StringType | PHPStan\Type\Accessory\AccessoryLiteralStringType |
non-empty-array | PHPStan\Type\ArrayType | PHPStan\Type\Accessory\NonEmptyArrayType |
After asking about method_exists() | Any object type | PHPStan\Type\Accessory\HasMethodType |
After asking about property_exists() | Any object type | PHPStan\Type\Accessory\HasPropertyType |
After asking about isset() or array_key_exists() | PHPStan\Type\ArrayType | PHPStan\Type\Accessory\HasOffsetType |
Generic template types are represented with classes that implement the PHPStan\Type\Generic\TemplateType
interface. This table describes which implementation is used for different bounds (the X in @template T of X
):
Type | PHPStan class |
---|---|
mixed or no bound | PHPStan\Type\Generic\TemplateMixedType |
Foo (object of class Foo ) | PHPStan\Type\Generic\TemplateObjectType |
Foo<T> | PHPStan\Type\Generic\TemplateGenericObjectType |
object | PHPStan\Type\Generic\TemplateObjectWithoutClassType |
int | PHPStan\Type\Generic\TemplateIntegerType |
string | PHPStan\Type\Generic\TemplateStringType |
Union types | PHPStan\Type\Generic\TemplateUnionType |
What can a type tell us? #
The PHPStan\Type\Type
interface offers many methods to ask about the capabilities of values of this specific type. Following list is by no means complete, please see the interface code for more details.
The describe()
method returns a string representation (description) of the type, which is useful for error messages. For example StringType
returns 'string'
.
The accepts()
method tells us whether the type accepts a different type. For example IntegerType
accepts a different IntegerType
or ConstantIntegerType
. The accepts()
method doesn’t return a boolean, but a TrinaryLogic
object. This method shouldn’t be used for querying a specific type because the accepts()
semantics are complicated. For example FloatType
also accepts IntegerType
.
There are methods that answer questions about properties, methods, and constants accessed on a type. The get*
methods return reflection objects.
canAccessProperties(): TrinaryLogic
hasProperty(string $propertyName): TrinaryLogic
getProperty(string $propertyName, Scope $scope): PropertyReflection
canCallMethods(): TrinaryLogic
hasMethod(string $methodName): TrinaryLogic
getMethod(string $methodName, Scope $scope): MethodReflection
canAccessConstants(): TrinaryLogic
hasConstant(string $constantName): TrinaryLogic
getConstant(string $constantName): ConstantReflection
It’s safe to call a get*
method only after making sure that call to has*
method with the same argument returns yes
.
Querying a specific type #
If we need to know whether we are working with a specific type, there are multiple ways to do it and it might seem they all work until we realize there’s an edge case that’s not covered by one of the naive approaches.
If we want to know that we’re working with a string
, the first thing that comes to mind is to do $type instanceof StringType
. But that’s not going to work if we have a numeric-string
(an IntersectionType
consisting of StringType
and AccessoryNumericStringType
as can be seen in the table above). Neither it’s going to work if we have a union of literal strings, like 'foo'|'bar'
.
Asking about an array with $type instanceof ArrayType
is also a wrong approach - it’s not going to work for unions of ConstantArrayType
and it’s not going to work for non-empty-array
- which is an IntersectionType
with NonEmptyArrayType
.
The best way to ask about a specific type is the PHPStan\Type\Type::isSuperTypeOf(Type $type): TrinaryLogic
method. To understand how it works we need to imagine types as circles. The biggest circle containing all other types is mixed
(also known as the top type). The smallest circle that’s empty (doesn’t contain any type besides itself) is never
(also known as the bottom type).
This visual image of overlapping circles tells us how isSuperTypeOf()
always responds. Let’s say we draw a hierarchy of Throwable
- Exception
- InvalidArgumentException
. It can look like this:
Let’s say we have three Type
objects: new ObjectType(\Throwable::class)
(T), new ObjectType(\Exception::class)
(E), and new ObjectType(\InvalidArgumentException)
(IAE). Asking both T->isSuperTypeOf(E)
and T->isSuperTypeOf(IAE)
will return yes
. Because T
is the largest circle and contains both E
and IAE
.
If we ask E->isSuperTypeOf(T)
, it returns maybe
. Because in runtime T
might contain something that falls inside the E
circle (like InvalidArgumentException
) or something that falls outside of the E
circle (like TypeError
).
If we add new ObjectType(\stdClass::class)
(S) to the mix, both T->isSuperTypeOf(S)
and S->isSuperTypeOf(T)
return no
because the S
type is a separate circle from the T
type.
These relationships work between all types, as long as you imagine the circles correctly.
Circling back to our original examples, the correct way to ask whether PHPStan\Type\Type
is a string
, use this piece of code:
use PHPStan\Type\StringType;
/// ...
$isString = (new StringType())->isSuperTypeOf($type);
if ($isString->yes()) {
// we definitely have a string in $type,
// such as StringType or a UnionType of ConstantStringType objects
} elseif ($isString->maybe()) {
// we might have a string in $type
// it might be string|null or even mixed
} else {
// we definitely don't have a string in $type
// so it's for example an int or something else
}
In PHPStan 1.10 and later there are also new and easy shortcut methods like Type::isString(): TrinaryLogic
that will tell you the same thing.
Read an article on the same topic: Why Is instanceof *Type Wrong and Getting Deprecated?
Type normalization #
Types in non-canonical form can and should be simplified. If we write mixed|int
(a union type) into a PHPDoc, PHPStan normalizes the type to mixed
, because int
is already contained in mixed
. Keeping the int
in the union doesn’t add any extra information. The supertype always wins.
If we write mixed&int
(an intersection type), it’s normalized to int
. The subtype always wins.
Type normalization also prevents some invalid types to exist, like string&int
- that’s normalized to never
and detected as an error by PHPStan rules.
When creating custom types like:
$union = new UnionType([$a, $b, $c]);
$intersection = new IntersectionType([$a, $b, $c]);
Consider using PHPStan\Type\TypeCombinator
instead to kick off the type normalization:
$union = TypeCombinator::union($a, $b, $c);
$intersection = TypeCombinator::intersect($a, $b, $c);
Considerations for custom types #
You can also implement a custom type, usually representing a subtype of a more general type. This is typically done by extending an existing Type implementation, like StringType
or ObjectType
. The methods you’ll always need to override in your implementation are:
public function describe(VerbosityLevel $level): string
public function equals(Type $type): bool
public function isSuperTypeOf(Type $type): TrinaryLogic
public function accepts(Type $type, bool $strictTypes): TrinaryLogic
It’s important to correctly implement and test the isSuperTypeOf()
method. It tells the relationships between types - which one is more general and which one is more specific (see Querying a specific type. The best way to test this method is through TypeCombinator::union()
(a more general type should win) and TypeCombinator::intersect()
(a more specific type should win).
For example if you’re implementing your own UuidStringType
, TypeCombinator::union(new StringType(), new UuidStringType())
should result in StringType
. On the other hand, TypeCombinator::intersect(new StringType(), new UuidStringType())
should result in UuidStringType
. PHPStan has excessive tests for the built-in types in TypeCombinatorTest which is a great starting point to learn from.
Once you have your own PHPStan\Type\Type
implementation, you can add support for it in PHPDocs through a custom TypeNodeResolverExtension.