In PHP 8 how to check with reflection whether passing an object will work?

Viewed 1962

Let's say I have

class X {
}
class Y {
}
function foo(X|Y $param) {
}
class Z extends Y {
}
class U {
}

Now, how could one verify that passing an instance of Z to foo works but an instance of U will not? Manually, it'd look like...

$r = new \ReflectionFunction('foo');
$found = FALSE;
foreach ($r->getParameters()[0]->getType()->getTypes() as $type) {
  $name = $type->getName();
  if ($name && is_subclass_of('\Z', $name)) {
    $found = TRUE;
  }
}

This is ugly to say the least and I am not even sure whether the non-emptiness check of $name is necessary.

Is there perhaps some "type matches" functionality in PHP 8?

4 Answers

I really don't understand why you want to check this yourself, when the interpreter already takes care of it, raising the correct error.

<?php

class X {
}

class Y {
}

function foo(X|Y $param) {
}

class Z extends Y {
}

class U {
}

$x = new X;
$y = new X;
$z = new Z;
$u = new U;

foo($x);
foo($y);
foo($z);
foo($u);

If you run this code, you will see that PHP correctly throws a TypeError on line 26, which is where foo($u) is being called, while all the other calls work:

Fatal error: Uncaught TypeError: foo(): Argument #1 ($param) must be of type X|Y, U given, called in /in/Ffidj on line 26 and defined in /in/Ffidj:9
Stack trace:
#0 /in/Ffidj(26): foo(Object(U))
#1 {main}
  thrown in /in/Ffidj on line 9

Process exited with code 255.

If you need to, you could simply wrap the call to foo in a try catch block to intercept the TypeError:

<?php

class X {
}

class Y {
}

function foo(X|Y $param) {
}

class Z extends Y {
}

class U {
}

function safeFoo($param) {
    try {
        foo($param);
        echo 'OK' . PHP_EOL;
    } catch (TypeError $e) {
        echo $e->getMessage() . PHP_EOL;
    }
}

$x = new X;
$y = new X;
$z = new Z;
$u = new U;

safeFoo($x);
safeFoo($y);
safeFoo($z);
safeFoo($u);

Output:

OK
OK
OK
foo(): Argument #1 ($param) must be of type X|Y, U given, called in /in/sEQIQ on line 20

Try something like this for php 8:

https://3v4l.org/6AnNZ#v8.0.17

UPD: Sorry for only-link answer, but there are a lot of code, and formatting it inside the stackoverflow editor is hard. So I put it in the most popular php sandbox 3v4l.org.

The code defines function for checking if variable is acceptable by given function.

UPD2: Here is the code

function isCompatibleWithType(string $type, mixed $value): bool {
    if (gettype($value) === 'object') {
        return get_class($value) === $type || is_subclass_of($value, $type);
    }
    
    return gettype($value) === $type;
}

function canAccept(callable $func, int $paramPosition, mixed $paramValue): bool {
    $r = new \ReflectionFunction($func);
    
    foreach ($r->getParameters() as $parameter) {
        if ($parameter->getPosition() !== $paramPosition) {
            continue;
        }
        
        $type = $parameter->getType();
        if ($type instanceof \ReflectionUnionType) {
            foreach ($type->getTypes() as $t) {
                if (isCompatibleWithType($t->getName(), $paramValue) || ($t->allowsNull() && is_null($paramValue))) {
                    return true;
                }
            }
            
            return false;
        } 
        
        if ($type instanceof \ReflectionIntersectionType) {
            foreach ($type->getTypes() as $t) {
                if (!isCompatibleWithType($t->getName(), $paramValue)) /* intersection types do not support nullables for now */ {
                    return false;
                }
            }
            
            return true;
        }
        
        return isCompatibleWithType($type->getName(), $paramValue) || ($t->allowsNull() && is_null($paramValue));
    }
    
    return false;
}

I think the solution is to create a ReflectionClass object out of the argument instead of the function. I've tested the following in PHP 8.1.4:

<?php

class X {
}
class Y {
}
class Z extends Y {
}
class U {
}

function foo($param) {
        $allowed_classes = ['X','Y'];
        $reflect = new ReflectionClass($param);
        $class_name = $reflect->name;
        $parent_class_name = '';
        if($parent_class = $reflect->getParentClass()){
                $parent_class_name = $parent_class->name;
        }
        if(in_array($class_name, $allowed_classes) || in_array($parent_class_name, $allowed_classes)){
                echo "will work\n";
        } else {
                echo "won't work\n";
        }
}

foo(new X); //will work

foo(new Z); //will work

foo(new U); //won't work
Related