Updated Rector YAML to PHP configuration, as current standard.
What if you could add scalar typehints int
, bool
, string
, null
to all parameter type and return type by running a CLI command? But also all classes, parent
, self
and $this
?
Do you think it's an easy task to move @param int $number
to (int $number)
?
Sneak peak what this post will be about:
There are tools that convert @param
and @return
doc to types today - like coding standards:
/**
* @param int $number
* @param string|null $name
* @return bool
*/
-public function isBigEnough($number, $name)
+public function isBigEnough(int $number, ?string $name): bool
{
}
But its breaks your code because it only works with tokens of the current file. It's like robot seeing the text by e a c h c h a r instead of understanding a sentence in a paragraph context.
You probably assume coding standard would not break your code, but then you spend 2 days fixing invalid typehints.
"How did the example above break your code?", you might ask. That one would pass. But what if your implements interface from /vendor
?
<?php
interface Sniff
{
/**
* @param int $position
*/
function process(File $file, $position);
}
Your code updated by coding standards:
<?php
final class SuperCoolSniff implements Sniff
{
/**
* @param int $position
*/
- public function process(File $file, $position)
+ public function process(File $file, int $position)
{
// ...
}
}
PHP Fatal error:
Declaration of SuperCoolSniff::process(File $file, int $position)
must be compatible with Sniff::process(File $file, $position)
...
"Just fix these cases manually". Yes, you could do that. But why would you test your code manually after each commit if you can cover them with tests in a matter of minutes?
I wonder what Albert Einstein would say seeing you do that work manually:
If you can't ~~explain~~ automate it simply,
you don't understand it well enough.
The problematic itself is not as simple as moving @return int
to int
.
If there is @param boolean
, can the typehint beboolean
?
/**
* @param integer $value
* @return boolean|NULL $value
*/
-function some($value)
+function some(int $value): ?bool
{
}
Since PHP 7.0 is dead now, we'll work with PHP 7.1 with void
and nullables on board.
I did some research on existing tools, their issues and Symfony code and this is what I found:
/**
* @param false|true|null $value
*/
-function some($value)
+function some(?bool $value)
{
}
/**
* @param $this $value
*/
-function some($value)
+function some(self $value)
{
}
/**
* @param array|Item[]|Item[][]|null $value
*/
-function some($value)
+function some(?array $value)
{
}
/**
* @param \Traversable|array $value
*/
-function some($value)
+function some(iterable $value)
{
}
Docs are quite easy, just parse few strings and change them to types that PHP accepts. phpdoc-parser by Jan Tvrdík helps it lot, together with format-preserving printer.
Let's get harder...
What happens when your interface is changed?
interface WorkerInterface
{
/**
* @param string $version
*/
- public function work($version);
+ public function work(string $version);
}
You need to update all its children:
final class StrongWorker implements WorkerInterface
{
/**
* @param string $version
*/
- public function work($version)
+ public function work(string $version)
{
}
}
final class SmartWorker implements WorkerInterface
{
/**
* @param string $version
*/
- public function work($version)
+ public function work(string $version)
{
}
}
Don't forget the interface too:
interface CacheableWorkerInterface extends WorkerInterface
{
/**
* @param string $version
*/
- public function work($version);
+ public function work(string $version);
}
And finally, one of my favorite cases I found in Symfony:
<?php
final class SmartWorker implements WorkerInterface
{
use BasicWorkerTrait;
}
Oh no, we almost forgot to upgrade the trait that implements the interface indirectly:
<?php
trait BasicWorkerTrait
{
- public function work($version)
+ public function work(string $version)
{
}
}
Trait has no doc block, no interface, no class, no other trait in it. She has no idea she should be updated.
self
& parent
self
and parent
are unique in each classes.
<?php
class P
{
}
class A extends P
{
/**
* @return self
*/
- public function foo()
+ public function foo(): self
{
}
/**
* @return parent
*/
- public function bar()
+ public function bar(): parent
{
}
}
class B extends A
{
- public function foo()
+ public function foo(): A
{
}
- public function bar()
+ public function bar(): P
{
}
}
Last but not least, different namespaces can cause another error:
<?php
namespace SomeNamespace;
class A
{
/**
* @return B ← "SomeNamespace\B"
*/
- public function foo()
+ public function foo(): B
{
}
}
namespace AnotherNamespace;
class C extends A
{
- public function foo()
+ public function foo(): B // missing class "AnotherNamespace\B"
+ public function foo(): \SomeNamespace\B // correct!
{
}
}
Do you want more wild code cases? You'll find the full test battery of 60 snippets here in Github test.
This where good old AST comes the rescue. It knows all the nodes in your scope = not in /vendor
, all children, all their implementations and used traits. It can traverse up and down this tree and see if the typehint would break something.
PHP 7.3 is out and PHP 7.0 is in end of life for 6 days. This is the best time to go PHP 7.1.
composer require rector/rector --dev
For those of you who have Rector already installed, use at least 0.3.24
version to get these features.
use Rector\TypeDeclaration\Rector\FunctionLike\ParamTypeDeclarationRector;
use Rector\TypeDeclaration\Rector\FunctionLike\ReturnTypeDeclarationRector;
use Rector\Config\RectorConfig;
return function (RectorConfig $rectorConfig): void {
$rectorConfig->rule(ParamTypeDeclarationRector::class);
$rectorConfig->rule(ReturnTypeDeclarationRector::class);
};
vendor/bin/rector process src --dry-run
# all good? instantly upgrade your code ↓
vendor/bin/rector process src
As there are many ways class-like elements can be connected - like the one with the trait that was accidentally part of interface -, there might be some more cases. Report everything you found, so one day this will be able to refactor all PHP Github code without breaking anything.
And when you're done, you can get your docblocks cleaned :)
Do you learn from my contents or use open-source packages like Rector every day?
Consider supporting it on GitHub Sponsors.
I'd really appreciate it!