A few words on type checking at runtime
Tags: php
Published: 2024-03-24 14:45:24

_This is a copy of an old post on my private blog from July 2023._

## Some ancient history

I was lucky enough to start my journey with PHP already at version 4. We were finally getting some OOP and stuff. But back then, PHP was still a weakly typed language (it still is, but we can change that if we want to). The dynamic nature of PHP made it possible to assign different types to the same property at runtime. There were only two ways to ensure we get the type we expected:

- tests - back then, only a minority of PHP developers wrote tests
- use the `instanceof` operator or functions like `is_string`

And so it went. People either didn't care and let things crash in unexpected ways at runtime, or they used those functions and the `instanceof` operator to make sure they really got what they expected.

Let's take this as an example:

```php
class Foo
{
  function doSomethingElse(){}
}

class Bar
{
  var $foo;
  
  function __construct($foo){
    $this->foo = $foo;
  }
  
  function getFoo(){
    $this->foo;
  }
  
  function doSomething(){
    $this->foo = 1;
  }
}

$bar = new Bar(new Foo());
$foo = $bar->doSomethig();
$foo = $bar->getFoo();
$foo->doSomethingElse();
```

Here, the only way to know if the method `getFoo()` returned an instance of `Foo` is to either have a test or use the `instanceof` operator. Otherwise, our code blows up on runtime.

## The modern times of PHP

Let's fast-forward 15 years when PHP 7 has already reached EOL, and PHP 8.3 (at the time of the writing) is the latest and greatest. And where tests are (or at least I genuinely hope so) are rather a norm than an exception.

I expected that as long as we use strict typing and tests, the usage of `instanceof` and other type-checking functions would be minimal. Please don't get me wrong. I totally understand that this is sometimes the way to go. While we almost always can get away without them, there might be some cases where we restore to such tactics for pragmatic reasons. But these would be exceptions.

I was somehow surprised to notice interesting ways and arguments why developers were still using those technics.

### Example 1

```php
enum Foo: string {
  case FOO_1 = 'foo_1';
  case FOO_2 = 'foo_2';
}

readonly class Bar
{
  public function __construct(public ?Foo $foo, public ?string $baz) {}
}

class MyService
{
  public function doSomething(Bar $bar): array
  {
    $data = [];
    if ($bar->foo instanceof Foo) {
      $data['foo'] = $bar->foo->value;
    }
    if (is_string($bar->baz)) {
      $data['baz'] = $bar->baz;
    }
    
    return $data;
  }
}
```

The data was later used so that both `foo` and `baz` were expected to be strings (if the values were present at all). This was done because we cannot just check for `if ($bar->foo)` or `if ($bar->baz)` because this does not guarantee that the value we get will be a string.

Let me try to polemize with this reasoning.

Let's take `instanceof` as an example. The problem I see there is that the fact that an object is an instance of a specific class tells us nothing about whether it can be represented as a string. In this case, we do not cast the object to a string but must use the public property `value`. To be complete, we must check if that property exists and is of a specific type. Checking that would require us to use reflection at runtime with EVERY invocation.

Last but not least - how do we know if the property `foo` even exists on `Bar`? Should we check that as well? Not practical, IMO.

Something similar could be said about the property `baz`. It's not as extreme as `foo`, though. We would only need to check if the property exists on Bar and if it `is_string`. We still wouldn't get away without reflection, but it is more straightforward.

This strongly resembles PHP 4, when we could change any type at runtime. However, the code above does not allow us to do this.

However, one could raise another argument: "But what if someone changes the property type? My code makes a wrong assumption now and will not work as expected."

This is a valid argument. Let's alter the code to something like this:

```php
enum Foo: int
{
  case FOO_1 = 1;
  case FOO_2 = 2;
}
```

If we do not do the checks we discussed before, our output is not what we wanted. Instead of a string, we now have an integer.

But is there a different way to ensure the result is as expected? I would say yes. All we need is a test case.

```php
class Test extends TestCase
{
  public function test(): void
  {
    $foo = Foo::FOO_1;
    $bar = new Bar($foo, 'baz');
    $myService = new MyService();
    
    $result = $myService->doSomething($bar);
    self::assertSame(['foo' => 'foo_1', 'baz' => $baz], $result);
  }
}
```

There is no way to keep the test green when altering the code. We would always need to adjust the test. This is a guarantee that what we were doing in `MyService` will result in a data structure as expected. There is no need to check if something is a string or a specific type. Especially not at runtime. Because it will never change at runtime. And if we temper with the code, our tests will tell us we did something wrong.

This could easily be extended to more situations, like when we return an optional from a method. If our signature tells us we get `?MyClass, ` then all we need to do is check if the value is not `null`. If we wrote a decent test checking for side effects, it can't be anything else than `MyClass`.