Type declarations are a simple programming concept that lots of developers use on a daily basis. In the code example below we see a basic method that uses type declarations. The method places a string type hint on the $foo
parameter and a string return type on the method itself.
function bar(string $foo): string
{
return $foo;
}
$result = bar('Hello');
var_dump($result);
// Output: string(5) "Hello"
These type declarations tell us two things about the method. It accepts one string argument and it will return a string. In a strictly typed language like Go if the bar()
method is called and an integer argument is passed in, eg 123, an error will occur. The code will also error if the method returns something other than a string for some reason.
Type declarations serve two basic purposes in code:
- Low-level data / code integrity: it is more difficult to misuse methods and type checking is reduced.
- Code readability: it is clearer what a method accepts and what it will return.
PHP has historically been a weakly typed language. Type declarations were only introduced in PHP 5, and in a limited form. Full type declarations and strict types did not appear until PHP 7's release in 2015.
PHP 7 was an exciting step forward for those of us who like the concept of type declarations and strictly typed code. There are though two important caveats that PHP developers should be aware of.
PHP 7 is a Weakly Typed Language
By default PHP 7 remains a weakly typed language. This may seem counterintuitive given the introduction of full type declarations and strict types. But it was decided to aid backwards compatibility that type declarations would not be forced on PHP code. Developers can turn strict types on by placing the declare(strict_types=1);
method at the top of a PHP file.
This implementation means that PHP will 'ignore' type hints and return types unless the declare(strict_types=1);
statement appears at the top of the file. This makes some sense as there is a lot of legacy PHP code still in use. And developers should be able to upgrade to the latest version of PHP without rewriting their entire codebase.
This approach does have some consequences though. If a developer defines type declarations but does not add declare(strict_types=1);
PHP will use type coercion to make things work. Type coercion basically means a value of one type will be cast to a value of another type when required. This is less than ideal when using type declarations as methods may not function as expected.
As an example, if we edit our 'foo bar' code from above we'll see the type coercion at work. The bar method will now have an integer type hint on the $foo
parameter and a string return type on the method. When we call the bar method with an integer argument of 123 the method will return 123 as a string. This is because type coercion has converted the integer value to a string value to comply with the defined return type.
function bar(int $foo): string
{
return $foo;
}
$result = bar(123);
var_dump($result);
// Output: string(3) "123"
It's only when we add declare(strict_types=1);
to the top of the PHP file that the type declarations are imposed.
declare(strict_types=1);
function bar(int $foo): string
{
return $foo;
}
bar(123);
Now when we call the bar method with an integer argument of 123 we get an error. This is because the method expects to return a string but it is attempting to return an integer. Strict types have been imposed on the method and the following error is outputted.
FATAL ERROR Uncaught TypeError: Return value of bar() must be of the type string, integer returned
Strict Types Are Imposed on a File by File Basis
The other caveat to be aware of is strict types have to be imposed on a file by file basis, they cannot be applied globally. This means you have to place declare(strict_types=1);
in every file where you want strict types to be imposed. Again this has been done to help backwards compatibility, but it means you cannot easily impose strict types across an application.
To show this in action let's look at another example. Imagine we have a small application that pulls in two functions from two separate files, foo.php
and bar.php
. We'll add a declare(strict_types=1);
to the top of the application file just to prove it has no effect on the files pulled in. Also we'll place the methods we call inside a try catch to handle any errors that are thrown.
/**
* This declares strict types but has no effect on the code.
*/
declare(strict_types=1);
/**
* Call in the foo.php and bar.php files.
*/
require __DIR__ . '/../src/foo.php';
require __DIR__ . '/../src/bar.php';
try {
/**
* This method will output a string 123.
*/
var_dump(foo(123));
/**
* This method will throw an error.
*/
var_dump(bar(123));
} catch (Error $e) {
/**
* Print out the error message thrown by the bar(123) method
* "Return value of bar() must be of the type string, integer returned"
*/
echo $e->getMessage() . PHP_EOL;
}
In the foo file we'll have a foo method that accepts an integer and returns a string. In this file there will be no declare(strict_types=1);
statement.
function foo(int $bar): string
{
return $bar;
}
In the bar file we'll have a bar method that also accepts an integer and returns a string. This time though we'll have a declare(strict_types=1);
at the top of the file.
declare(strict_types=1);
function bar(int $foo): string
{
return $foo;
}
When we call the foo(123)
and bar(123)
methods respectively we'll get some very different output. The foo method will return a string so we'll see an output of string(3) "123"
. By contrast the bar method will throw an error because strict types have been imposed. We'll see an error message outputted, Return value of bar() must be of the type string, integer returned
. This example shows strict types can be both on and off in the same application.
The Consequences
I don't wish to get into the rights and wrongs of PHP's approach to type declarations, smarter people than I have designed and implemented them. But the consequences of this approach should be obvious. The most important is PHP's type system cannot be trusted. And this means low level integrity errors can slip into code unless you are meticulous with declare(strict_types=1);
statements.
It also means you should be careful when testing code. For instance when unit testing with PHPUnit you should use assertSame()
instead of assertEquals()
. This is because assertSame()
checks value and type where as assertEquals()
only checks value. And if you want to ensure code integrity you need to check type as well as value.
Type declarations are a big step forward for PHP and I believe all PHP developers should use them whether declare(strict_types=1);
is on or off. It is though important that PHP developers understand how they work as misunderstanding PHP's type system will lead to bugs and will have negative effects on applications.
For more information on PHP's type system I suggest you read the RFC on PHP scalar type declarations.
Top comments (12)
Even though I sometime use type declarations, I try to prevent myself to start using them in every single scenario. An Example of where I mostly reach for type declarations is this:
I think in cases like the top one it helps for readability.
I feel sometimes that when I reach for so much type hints and return types, I'm making PHP something more Like Java (And I really don't enjoy Java).
It's just my way of thinking. Totally respect your point of view also. 😊
I think this often depends on your code background. I got into PHP after I'd written more strongly typed languages like c#. So I've always felt comfortable around type hints, etc.
Absolutely agree with you.
then how can you tell what to pass to functions?
lol, your talking like the developer is a kid. I’m expected to have a certain domain over the codebase. 😁
Until you don't.
This is debatable.
Have you looked into using Haxe as a possible way to have strict typing while still being able to target PHP?
Granted type checking only happens during compile time and generated PHP code might be harder to read, but Haxe adds a lot more than just type checking.
So if you've never heard of Haxe, it might be worth checking it out.
Disclaimer: Despite my user name, I'm not connected to the makers of Haxe, but I'm using Haxe for my past, current and future PHP projects.
Thanks for the suggestion I'll give it a look.
I think you got the per-file basis backwards: strict types must be declared in the file calling the function, not the file where the function is defined.
From the PHP manual:
See php.net/manual/en/functions.argume...
Sadly, I think this partly defeats the purpose of enforcing types, since having to remember to declare strict types in each file that calls a function is not that different from just getting the function calls right in the first place.
Yes what the manual says is sort of right on parameter types. If you don't declare strict types then the calling code will fall back to type coercion and not impose the parameter types defined in the required file. But if it can't coerce to the defined parameter type the code will still fail.
I've updated the associated GitHub library to highlight this point. github.com/RobDWaller/type-declara...
The article itself though is still technically correct it's just it is focused on return types and the fact you can't trust what you receive from a method without strict types imposed. I should maybe have stressed this point more.
I agree though that PHP Types aren't ideal but do believe they still have value when used correctly, you just have to impose strict types everywhere. 😂
"This approach does have some consequences though." - it's not this approach though.
Type coercion is what PHP always does by default. Basically, to make it simple, strict types means PHP won't do coercion when types are declared and will perform the default coercion behaviour otherwise.