A case study of a simple PHP function
Recently, on one of Discord servers an example of how to reverse a string in PHP was posted. Because the example was presented as a production code example, let's take a look and discuss it. Looking ahead, that wasn’t the best example.
function reverse_string($str) {
return strrev($str) . PHP_EOL;
}
What is conceptually wrong with it? To me, the main problem lies in the argument type. Let me explain.
As you probably know, all programming languages may be categorized by type checking (static/dynamic) and by treating types (strong/weak). If this doesn’t ring a bell to you, please conduct your own investigation – these concepts are crucially important to be aware of.
Going back to the example. For years, PHP was a dynamically typed language with weak type checking. But as time went on and the language evolved, PHP gained the ability to behave as a statically typed language in some cases. These cases are limited to type checking of function/method arguments and type checking of return values. This ability is activated with the strict_types
directive and this is an interesting one. It is not only applied to the file where it was declared, but:
By default, PHP will coerce values of the wrong type into the expected scalar type declaration if possible. For example, a function that is given an int for a parameter that expects a string will get a variable of type string.
It is possible to enable strict mode on a per-file basis. In strict mode, only a value corresponding exactly to the type declaration will be accepted, otherwise a TypeError will be thrown. The only exception to this rule is that an int value will pass a float type declaration.
Note: Strict typing applies to function calls made from within the file with strict typing enabled, not to the functions declared within that file. If a file without strict typing enabled makes a call to a function that was defined in a file with strict typing, the caller's preference (coercive typing) will be respected, and the value will be coerced.
Note: Strict typing is only defined for scalar type declarations.
Source: https://www.php.net/manual/en/language.types.declarations.php
Why do I mention this ability? Let’s run an experiment and provide a large set of different inputs to this function.
Note I'm going to use version 8.2 of PHP (the results of the experiment may vary depending on the version):
declare(strict_types=0); // strict_types are disabled
echo 'for string: ' . reverse_string('string'); // gnirts
echo 'for number string: ' . reverse_string('42 string'); // gnirts 24
echo 'for integer: ' . reverse_string(42); // 24
echo 'for float: ' . reverse_string(42.2); // 2.24
echo 'for null: ' . reverse_string(null); // empty result and Warning
echo 'for true: ' . reverse_string(true); // 1
echo 'for false: ' . reverse_string(false); // empty result
echo 'for array: ' . reverse_string([]); // Fatal error in strrev()
echo 'for object: ' . reverse_string(new stdClass()); // Fatal error in strrev()
echo 'for resource: ' . reverse_string(fopen('php://memory', 'r')); // Fatal error in strrev()
function reverse_string($str) {
return strrev($str) . PHP_EOL;
}
Well, if this function is not covered by any tests, we are in big troubles. Even worse, we don’t even know what to expect from this function during runtime (if the code base is quite large, it is impossible to control and guarantee that this function is going to get strings only). Probably type-hinting is going to help us? Let’s see:
declare(strict_types=0); // strict_types are disabled
echo 'for string: ' . reverse_string('string'); // gnirts
echo 'for number string: ' . reverse_string('42 string'); // gnirts 24
echo 'for integer: ' . reverse_string(42); // 24
echo 'for float: ' . reverse_string(42.2); // 2.24
echo 'for null: ' . reverse_string(null); // Fatal error in reverse_string()
echo 'for true: ' . reverse_string(true); // 1
echo 'for false: ' . reverse_string(false); // empty string
echo 'for array: ' . reverse_string([]); // Fatal error in reverse_string()
echo 'for object: ' . reverse_string(new stdClass()); // Fatal error in reverse_string()
echo 'for resource: ' . reverse_string(fopen('php://memory', 'r')); // Fatal error in reverse_string()
function reverse_string(string $str): string {
return strrev($str) . PHP_EOL;
}
This is a little bit better – it doesn’t accept null
value anymore. The IDE started to warn us about wrong types. But more importantly, notice that the place where the error was thrown has changed. Now, we’ve got more control over the situation and we know the exact reason why this error happened (you don’t need to read the documentation and check the signature of strrev()
anymore). But let’s see if we can even improve it.
declare(strict_types=1); // strict_types are enabled
echo 'for string: ' . reverse_string('string'); // gnirts
echo 'for number string: ' . reverse_string('42 string'); // gnirts 24
echo 'for integer: ' . reverse_string(42); // Fatal error in reverse_string()
echo 'for float: ' . reverse_string(42.2); // Fatal error in reverse_string()
echo 'for null: ' . reverse_string(null); // Fatal error in reverse_string()
echo 'for true: ' . reverse_string(true); // Fatal error in reverse_string()
echo 'for false: ' . reverse_string(false); // Fatal error in reverse_string()
echo 'for array: ' . reverse_string([]); // Fatal error in reverse_string()
echo 'for object: ' . reverse_string(new stdClass()); // Fatal error in reverse_string()
echo 'for resource: ' . reverse_string(fopen('php://memory', 'r')); // Fatal error in reverse_string()
function reverse_string($str) {
return strrev($str) . PHP_EOL;
}
Oh, this is nice! Even though the PHP language is not a compiled language, the benefits are obvious. The function has become more predictable, it doesn’t accept any wrong data anymore, and it conforms to the Fail fast principle. In addition, we get full control over the place where the error happens.
To summarize. We were not talking about whether the given function is wrong or not. The main point of this post is that you need to calculate ahead possible consequences of certain decisions (trade-offs) and be aware of the problems and tricky situations related to the dynamic PHP nature. The strict_types
directive will help you to gain control over these situations and make the behavior of your code more predictable.