Resolving Types - Structure - Typed PHP, Stronger Types for Cleaner Code

Typed PHP, Stronger Types for Cleaner Code (2014)

Structure

Resolving Types

PHP is a weakly typed language. That is, variables can be declared without specifying a type. Their type can be changed at any point. They can be coerced into different types on demand.

If we want to implement stronger type-handling, we need to be able to identify the type of a variable. PHP provides a number of functions which help with this, but they have a fe issues to overcome…

Scalar Types

The gettype function returns the data type of a variable. It can identify the following types:

· "boolean"

· "integer"

· "double"

· "string"

· "array"

· "object"

· "resource"

· "NULL"

If none of these types correctly describes the variable, gettype will return "unknown type". Again we see some strange inconsistencies. Firstly, floats are identified as "double" and null values are returned as "NULL" (uppercase, in contrast to every other type identifier).

We can abstract around this function, with something resembling the following:

1 <?php

2

3 function type($variable) {

4 $type = gettype($variable);

5

6 switch ($type) {

7

8 case "integer":

9 case "double":

10 return "number";

11

12 case "NULL":

13 return "null";

14

15 }

16

17 return $type;

18 }

19

20 print type(0); // "number"

21 print type(.0); // "number"

22 print type(null); // "null"

What about when the variable contains a callback? In that case, we can use…

Functions

The is_callable function checks to see if something is a callable function, whether it is a string or a anonymous function.

1 <?php

2

3 print is_callable("is_callable"); // true

4 print is_callable(function(){}); // true

5 print is_callable(null); // false

6

7 class Foo

8 {

9 public function bar()

10 {

11

12 }

13

14 public function identify()

15 {

16 return is_callable([$this, "bar"]);

17 }

18 }

19

20 $foo = new Foo();

21

22 print $foo->identify(); // true

Technically, is_callable identifies a valid argument to any callback-accepting function in PHP. The following are all valid for these kinds of functions:

· An actual function (function(){})

· An array of context and method name ([$this, "bar"])

· The name of a function, as a string ("is_callable")

Due to how loose this method is, you need to be careful when trying to identify strings and functions in the same resolver function. If you’re using is_string and is_callable at the same time, your results may differ depending on the order in which these are called.

For example:

1 $variable = "is_callable";

2

3 if (is_string($variable)) {

4 die("variable is a string");

5 }

6

7 if (is_callable($variable)) {

8 die("variable is callable");

9 }

The script will terminate with the string "variable is a string", because it is a string. But it’s also the name of a callable function, so swapping the conditional statements will make the script terminate in "variable is callable".

If you want to get more specific, then it’s helpful to know that gettype(function(){}) will return "object". In that case, we can do this:

1 <?php

2

3 function is_function($variable) {

4 return is_callable($variable) and gettype($variable) === "object";

5 }

6

7 print is_function(function(){}); //true

8 print is_function("is_function"); //false

9 print is_function(new stdClass); //false

Classes

Sometimes what we want to do is identify the type of object we’re working with. gettype won’t tell us much more than that the variable is an object. We’ll have to combine that with the get_class function:

1 <?php

2

3 function type($variable) {

4 $type = gettype($variable);

5

6 switch ($type) {

7

8 case "integer":

9 case "double":

10 return "number";

11

12 case "NULL":

13 return "null";

14

15 case "object":

16 return get_class($variable);

17

18 }

19

20 return $type;

21 }

22

23 print type("foo"); // "string"

24

25 class Foo

26 {

27

28 }

29

30 $foo = new Foo();

31

32 print type($foo); // "Foo"

33 print type(function(){}); // "Closure"

We could further normalise "Closure" to something like "function"; if we wanted to treat it with the same gravity as the other types.

Regular Expressions

Since PHP has multiple sets of string functions (one for plain strings and a few for regular expressions), it would be great if we had a way to differentiate between things that look like regular expressions and things that don’t.

It’s important to differentiate between definitively identifying regular expressions and identifying things that look like. Just because something looks like a regular expression doesn’t mean that it is. Nor does it mean that the intention of the author was for it to be a regular expression.

The PHP documentation (at http://www.php.net/manual/en/intro.pcre.php) describes expressions as [a string] enclosed in delimiters. These delimiters can be any non-alphanumeric that aren’t also a backslash or null byte.

We can cover a large majority of cases, using the following:

1 <?php

2

3 function is_regex($variable) {

4 return @preg_match($regex, "") !== false

5 && preg_last_error() == PREG_NO_ERROR;

6 }

7

8 print is_regex("/^.*$/"); // true

9 print is_regex("/hello world/"); // true

10 print is_regex("/hello world/i"); // true

11 print is_regex("hello world"); // false

12 print is_regex("\\hello world\\"); // false

13 print is_regex("\\x00foo\\x00"); // false

14 print is_regex("1foo1"); // false

15 print is_regex("afooa"); // false

The preg_match function returns a 1 for a match, 0 for no match and false if an error occurred. Assuming preg_match returns false, the error could be for any number of reasons (like PREG_INTERNAL_ERROR or PREG_BAD_UTF8_ERROR). So then we use the preg_last_error function to rule out all other errors.

Using error suppression (@) is generally a bad idea. There are very few instances when using it is considered ok. This is one of them!

This isn’t enough for us to simulate an extensible type system, or this would be a short book.