2
\$\begingroup\$

As you're probably aware, PHP doesn't support type hinting for scalar types. I feel this might be good practice to build into my framework, although I'm aware some of you might disagree seeing as PHP is a loosely-typed language.

But I'd like some feedback on the implementation of this. Do you think it's efficient, pointless? Or could it be achieved in a better way?

This works by causing an error. By specifying a typehint (integer), PHP is expecting a class/object called "integer", so it's giving an error as we are passing a "real integer"/scalar value".

Giving this error,

Argument 1 passed to test() must be an instance of integer, integer given.

So with a custom error handler function, I can use preg_match() to find the expected & given types, and just compare them via the name.

The code

<?php

ini_set('display_errors', 'on');
error_reporting(E_ALL);

function typehint($level, $message){

    if($level == E_RECOVERABLE_ERROR){ 

        if(preg_match('/^Argument (\d)+ passed to (?:(\w+)::)?(\w+)\(\) must be an instance of (\w+), (\w+) given/', $message, $match)){ 
            if($match[4] == $match[5]){
                return true;
             }
        }                 
    } 

    return false; 
} 

set_error_handler('typehint');

function test(integer $value){
    var_dump($value);   
}

for($int = 0; $int < 1000; $int++){
    test(23);
}

?>

It seems to work ok, but I'm not sure if using preg_match() on every function argument is a good idea?

Here's the benchmarks, on 1000 function calls. (1 typeint per function)

Type-hinting        No Type-hinting
0.015               0.0017
0.0169              0.002
0.0172              0.002
0.0148              0.0019
0.0159              0.0029
0.0157              0.0019
0.0149              0.0018
0.0161              0.0029
0.0167              0.002
0.0173              0.0018
0.0149              0.0019
0.0156              0.0019
0.0153              0.0029
0.0182              0.0023
0.0151              0.0029
0.0163              0.0017
0.0179              0.0018
0.0159              0.0018
0.0154              0.0027
0.0149              0.0023

Average             Average
0.016               0.002155

So just after some thoughts on this really... good or bad?

EDIT: Using strstr() seems to be a little quicker, although I have to specify what types to check for. Which is a small number anyway. Average time: 0.009595.

function typehint($level, $message){

    if($level == E_RECOVERABLE_ERROR){ 

        $integer = 'must be an instance of integer, integer given';
        $string = 'must be an instance of string, string given';

        if(strstr($message, $integer) !== false OR strstr($message, $string) !== false){
            return true;
        }
    } 

    return false; 
} 
\$\endgroup\$

2 Answers 2

7
\$\begingroup\$

"So just after some thoughts on this really... good or bad?"

Bad, really bad, I'm afraid. You're trying to make the language do something it doesn't want to do, or even: can't do. You're gluing feathers to a fish's fins, and throwing it up into the air, so it can mimic a bird's behaviour.
Your test code shows a dramatic decrease of speed (after switching to strstr, still ~5 times slower than usual). And you're only testing your code with valid input. Besides, have you even checked if you can get this function to return what it needs to return, and to Where?

The simple fact of the matter is this: each function call with your improvised (hacked) type-hints will issue an error, even if the type is correct.
It will, however, issue a notice in these cases, too:

function test(integer $foo)
{
    return $foo;
}
test('123');

How will you recover from this error? Are you going to add:

if (settype($argument, $expected) == $argument)
{
    call_user_func_array($function, array($argument));
}

to your error handler? and how are you going to deal with functions that take arrays of ints, or what if the user passes null?
There's just too many things to consider here.

Your approach also poses problems when you scale things up a little: Have you thought about what would happen if you used this code in tandem with a framework, that uses namespaces, sets all sorts of handlers all over the place, and uses complicated autoloading trickery?
Even simple autoloading would mean that PHP sets out to look for this integer class. That means calling the autoloader, when that turns out to be unsuccessfull, PHP will of course use the include paths, and a lot of I/O disk access is the result. Disk I/O is a speed killer, as we all know.

Note that lookups of files that aren't found are not cached!, so each type-hint will result in disk access. Whomever is hosting this code, if it ever got used in even a medium traffic site, will not be happy.
Perhaps this has changed with PHP5.5, but most hosting services are yet to upgrade.

Each function call, in your simple example, is already approx. 5 times slower than a normal one. Add the overhead of a custom autoloader, several layers of error/exception handlers to that, and you'll see the avg time per function call drop even more (probably converging on the initial times you had). Simply because autoloaders often access the disk, too, with file_exists calls, using the include path to scan for files who's name resembles that of the class that needs to be loaded, only to fail, and have PHP do the same disk operations again.
Then, like the example I listed: getting ints from a db, or user input means that these numbers are all passed around as strings, but are perfectly castable/usable as integers.

Suppose you were able to get the called function, cast the arguments accordingly and call the function again, what would happen? Well, the function gets called again, so another error is triggered, and your handler, which is now the caller of the function will be called twice.
Of course, the second time, owing your intervention, the types will match this time, but that's 4 function calls (5 including call_user_func_array) + 2 errors being raised to complete a single call:

Original caller           /->fails, PHP====\\
 \\              //``!AUTOLOADER!``<=\\    ||
  \\             ||                   \\   \/
   \=======> function -------> error   \==>DISK I/O !!TWICE!! per call
    \|            /\              \\
    ??            ||               \==> handler, type checking + casts
    ||            ||                    ||   /\
    ||            ||call_user_func_array||    |
    ??            |======================|    |
    ||                 \\    error2           |
    ||                  \----------------------
    ||                         ||
    ??                         \/
    ||      suppose it returns, still disk I/O and
    ||                                         ||
    ||           good luck getting back there  //
    \\=========================================/

An that's just assuming no intern gets the brilliant brain-wave to actually create a string or integer class or interface!

Even if you manage to get all of this working, a recoverable error isn't guaranteed to have anything to do with your special-case type-hints. Your handler was created with a specific type of error in mind, which -logic dictates- should be an E_USER_NOTICE.

Of course, you can't force the language to emit such an error when a non-custom error is encountered.

Ah well, you could try to work on this a bit more, if you want to, but I'd say you're better of spending that time to refactoring your functions like so:

/**
 * my test func - EXPECTS INTEGER
 * Any decent IDE uses these doc-blocks
 * @param int $integer
 * @return int
 **/
function test($int)
{
    $int = (int) $int;//cast to be sure
    //code
    return ++$int;
}

That'd be a better way to spend your time, and just ensure people you work with have a decent IDE, that parses the doc-blocks.

\$\endgroup\$
1
\$\begingroup\$

Yes, Now its possible, After a long discussion, a proposal to implement type hinting for scalar function parameters and return values was just approved with the highest vote count so far, check for details :

Scalar type hinting consists of declaring the types of function parameters and return values that can be of the types int, float, string and bool.This allows the PHP runtime engine to check if the types of values passed to parameter functions and return values are of the expected types in order to detect eventual programming mistakes. Type hinting for objects, arrays and callables was already allowed in past PHP versions. The current implementation introduces five new reserved words: int, float, bool, string and numeric. These were not previously reserved, because casting is a special case in the lexer.

Example :
function test(float $a) {
    var_dump($a); 
}

test(1); // float(1)
test("1"); // float(1)
test(1.0); // float(1)
test("1a"); // E_RECOVERABLE_ERROR
test("a"); // E_RECOVERABLE_ERROR
test(""); // E_RECOVERABLE_ERROR
test(1.5); // float(1.5)
test(array()); // E_RECOVERABLE_ERROR
test(new StdClass); // E_RECOVERABLE_ERROR

You have also an option to declare into source file where you can allow Scaler type hinting.It must be 1st line of your config script and can’t be declared elsewhere in the same file.

Like : declare(strict_types=1); At runtime, when the PHP engine tries to return a value, it will check if doesn’t match as declared it will throw a fatal error like, Fatal error: Argument 1 passed to increment() must be of the type integer, string given

With this new features of declaration, you can write more robust applications by detecting early programming mistakes caused by passing values of the wrong types to functions.

Automatic changes of types may also happen. For example, int types can be change into float type parameters automatically,

function test(float $x){
    var_dump($x);
}
test(10); // works fine

Declaring the Return Type

We can declare the return types adding a colon followed by the expected type between the last parenthesis and the first bracket in the function declaration.

For functions that do not return any value, nothing should be added in the return type declaration section.

function mustReturnInt(): int { ... }
function mustReturnString(): string { ... }
function mustReturnBool(): bool { ... }
function mustReturnFloat(): float { ... }
function doesNotReturnAnything() { ... }

A Little Bit more Complex Example

declare(strict_types=1);  
class StrictTypesTestingClass {  
public function returnSameInt(int $value): int {   return $value;  }   
public function returnSameFloat(float $value): float {   return $value;  }  
public function returnSameString(string $value): string {   return $value;  }   
public function returnSameBool(bool $value): bool {   return $value;  } }  
$check = new StrictTypesTestingClass();  // calls that work  print $check->returnSameInt(10); 
print $check->returnSameFloat(10.0); 
print $check->returnSameString("test"); 
print $check->returnSameBool(true) ? 'true' : 'false';  // calls that throw exceptions 
print $check->returnSameInt("10"); 
print $check->returnSameFloat("10.0"); 
print $check->returnSameString(10);
print $check->returnSameBool("true");

Behavior of Weak Type Checking and Type Conversion : The weak type checking mode can be used with the statement declare(strict_types=0); or the absence of the strict types declaration. There are a few of points to take into account: Weak type checked calls to an extension or built-in PHP function have the same behaviour as in previous PHP versions The weak type checking rules for new scalar type declarations are mostly the same as those of extension or built-in PHP functions. NULL is a special case in order to be consistent with the current type declarations for classes, callables and arrays. NULL is not accepted by default, unless it is a parameter and is explicitly given a default value of NULL, for instance: function sample(int $a = NULL);

There are a lots of advantages to this approach. You get type safety. Which means that you can finally statically analyze code! You can detect bugs where you accidentally take a string from one function and pass it as an integer to another.For me, a developer that uses PHP on a daily basis and sees Java as a reference for OOP languages, this is great progress for PHP.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Great to see that scalar type-hinting is coming, but would you mind awfully linking to the actual proposal/vote page, so people can read what was discussed, and can see when it'll be added (probably PHP-NG/PHP7, but who knows...) \$\endgroup\$ Mar 23, 2015 at 7:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.