phptypingtypeoftype-testing

Check if variable satisfies type definition in string


Is there a way to test if a $var satisfies a type defined in a string other than this:

function test_type(mixed $value, string $type): bool 
{
    try { 
          eval('(fn('.$type.' $a)=>true)($value);'); 
          return true;  
    }  
    catch (Throwable  $e) { 
          return false; 
    }
}
test_type(null,  "int") ; // returns false. 
test_type(null,  "?int") ; // return true. 
test_type("hello, world", "bool") ; // returns true. I want it to return false
test_type(new DateTime, "DateTimeInterface") ; // return true. 
test_type(new DateTime, "null|int|DateTime|DateTimeImmutable") ; // return true

There are two major problems with this approach that do not need further explanation:

Is there a way to avoid these problems? I've Googled a lot but haven't found anything up to now.


Solution

  • @Jaquarh convinced me it was way easier than I thought it would be by just testing all the types and unions and intersections. His version was the basis for this solution. Thanks, Jaquarh!

    function test_type(mixed $value, string $type): bool
    {   if ( $type == 'mixed' ) return true;
        $foundType =  strtolower(gettype($value)); 
        $foundType = ['boolean'=>'bool', 'integer'=>'int', 'double'=>'float'][$foundType] ?? $foundType;
        
        $type = str_replace('?', 'null|', $type);
        $typesUnion = explode('|', $type);
        $typesIntersection = explode('&', $type);
        if ( count($typesIntersection)>1) {
            if (  ! is_object($value) ) return false;
            foreach($typesIntersection as $subtype) {
                if ( ! ($value instanceof $subtype)) return false;
            }
            return true;
        }
    
        foreach($typesUnion as $subtype) {
            if(match(strtolower($subtype)) {
                 $foundType => true,
                 'false'    => $value === false,
                 'true'     => $value === true,
                 'callable' => is_callable($value),
                 default    => is_object($value) &&  $value instanceof $subtype
                }) return true;
        }
        return false;
    }
    
    enum hello { case world; case earth; case mars; };
    
    echo (int) test_type(false,  "int|bool");                                   // 1
    echo (int) test_type(null,  "?int");                                        // 1
    echo (int) test_type(null,  "int|string");                                  // 0
    echo (int) test_type("hello, world", "?bool");                              // 0
    echo (int) test_type(hello::world, "hello");                                // 1
    echo (int) test_type(false, "DateTimeInterface|false");                     // 1
    echo (int) test_type(true, "DateTimeInterface|false");                      // 0
    echo (int) test_type(new DateTime, "DateTimeInterface|false");              // 1
    echo (int) test_type(new DateTime, "null|int|DateTime|DateTimeImmutable");  // 1
    echo (int) test_type(new DateTime, "DateTime&DateTimeInterface");           // 1
    echo (int) test_type([1,2,3,4], "Array");                                   // 1
    echo (int) test_type(fn($x) => $x+1, "callable");                           // 1
    echo (int) test_type($x = fopen('php://output', 'w'), "object|resource");   // 1
    

    https://onlinephp.io/c/59f73