Skip to content

Toobo/TypeChecker

Repository files navigation

Type Checker

Quality Assurance

What is this

You can think of it as a way to build an is_a() function on steroids.

is_a() only works for classes, interfaces and enums. It does not work for scalars (string, int, etc., including unary types like true and false) and it does not work for virtual types (iterable, callable).

Moreover, it does not work for complex types, such as unions, intersections, and DNF.

If you ever wanted to do something like:

is_a('iterable|MyThing|(Iterator&Countable)|null', $thing);

Then this package is for you. But there's more.

is_a() accepts a type string, but sometimes one wants to check against a ReflectionType, and this package can do that as well.

An example with a string:

use Toobo\TypeChecker\Type;

Type::byString('iterable|MyThing|(Iterator&Countable)|null')->satisfiedBy($thing);

with a ReflectionType:

use Toobo\TypeChecker\Type;

function test(iterable|MyThing|(Iterator&Countable)|null $param) {}
$refType = (new ReflectionFunction('test'))->getParameters()[0]->getType();

Type::byReflectionType($refType)->satisfiedBy($thing);

A deeper look

Named constructors

Besides by strings and by reflection, the Type class can also be instantiated using named constructors such as Type::string(), Type::resource(), or Type::mixed(), but also Type::iterable(), Type::callable() or even Type::null(), Type::true(), and Type::false().

For completeness' sake, it is also possible to create instances for types that will never match any value, like Type::void() or Type::never().

Matching types

The Type class aims at representing the entire PHP type system. And it is possible to compare one instance with another:

assert(Type::byString('IteratorAggregate&Countable')->matchedBy('ArrayObject'));
assert(Type::byString('IteratorAggregate&Countable')->matchedBy(Type::byString('ArrayObject')));

assert(Type::byString('ArrayObject')->isA('IteratorAggregate&Countable'));
assert(Type::byString('ArrayObject')->isA(Type::byString('IteratorAggregate&Countable')));

Type::matchedBy() and Type::isA() both accept a string or another type instance and check type compliance. They are the inverse of each other.

Type::matchedBy() behavior can be described as: if a function's argument type is represented by the type calling the method, would it be satisfied by a value whose type is represented by the type passed as argument?

Type::isA() behavior can be described as: if a function's argument type is represented by the type passed as argument, would it be satisfied by a value whose type is represented by the instance calling the method?

"Checked" VS "Unchecked" instantiation

When instantiating a Type instance from a string, that is checked to make sure the string represents a valid type (or at least, looks like a valid type). Passing a string that does not contain valid PHP syntax for a type definition (including usage of reserved PHP keywords) will result in an error being thrown.

On the other hand, instantiation by ReflectionType is "unchecked" because if we obtain a ReflectionType instance, we know that's a valid PHP type. The only possible error building from ReflectionType can be caused by using "late state binding" types, more on this below.

Late Static Binding Types

PHP support "late state binding" (LSB) types self, parent and static in return-only type declaration. These types are called "late" as their actual type is calculated at runtime.

The main goal of this library is to check types, and it is impossible to check LSB types without knowing the context where they were used, and such context is missing in a simple string such as "self" or in a "ReflectionType instance.

For that reason this library does not support them. Trying to create a Type instance from any type that reference those LSB types will result in an error being thrown.

Type information

The Type class has several methods to get information about the PHP type it represents.

  • isStandalone()
  • isUnion()
  • isIntersection()
  • isDnf()

can tell what kind of composite type is, or if not composed at all.

There's also a isNullable() method.

Type position utils

PHP allows type declarations in three places:

  • Function arguments types
  • Function return
  • Properties declaration

And a slightly different set of types is supported in the three positions.

For example, void and never can only be used as return types, and callable can not be used as property type.

The Type class has three methods: Type::isPropertySafe(), Type::isArgumentSafe(), and Type::isReturnSafe() which can be used to determine if the instance represents a type that can be used in the three positions.

Comparison with other libraries

There are other libraries out there that deals with "objects representing types".

  • PHPDoc Parser for PHPStan is an amazing type parser from doc bloc. While it is similar to this package about creating "type objects" from strings, the PHPStan package more powerful supporting much more than the PHP native type system. However, it does not support the possibility to check if a value belongs to that type, which is this package's main reason to exist.

  • PHP Documentor's TypeResolver is based on the PHPStan's package mentioned above. And the same differences highlighted above apply.

  • Symfony type-info component. The "new guy" on the scene, still experimental. It is also based on the PHPStan library for string parsing, but similarly to this library also deals with reflection types. At the moment of writing, the possibility to check a value satisfy a type is only limited to native "standalone" types, which includes virtual types such as iterable and callable, but does not include composed types.

In general, this package dependency-free, simpler in only targeting PHP-supported type rather than "advanced" types that are used in documentation and static analysis. This limited scope allows for much simpler code in a single class. Moreover, this library focuses on checking type of values more than "parsing" or "extracting" types from strings, like other libraries.