How does PHP handle type declaration?

Asked

Viewed 264 times

20

Still in PHP 5 it was already possible to make the declaration of types in arguments of function.

Type statements

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

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

From PHP 7 it is possible to declare type of return of a particular function.

Declaration of type of return

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

Strict typing also affects return typing. In standard mode (weak casing) the returned values will be converted to the type correct if they do not fit the type reported. In strong typing mode the returned values must be the correct type or an exception Typeerror will be launched.

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 I realized that PHP does not check if the type is declared and I still try the same behavior above.

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

foo();

The exit will be the exception TypeError:

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

How, then, the language checks whether the return is an instance of Foo without the class Foo is there? Evaluating the behavior of the structure instanceof we can realize that it accepts verification from only the class name, as string.

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

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

Is this the behavior used in checking argument types and returns? The interpreter stores internally the type as string and checks only from the name?

  • 1

    Very good question, also would like to know this in more detail.

  • 1

    This is a very interesting question. Could you please publish the question at https://bugs.php.net/report.php and give a more detailed assessment of the case?

  • Anderson, I think the assessment is wrong. Your var_dump demonstrates that $obj is not instance of Foo. Similarly, the TypeError launched is correct, since the initial validation (string !== (instance of Foo)) occurs, it is not necessary for the interpreter to check whether the Foo class exists or not. The class existence error would occur if the return is return new Foo.

  • @Rafaelaraújo But that’s exactly the question, about how PHP analyzes the type of return even when the type is not defined. I didn’t understand what the wrong assessment would be.

  • @Andersoncarloswoss the first check is retornoDaFuncao === retornoDefinidoDaFuncao. This verification is done via the metada of the data type name and therefore it is not necessary that the data type actually exists.

  • @Rafaelaraújo This would not be the answer? What is the wrong assessment you say I asked in the question? The var_dump at the end was to show that the instaceof works with strings and there doesn’t have to be the type to do that check. Soon after that I asked if this is how the interpreter does internally, if it stores the type of return as string to make the check later. If you know how it works internally, please prepare a reply :D

  • All right. I’m gonna throw it in response. ;-)

  • 1

    Related: https://answall.com/q/93707/101

Show 3 more comments

2 answers

3


Searching the PHP source code, more specifically in the archive Zend/zend_execute.c, there is a function zend_verify_internal_return_type, which verifies what is the return of a PHP function, and within it is called the function zend_check_type, that checks the value returned with the type of function, the relevant part for 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 variable value default_value will always be null, it is only used in checking the type of the function parameters, then the expression in brackets after the logical OR will always be false. Just like the cache_slot will also always be null and content within the condition EXPECTED(*cache_slot) will never be executed. Both variables are always null because within the function zend_verify_internal_return_type is called zend_check_type always passing 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 in this function it is already checked if the type is void and the return is NULL

If the type of the function is a class, then it looks for the class, as the class does not 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 is not your case, then this function will return false

Now, if the class is found and the return type is an object, then the function instanceof_function 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 make an exception because the type has not yet been declared, instead it will make an exception of the 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 does not exist, but the function can return NULL (prefixing the guy with a question mark ?) and really return NULL, the code will be executed successfully:

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

bar();

Allowing for 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 yes, returns its instance, if no, returns null

1

Hi, Anderson.

Yes, in this step of compatibilization of the signature with the return, the language checks if the return is an instance of Foo without the Foo class exists.

Considering the moment from which we are at the time of return of the function, ie, we have already successfully passed both the execution of the function input and the body of the function successfully, and we are at the step where the function is returning the result of its execution:

In return, the interpreter will query the metadata of the data type definition being returned. In your example, the metadata of what will be returned is of the type string, an internal type, and it will be compared with the signature metadata of the method, which is defined as Foo, an object. These structures are compared and, because they are incompatible, the interpreter already launches the type error.

Since there is anyway an incompatibility, this step is carried out without the need to verify the existence of Foo in the system in order to save resources, because if compatible (both are objects, for example), then the interpreter would have to compare the objects in all the complexity of the chair (inheritance, implementation of interfaces, etc).

  • 4

    But how is this comparison made? Can you demonstrate with any official source (documentation/code) how this happens? Because I opened the question precisely because it is not clear how this process is internally.

Browser other questions tagged

You are not signed in. Login or sign up in order to post.