Code filled with docblocks param, var, and return types is a gold mine. Not in the meaning of valuable resource, but rather as exploding metal covered with a thin piece of gold, so we grab it without thinking. While these docblocks give us much information about the code, they might be nothing more than a wish, dream, or promise.
Have you ever blindly trusted docblocks and switched them to type declarations? Then you know the explosive regression this move brings.
Yet, how can we turn to add strict types to our code without fear of breaking it?
What can we read from this code if we trust it?
class SweetPromise
{
private $values;
/**
* @param mixed[] $values
*/
public function setValues($values)
{
$this->values = $values;
}
/**
* @return string[]
*/
public function getValues()
{
return $this->values;
}
}
The values are always an array
of strings. Let's tolerate strings and work with the array
type.
If that is true, these should not be possible:
$sweetPromise = new SweetPromise();
echo $sweetPromise->getValues(); // null or array?
$sweetPromise->setValues('{55}'); // string or array?
echo $sweetPromise->getValues(); // string or array?
We have no idea about the value we put in. We cannot make any assumptions unless we're ready to risk the type failure and manual verification of every single type.
The honest filter would show the class above like this:
class SweetPromise
{
private $values;
public function setValues($values)
{
$this->values = $values;
}
public function getValues()
{
return $this->values;
}
}
Now it's obvious we can't trust the docblocks. Mainly because the docblock types are not based on actual code but ideal state = if every method is 100 % reliable, called in precisely defined order, and with 100 % reliable typed input variables. Which they're not.
This sound a little depressing. So what can we do if we have millions of lines of code with docblocks? We want to move fast and safe at the same time.
We have a few options:
Certain types are based on actual values, operations, and logical structures that have under any circumstances always exactly one type:
5
→ int
"hi"
→ string
$anything && $somethingElse
always returns bool
100 - 20
always returns int
! $anything
always returns bool
Not only one-line values but also more complex structures like created and filled array:
function provideDreams()
{
$dreams = [];
foreach ($this->dreamRepository->fetchAll() as $dream) {
$dreams[] = $dream;
}
return $dreams;
}
This code always returns array
, no matter what $dream
actually is.
Based on these 4 rules, we can complete the following types with 100 % certainty:
final class Reality
{
- public function getAge()
+ public function getAge(): int
{
return 100;
}
- public function removeTax(int $value)
+ public function removeTax(int $value): int
{
return $value - 200;
}
- public function transform($value)
+ public function transform($value): string
{
if ($value === null) {
return '';
}
return base64_encode($value);
}
}
And much more!
Depending on what age your project is coming from, there is one requirement to make this work. You must use PHP 7.0, where return type declarations were added.
Rector can handles some case above in the basic version. Add these rules to your code and see for yourself:
<?php
use Rector\Config\RectorConfig;
use Rector\CodeQuality\Rector\ClassMethod\ReturnTypeFromStrictScalarReturnExprRector;
use Rector\TypeDeclaration\Rector\ClassMethod\ReturnTypeFromStrictNativeCallRector;
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->rules([
ReturnTypeFromStrictNativeCallRector::class,
ReturnTypeFromStrictScalarReturnExprRector::class,
]);
};
Run Rector and see how many new types were added. In the project we tested these rules on, we had over 40 changed files in the first run. From zero to 40 files, quite impressive. And as you know, strict types spread exponentially.
The rest of the cases and more will be covered in rules of upcoming Rector Enterprise version, with more extra rules and features under private paid license.