Typist is a PHP library enforcing types of local variables.
It internally uses references to typed properties introduced in PHP 7.4.
composer require sj-i/typist
- PHP 7.4 or later
use Typist\Typist;
// type enforcements are valid during the lifetime of this `$_`
$_ = [
Typist::int($typed_int, 1),
Typist::string($typed_string, 'str'),
Typist::bool($typed_bool, false),
Typist::float($typed_float, 0.1),
Typist::class(\DateTimeInterface::class, $typed_object, new \DateTime()),
];
assert($typed_int === 1);
assert($typed_string === 'str');
assert($typed_bool === false);
assert($typed_float === 0.1);
assert($typed_object instanceof \DateTime);
// modifications with valid types are OK
$typed_int = 2;
$typed_string = 'trs';
$typed_bool = true;
$typed_float = -0.1;
$typed_object = new DateTimeImmutable();
// any statements below raises TypeError
$typed_int = 'a';
$typed_string = 1;
$typed_bool = 'a';
$typed_float = 'a';
$typed_object = 'a';
Function interfaces are also available.
use function Typist\int;
use function Typist\float;
use function Typist\string;
use function Typist\bool;
use function Typist\class_; // trailing underscore is needed
$_ = [
int($typed_int, 1),
string($typed_string, 'str'),
bool($typed_bool, false),
float($typed_float, 0.1),
class_(\DateTimeInterface::class, $typed_object, new \DateTime()),
];
use Typist\Typist;
$_ = [
Typist::nullable()::int($typed_int1, 1),
Typist::nullable()::int($typed_int2, null),
Typist::nullable()::string($typed_string1, 'str'),
Typist::nullable()::string($typed_string2, null),
Typist::nullable()::bool($typed_bool1, false),
Typist::nullable()::bool($typed_bool2, null),
Typist::nullable()::float($typed_float1, 0.1),
Typist::nullable()::float($typed_float2, null),
Typist::nullable()::class(\DateTimeInterface::class, $typed_object1, new \DateTime()),
Typist::nullable()::class(\DateTimeInterface::class, $typed_object2, null),
];
or if you use PHP8, ()
can be omitted.
use Typist\Typist;
$_ = [
Typist::nullable::int($typed_int, null),
Typist::nullable::string($typed_string, null),
Typist::nullable::bool($typed_bool, null),
Typist::nullable::float($typed_float, null),
Typist::nullable::class(\DateTimeInterface::class, $typed_object, null),
];
Function interfaces are available here too.
use function Typist\nullable_int;
use function Typist\nullable_float;
use function Typist\nullable_string;
use function Typist\nullable_bool;
use function Typist\nullable_class; // trailing underscore is not needed
$_ = [
nullable_int($typed_int, null),
nullable_string($typed_string, null),
nullable_bool($typed_bool, null),
nullable_float($typed_float, null),
nullable_class(\DateTimeInterface::class, $typed_object, null),
];
If you have ever read the RFC of Typed Properties, especially its section describing how it works with references, you might not have anything unclear.
So I try to put a bit of explanation here, with the readers who haven't read yet in mind.
Firstly, please see the following code.
$a = 1;
$b =& $a;
$c =& $b;
Let's illustrate this code step by step, line by line.
$a = 1;
The first line can be illustrated as below.
There is a variable named $a
, and its value is 1
.
That's all, very simple.
$b =& $a;
Then the code added the second line can be illustrated as below.
There are two variables named $a
and $b
.
They are just different names indicating the same data, which is 1
in this case.
The relationship is not like "$a is a variable which contains 1, and $b is a pointer to $a".
The functionality of the reference assignment operator =&
is just creating another name of data.
$c =& $b;
One more reference assignment creates one more name. That's it. The final state can be illustrated as below.
So there can be a group that consists of multiple names indicating the same data. Any modifications via any names affect the result of reading via any other names in the group. The RFC of the Typed Properties calls them "reference set".
Then, what happens if a reference set contains typed properties?
Now it's about properties, so using $a
or $b
as an example is not appropriate anymore.
This time I change the code as below.
$o = new class() {
public int $a = 1;
public float $b;
};
$o->b =& $o->a;
$c =& $o->b;
It can be re-illustrated like this.
So let's try to assign some values to $o->a
.
$o->a = 1; // legal
$o->a = 'abc'; // TypeError will be thrown. $o->a is declared as int
It's easy and understandable results.
On the other hand, what about the example below?
$o->b = 1.5; // $o->b is declared as float, but...
var_dump($o->a); // $o->a is declared as int, so this must not be `1.5`!
And what about the example below?
$c = new DateTime(); // $c is a local variable, so doesn't have any type constraint itself, but...
var_dump($o->a, $o->b); // Neither $o->a and $o->b must not be a DateTime!!!
If a reference set contains typed properties, all types in the set must be satisfied on its assignment. The interpreter actually checks each property type in that situation and throws \TypeError
on error.
Here you have seen how a local variable can be a typed variable. A local variable within a reference set that contains typed properties is checked with its type.
You can get such local variables in two ways. One is reference assignment from typed properties, and another is reference assignment to typed properties.
Reference assignment from typed properties can make typed local variables. Though I haven't chosen this way, I think this method is intuitive because what we are trying is virtually to extract the ability of type checking from typed properties.
I have seen two instances of this method.
- Poor Man's Typed Variables from the slides by @nikic
- azjezz/typed by @azjezz
The implementation of them could be summarized as below.
- Define functions like below for each type.
- Instantiate object which has a property with requested type.
- Grab the object in the global state, because the object will be GCed otherwise.
- Return reference to the typed property.
Both of them leak memory because each creation of typed variables put the reference to the typed property object in the global state. azjezz/typed provides methods to free the references manually.
There are two things I don't like about this method.
- Memory leak (or manual memory management)
- Forcing reference assignment in user code
- using
&=
everywhere is somehow not pretty! - yeah, it's just my preference!!!!
- using
As you can see from the code in this repository, problem 2 can be solved by using pass-by-reference.
Reference assignment from a typed property makes a reference set containing a typed property.
Reference assignment to a typed property also makes a reference set containing a typed property.
Then the return value can be used for something else.
By returning typed property object (this is called Enforcer in this library) and grab it by a local variable in the caller, problem 1 can be solved. The variable is only used to keep refcount of Enforcer, so an inconspicuous name like $_
would be good.
Psalm emits warning when the type of a pass-by-reference variable is changed.
So if you use this library, the type of local variable can also be checked statically.
And more, $_
is ignored by UnusedVariable checks.