How does PHP handle type declarations?

Question:

Even in PHP 5 it was already possible to declare types in function arguments .

Type declarations

Type declarations allow functions to require parameters to be of certain types when calling them. If the value given in the parameter has an incorrect type then an error is generated: in PHP 5 it will be a recoverable fatal error, whereas in PHP 7 it will throw a TypeError exception

To declare the type its name must be added before the parameter name. The declaration can be made to accept NULL if the parameter's default value is also set to NULL.

As of PHP 7 it is possible to declare the return type of a given function.

Return Type Declaration

PHP 7 adds return type declaration support. Similar to the argument typing declaration, the return type declaration specifies the type of value that will be returned from a function. The same types that are available for declaring arguments are available for typing returns.

Strict typing also affects return typing. In standard mode (weak trimming) the returned values ​​will be converted to the correct type if they do not fit the type informed. In strong typing mode the returned values ​​must be the correct type or a TypeError exception will be thrown.

However, in the documentation, nothing is said about the need for the type to be declared or not in the current scope. I did the test and realized that PHP doesn't check if the type is declared and still has the same behavior mentioned above.

function foo(): Foo {
    return 'foo';
}

foo();

The output will be the TypeError exception:

Return value of foo() must be an instance of Foo, string returned

How, then, does the language check whether the return is an instance of Foo without the Foo class existing? Evaluating the behavior of the instanceof structure, we can see that it accepts verification from just the class name, such as string .

$obj = 'foo';
$class = 'Foo';

var_dump($obj instanceof $class);  // bool(false)

Is this the behavior used in checking argument and return types? Does the interpreter store the type as a string internally and check from the name only?

Answer:

Looking in the PHP source code , more specifically in the file Zend/zend_execute.c , there is a function zend_verify_internal_return_type , which checks what is the return of a PHP function, and, inside it, the function zend_check_type is called, which checks the returned value with the role type, the relevant part of the question is:

if (ZEND_TYPE_IS_CLASS(type)) {
    if (EXPECTED(*cache_slot)) {
        *ce = (zend_class_entry *) *cache_slot;
    } else {
        *ce = zend_fetch_class(ZEND_TYPE_NAME(type), (ZEND_FETCH_CLASS_AUTO | ZEND_FETCH_CLASS_NO_AUTOLOAD));
        if (UNEXPECTED(!*ce)) {
            return Z_TYPE_P(arg) == IS_NULL && (ZEND_TYPE_ALLOW_NULL(type) || (default_value && is_null_constant(scope, default_value)));
        }
        *cache_slot = (void *) *ce;
    }
    if (EXPECTED(Z_TYPE_P(arg) == IS_OBJECT)) {
        return instanceof_function(Z_OBJCE_P(arg), *ce);
    }
    return Z_TYPE_P(arg) == IS_NULL && (ZEND_TYPE_ALLOW_NULL(type) || (default_value && is_null_constant(scope, default_value)));
} else if [ ... ]

Note: the default_value variable value will always be null, it is only used in the type checking of the function parameters, so the expression in parentheses after the logical OR will always be false. Just like the cache_slot will always be null and the content inside the EXPECTED(*cache_slot) will never be executed. Both variables are always null because inside the zend_verify_internal_return_type function is called zend_check_type passing always NULL :

static int zend_verify_internal_return_type(zend_function *zf, zval *ret)
{
    zend_internal_arg_info *ret_info = zf->internal_function.arg_info - 1;
    zend_class_entry *ce = NULL;
    void *dummy_cache_slot = NULL;

    if (ZEND_TYPE_CODE(ret_info->type) == IS_VOID) {
        if (UNEXPECTED(Z_TYPE_P(ret) != IS_NULL)) {
            zend_verify_void_return_error(zf, zend_zval_type_name(ret), "");
            return 0;
        }
        return 1;
    }

    if (UNEXPECTED(!zend_check_type(ret_info->type, ret, &ce, &dummy_cache_slot, NULL, NULL, 1, 0))) {
        zend_verify_internal_return_error(zf, ce, ret);
        return 0;
    }

    return 1;
}

Note that this function is already checked if the type is void and the return is NULL

If the function's type is a class, then it looks for the class, as the class doesn't exist, the line will be executed:

return Z_TYPE_P(arg) == IS_NULL && (ZEND_TYPE_ALLOW_NULL(type) || (default_value && is_null_constant(scope, default_value)));

Which will check if the return type is NULL and if the function accepts the null return, it's not the case, then this function will return false

Now, if the class is found and the return type is an object, then the instanceof_function function is used to check if the return is an instance of the found class

So, even if you create a function and it returns any object, PHP will never throw an exception because the type hasn't been declared yet, instead it will throw an exception of type TypeError :

function foo(): Foo {
  return new DateTime();
}

foo();

PHP Fatal error: Uncaught TypeError: Return value of foo() must be an instance of Foo, instance of DateTime returned in […]

Even if the defined type doesn't exist, but the function can return NULL (prefixing the type with a question mark ? ) and actually return NULL , the code will execute successfully:

function bar(): ?Bar {
  return null;
}

bar();

Allowing something like:

function baz(arrray $args): ?Baz {
  return class_exists('Baz') ? new Baz(...$args) : null;
};

baz($args);

Which checks if the class exists and, if so, returns its instance, if not, returns null

Scroll to Top