Skip to main content

How I upgraded eight PHP katas

·20 mins

Split screen
Split screen
Photo generated by DALL-E

I recently upgraded eight PHP Katas from a minimum PHP 7.3 to a minimum of PHP 8.0. I used Rector, Easy Coding Standard (ECS) and PhpStan to help me. This is how I approached the upgrade and used these tools.

Why did I upgrade #

I stated in my blog post on “Why I enjoy coding katas” I had updated six katas to be consistent, all with the same tooling and documentation. This means anyone who can try one Kata will know how to pall any of the six others.

PHP 8 has been around for over two years now. The katas have already been bumped, two years ago, with support for PHP 8. Now that PHP 7.4 is at the end of its life it was a good time to fully update the starting code to the latest PHP 8.0 standard. The most noticeable is the way Classes are constructed, and the removal of properties, which are now generated in the constructor.

If I open a Class and see properties, a constructor with parameters and the parameters being assigned to the properties, I know I’m in an ‘old’, pre-PHP 8 class.

It would therefore be good to upgrade all the starting code to be the PHP 8 style. The purpose of the Kata is to practice refactoring. The starting code should not need upgrading.

How did I upgrade #

I’ve already completed six of the eight Kata. I have a plan of attack.

  1. create a ‘php8’ branch
  2. update composer.json
  3. update the readme.md
  4. remove .editorconfig and composer.lock
  5. add them to the .gitignore
  6. upgrade the easy coding standard config file (ecs.php)
  7. run composer install
  8. require Rector
  9. run the tests
  10. check the code standard
  11. check the static analysis
  12. repeat 9-11 until I’m satisfied
  13. remove Rector
  14. push the code and create a PR

Required tooling #

All the katas have:

I want all the katas to have the latest versions of these tools, ECS also needs an updated config file.

I also want to temporarily pull in Rector PHP, to try and automatically upgrade the code.

Create a branch #

I have already forked the code from emilybache/Racing-Car-Katas to my repo and cloned it to my computer.

cd Racing-Car-Katas/php
git checkout -b php8

update composer.json #

The composer.json file can be updated in three parts:

Bump the minimum PHP version to 8 #

To bump the minimum required PHP version to 8, update the php requirement:

From:

"require": {
"php": "^7.3|^8.0"
},

To:

"require": {
"php": "^8.0"
},

Update the tooling #

By this point, I had already locked in the tooling, by using composer remove –dev and re-installing using composer require –dev, while running PHP 8.0. Composer installed the latest version.

Was:

"require-dev": {
"phpunit/phpunit": "^9.5",
"phpstan/phpstan": "^0.12.25",
"phpstan/phpstan-phpunit": "^0.12.10",
"symplify/easy-coding-standard": "^9.0",
"symplify/phpstan-extensions": "^9.0"
},

I had the exact versions from the previous kata, which I had already upgraded. Now:

"require-dev": {
"phpunit/phpunit": "^9.5",
"phpstan/phpstan": "^1.9",
"phpstan/phpstan-phpunit": "^1.3",
"symplify/easy-coding-standard": "^11.1",
"symplify/phpstan-extensions": "^11.1"
},

update the scripts #

Remove the unnecessary scripts and update the ecs scripts, which get the folder from the config file.

"scripts": {
- "checkcode": "phpcs src tests --standard=PSR12",
- "fixcode": "phpcbf src tests --standard=PSR12",
- "test": "phpunit",
"tests": "phpunit",
"test-coverage": "phpunit --coverage-html build/coverage",
-"check-cs": "ecs check src tests --ansi",
+"check-cs": "ecs check --ansi",
-"fix-cs": "ecs check src tests --fix --ansi",
+"fix-cs": "ecs check --fix --ansi",
"phpstan": "phpstan analyse --ansi"
}

Update the README.md #

The PHP version of README.md needs to be consistent across all the Katas.

If anyone pulls one Kata they will find the other PHP Kata to be consistent and familiar. This means they can get on with the Kata rather than working out how each one is set up.

Remove .editorconfig and composer.lock and add them to the .gitignore #

The .editorconfig isn’t required, the code standard will take care of incorrect spaces.

composer.lock can be removed, as a composer install will regenerate a composer.lock

.gitignore can be updated to ignore these files:

/.idea
/build
/vendor
/.vscode
/.editorconfig
/.phpunit.result.cache
/composer.lock

Upgrade the ECS config file #

A new ecs.php can be copied from previous projects. A new template can be created by running vendor/bin/ecs init which will update ECS to the latest config. The exact config can be tailored to each project.

<?php

declare(strict_types=1);

use PhpCsFixer\Fixer\ArrayNotation\ArraySyntaxFixer;
use PhpCsFixer\Fixer\Strict\DeclareStrictTypesFixer;
use Symplify\EasyCodingStandard\Config\ECSConfig;
use Symplify\EasyCodingStandard\ValueObject\Set\SetList;

// composer require --dev symplify/easy-coding-standard
// vendor/bin/ecs init

return static function (ECSConfig $ecsConfig): void {
   $ecsConfig->paths([
       __DIR__ . '/src',
       __DIR__ . '/tests',
       __DIR__ . '/ecs.php',  // check this file too!
   ]);

   $ecsConfig->skip([
       // rules to skip
   ]);

   // run and fix, one by one
   $ecsConfig->sets([
       SetList::SPACES,
       SetList::ARRAY,
       SetList::DOCBLOCK,
       SetList::NAMESPACES,
       SetList::CONTROL_STRUCTURES,
       SetList::CLEAN_CODE,
       SetList::STRICT,
       SetList::PSR_12,
       SetList::PHPUNIT,
   ]);

   // add declare(strict_types=1); to all php files:
   $ecsConfig->rule(DeclareStrictTypesFixer::class);

   // change $array = array(); to $array = [];
   $ecsConfig->ruleWithConfiguration(ArraySyntaxFixer::class, [
       'syntax' => 'short',
   ]);

   // [default: PHP_EOL]; other options: "\n"
   $ecsConfig->lineEnding("\n");
};

Remove the config file ecs.yaml. ECS used to use a yaml file format for its config setting. The php style config was introduced several years ago, which is a much better completion and has no more errors due to tabs or spaces!

Run composer install #

The Kata is ready to be started, with the required tooling.

composer install

Run the tests #

Make sure all the tests are passing, although this Kata starts with a failing test.

pu.bat
# or composer tests
composer tests
> phpunit
PHPUnit 9.5.27 by Sebastian Bergmann and contributors.

Runtime:       PHP 8.0.27
Configuration: E:\laragon\cli\Racing-Car-Katas\php\phpunit.xml.dist

....IEF.F                                                           9 / 9 (100%)

Time: 00:00.038, Memory: 4.00 MB

There was 1 error:

1) Tests\TextConverter\HtmlPagesConverterTest::testFoo
fopen(foo): Failed to open stream: No such file or directory

E:\laragon\cli\Racing-Car-Katas\php\src\TextConverter\HtmlPagesConverter.php:18
E:\laragon\cli\Racing-Car-Katas\php\tests\TextConverter\HtmlPagesConverterTest.php:14

--

There were 2 failures:

1) Tests\TextConverter\HtmlTextConverterTest::testFoo
Failed asserting that two strings are identical.
--- Expected
+++ Actual
@@ @@
-'fixme'
+'foo'

E:\laragon\cli\Racing-Car-Katas\php\tests\TextConverter\HtmlTextConverterTest.php:15

2) Tests\TurnTicketDispenser\TicketDispenserTest::testFoo
Failed asserting that 0 is identical to -1.

E:\laragon\cli\Racing-Car-Katas\php\tests\TurnTicketDispenser\TicketDispenserTest.php:16

--

There was 1 incomplete test:

1) Tests\TelemetrySystem\TelemetryDiagnosticControlsTest::testCheckTransmissionShouldSendAndReceiveDiagnosticMessage

E:\laragon\cli\Racing-Car-Katas\php\tests\TelemetrySystem\TelemetryDiagnosticControlsTest.php:14

ERRORS!
Tests: 9, Assertions: 10, Errors: 1, Failures: 2, Incomplete: 1.
Script phpunit handling the tests event returned with error code 2

Ideally, full test coverage is preferred, but this Kata doesn’t start that way, the upgraded code may need to be reviewed later.

Require rector PHP #

Require rector PHP and initialise it, following the documentation.

composer require rector/rector --dev
# …
# Package operations: 1 install, 0 updates, 0 removals
# - Downloading rector/rector (0.15.3)
# - Installing rector/rector (0.15.3): Extracting archive
# …

vendor/bin/rector init
# or vendor\bin\rector.bat init
#  [OK] "rector.php" config file was added

The init command will automatically generate a starter rector.php config, this file can be edited to help, the nice thing about the config is how easy it is to read. In the future, if PHP 8.1 or PHP 8.2 is the minimum then change the LevelSetList::UP_TO_PHP_<version>.

<?php

declare(strict_types=1);

use Rector\CodeQuality\Rector\ClassMethod\ReturnTypeFromStrictScalarReturnExprRector;
use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\LevelSetList;
use Rector\TypeDeclaration\Rector\ClassMethod\ReturnTypeFromStrictNativeCallRector;

return static function (RectorConfig $rectorConfig): void {
   $rectorConfig->paths([
       __DIR__ . '/src',
       __DIR__ . '/tests',
   ]);

   // register rules to add return type when known
   $rectorConfig->rules([
       ReturnTypeFromStrictNativeCallRector::class,
       ReturnTypeFromStrictScalarReturnExprRector::class,
   ]);

   // define sets of rules
   $rectorConfig->sets([
       LevelSetList::UP_TO_PHP_80
   ]);
};

To see a preview of suggested changes, run the rector command with –dry-run option:

vendor\bin\rector.bat --dry-run
# or vendor/bin/rector --dry-run

The output was reviewed, and 13 files would be updated. Most of the changes where to do with updating the constructor e.g.

 class Leaderboard
 {
---    /**
---     * @var Race[]
---     */
---    private $races;
---
---    public function __construct(array $races)
---    {
---        $this->races = $races;
+++    /**
+++     * @param \RacingCar\Leaderboard\Race[] $races
+++     */
+++    public function __construct(private array $races)
+++    {
     }

The other changes were to add the implements \Stringable when the class uses __toString e.g.

namespace RacingCar\Leaderboard;

--- class Driver
+++ class Driver implements \Stringable
 {
     public $name;

@@ @@
         $this->country = $country;
     }

---    public function __toString()
+++    public function __toString(): string
     {
---        return "{$this->name} {$this->country}";
+++        return (string) "{$this->name} {$this->country}";
     }
 }

Another change was to inline the parameter type e.g.

10) src/TelemetrySystem/TelemetryClient.php:10

    ---------- begin diff ----------
@@ @@
 {
     public const DIAGNOSTIC_MESSAGE = 'AT#UD';

---    private $onlineStatus = false;
+++    private bool $onlineStatus = false;

---    private $diagnosticMessageJustSent = false;
+++    private bool $diagnosticMessageJustSent = false;

Although in the test cases, the full class address is used. The Driver class would be better to be a use statement:

 class LeaderboardTest extends TestCase
 {
---    private $driver1;
+++    private \RacingCar\Leaderboard\Driver $driver1;

An interesting one was the random_int rector:

5) src/TirePressureMonitoring/Sensor.php:20

    ---------- begin diff ----------
@@ @@
     private static function getSamplePressure()
     {
         // placeholder implementation that simulates a real sensor in a real tyre
---        return 6 * mt_rand() / mt_getrandmax() * mt_rand() / mt_getrandmax();
+++        return 6 * random_int(0, mt_getrandmax()) / mt_getrandmax() * random_int(0, mt_getrandmax()) / mt_getrandmax();
     }
 }
    ----------- end diff -----------

This improvement would have been easily missed.

I wanted to double-check the output is comparable, by opening an interactive php shell:

php -a
Interactive shell

php > echo 6 * mt_rand() / mt_getrandmax() * mt_rand() / mt_getrandmax();
0.28789230076317
php > 6 * mt_rand() / mt_getrandmax() * mt_rand() / mt_getrandmax();
php > echo 6 * mt_rand() / mt_getrandmax() * mt_rand() / mt_getrandmax();
0.59193628730294
php > echo 6 * mt_rand() / mt_getrandmax() * mt_rand() / mt_getrandmax();
2.3353527303516
php > echo 6 * mt_rand() / mt_getrandmax() * mt_rand() / mt_getrandmax();
3.9111024607976
php > echo 6 * mt_rand() / mt_getrandmax() * mt_rand() / mt_getrandmax();
1.3882777102362
php > echo 6 * mt_rand() / mt_getrandmax() * mt_rand() / mt_getrandmax();
1.3381505674129
php > echo 6 * random_int(0, mt_getrandmax()) / mt_getrandmax() * random_int(0, mt_getrandmax()) / mt_getrandmax();
0.33623375077603
php > echo 6 * random_int(0, mt_getrandmax()) / mt_getrandmax() * random_int(0, mt_getrandmax()) / mt_getrandmax();
0.21711217311234
php > echo 6 * random_int(0, mt_getrandmax()) / mt_getrandmax() * random_int(0, mt_getrandmax()) / mt_getrandmax();
1.8174108426317
php > echo 6 * random_int(0, mt_getrandmax()) / mt_getrandmax() * random_int(0, mt_getrandmax()) / mt_getrandmax();
0.59516582006594
php > echo 6 * random_int(0, mt_getrandmax()) / mt_getrandmax() * random_int(0, mt_getrandmax()) / mt_getrandmax();
0.13920652881307
php > echo 6 * random_int(0, mt_getrandmax()) / mt_getrandmax() * random_int(0, mt_getrandmax()) / mt_getrandmax();
0.56419425470649
php > echo 6 * random_int(0, mt_getrandmax()) / mt_getrandmax() * random_int(0, mt_getrandmax()) / mt_getrandmax();
1.1058509887747
php > echo 6 * random_int(0, mt_getrandmax()) / mt_getrandmax() * random_int(0, mt_getrandmax()) / mt_getrandmax();
4.1742725948899
php > echo 6 * random_int(0, mt_getrandmax()) / mt_getrandmax() * random_int(0, mt_getrandmax()) / mt_getrandmax();
1.8491269459026
php > echo 6 * random_int(0, mt_getrandmax()) / mt_getrandmax() * random_int(0, mt_getrandmax()) / mt_getrandmax();
2.8255782588891

Ok, the recommended change is giving a similar ‘random’ output.

One other PHP 8 improvement was str_contrains:

---            if (strpos($line, 'PAGE_BREAK') !== false) {
+++            if (str_contains($line, 'PAGE_BREAK')) {

Much nicer to read!

I’m happy with what I have seen. I am also confident after upgrading six other Kata’s that this will not break the code!

To make changes happen, run the bare command:

vendor\bin\rector.bat
# or vendor/bin/rector

I can re-run the tests can confirm the tests are giving the same results:

pu.bat
# or composer tests

The results are exactly the same:

Runtime:       PHP 8.0.27
Configuration: E:\laragon\cli\Racing-Car-Katas\php\phpunit.xml.dist

....IEF.F                                                           9 / 9 (100%)

Time: 00:00.016, Memory: 4.00 MB
# ….

ERRORS!
Tests: 9, Assertions: 10, Errors: 1, Failures: 2, Incomplete: 1.
Script phpunit handling the tests event returned with error code 2

Check the code standard #

Run cc.bat or composer check-cs to check the code quality

Most of the fixes are for StandaloneLinePromotedPropertyFixer

E.g.

-class TurnTicket
-{
-    public function __construct(private int $turnNumber)
-    {
-    }

+class TurnTicket
+{
+    public function __construct(
+        private int $turnNumber
+    ) {
+    }

Which makes the code easier to read, especially when there are multiple properties.

Most of the other fixes are to do with spaces, brace positions and gaps between imports, all cosmetic changes, which keep the code confident to read.

  • PhpCsFixer\Fixer\Basic\CurlyBracesPositionFixer
  • PhpCsFixer\Fixer\Import\SingleLineAfterImportsFixer
  • PhpCsFixer\Fixer\Whitespace\NoWhitespaceInBlankLineFixer

One change I don’t want was NoUnusedImportsFixer, as this is starter code with incomplete tests I want to leave the import as a clue to what to start testing.

// This import is a clue, I want to leave it in the starting code:
---use RacingCar\TelemetrySystem\TelemetryDiagnosticControls;

use PHPUnit\Framework\TestCase;

class TelemetryDiagnosticControlsTest extends TestCase
 {
    public function testCheckTransmissionShouldSendAndReceiveDiagnosticMessage(): void
     {
         $this->markTestIncomplete();
     }
}

I can add this rule to the skip rules section of ecs.php config file:

$ecsConfig->skip([
   // keep unused imports until the refactor is complete
   PhpCsFixer\Fixer\Import\NoUnusedImportsFixer::class
]);

The other problem was the report was too long to read, I ran the script to pipe the output to a text file:

vendor\bin\ecs.bat --no-ansi > ecsoutput.txt

I can now open ecsoutput.txt in my IDE and review the changes. It is harder to read as the colour coding has been stripped. At this stage, I’m more interested in the rules used, as I don’t want the starter code to be removed. Finding Applied checkers: and reviewing the rules that would be applied to each file.

I’m now satisfied with the results, so I can run the fix

fc.bat
# or composer fix-cs
// ….

 [OK] 21 errors successfully fixed and no other errors found!

I can delete the ecsoutput.txt file, which is no longer needed.

The tests can be re-run, I’m not expecting any surprises, as the changes to code style are more cosmetic, so running the tests is a precaution.

pu.bat
# or composer tests

The results are the same:

Runtime:       PHP 8.0.27
Configuration: E:\laragon\cli\Racing-Car-Katas\php\phpunit.xml.dist

....IEF.F                                                           9 / 9 (100%)

Time: 00:00.015, Memory: 4.00 MB
# ….

ERRORS!
Tests: 9, Assertions: 10, Errors: 1, Failures: 2, Incomplete: 1.
Script phpunit handling the tests event returned with error code 2

I’m happy the tests are the same, so can move on to the next thing.

Check the static analysis #

Before the analyser can be run the config file needs to be updated:

includes:
   - vendor/symplify/phpstan-extensions/config/config.neon
   - vendor/phpstan/phpstan-phpunit/extension.neon

parameters:
   paths:
       - src
       - tests

---   # Level 8 is the highest
---   level: 8
+++   # max is the highest level
+++   level: max

   checkGenericClassInNonGenericObjectType: false

Two years ago 8 was the highest level, it’s now 9, and we can set it to max.

Run PhpStan static analyser:

ps.bat
# or composer phpstan

The output shows each file with the error, there are 33 error in total:

λ ps.bat

composer phpstan
> phpstan analyse --ansi
Note: Using configuration file E:\laragon\cli\Racing-Car-Katas\php\phpstan.neon.
 20/20 [============================] 100%

 ------ ------------------------------------------------------------------------
  Line   src\Leaderboard\Driver.php
 ------ ------------------------------------------------------------------------
  9      Property RacingCar\Leaderboard\Driver::$name has no type specified.
  11     Property RacingCar\Leaderboard\Driver::$country has no type specified.
 ------ ------------------------------------------------------------------------

 ------ -------------------------------------------------------------------------------------------------------------------------------
  Line   src\Leaderboard\Leaderboard.php
 ------ -------------------------------------------------------------------------------------------------------------------------------
  17     Method RacingCar\Leaderboard\Leaderboard::getDriverResults() has no return type specified.
  32     Method RacingCar\Leaderboard\Leaderboard::getDriverRankings() return type has no value type specified in iterable type array.
         � See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
 ------ -------------------------------------------------------------------------------------------------------------------------------

 ------ -----------------------------------------------------------------------------------------------------------------
  Line   src\Leaderboard\Race.php
 ------ -----------------------------------------------------------------------------------------------------------------
  9      Property RacingCar\Leaderboard\Race::$points type has no value type specified in iterable type array.
         � See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
  11     Property RacingCar\Leaderboard\Race::$driverNames type has no value type specified in iterable type array.
         � See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
  17     Property RacingCar\Leaderboard\Race::$name is never read, only written.
         � See: https://phpstan.org/developing-extensions/always-read-written-properties
  31     Method RacingCar\Leaderboard\Race::getPosition() has parameter $driver with no type specified.
  33     Method RacingCar\Leaderboard\Race::getPosition() should return int but returns int|string|false.
  41     Method RacingCar\Leaderboard\Race::getResults() return type has no value type specified in iterable type array.
         � See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
 ------ -----------------------------------------------------------------------------------------------------------------

 ------ -----------------------------------------------------------------------------------------
  Line   src\Leaderboard\SelfDrivingCar.php
 ------ -----------------------------------------------------------------------------------------
  9      Property RacingCar\Leaderboard\SelfDrivingCar::$algorithmVersion has no type specified.
 ------ -----------------------------------------------------------------------------------------

 ------ ---------------------------------------------------------------------------------------------------
  Line   src\TelemetrySystem\TelemetryClient.php
 ------ ---------------------------------------------------------------------------------------------------
  98     Method RacingCar\TelemetrySystem\TelemetryClient::getOnlineStatus() has no return type specified.
 ------ ---------------------------------------------------------------------------------------------------

 ------ --------------------------------------------------------------------------------------------------------
  Line   src\TelemetrySystem\TelemetryDiagnosticControls.php
 ------ --------------------------------------------------------------------------------------------------------
  13     Property RacingCar\TelemetrySystem\TelemetryDiagnosticControls::$diagnosticInfo has no type specified.
 ------ --------------------------------------------------------------------------------------------------------

 ------ -----------------------------------------------------------------------------------------------------------------------
  Line   src\TextConverter\HtmlPagesConverter.php
 ------ -----------------------------------------------------------------------------------------------------------------------
  9      Property RacingCar\TextConverter\HtmlPagesConverter::$breaks type has no value type specified in iterable type array.
         � See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
  16     Parameter #1 $stream of function fgets expects resource, resource|false given.
  19     Parameter #1 $stream of function ftell expects resource, resource|false given.
  20     Parameter #1 $stream of function ftell expects resource, resource|false given.
  23     Parameter #1 $stream of function ftell expects resource, resource|false given.
  24     Parameter #1 $stream of function fclose expects resource, resource|false given.
  32     Parameter #1 $stream of function fseek expects resource, resource|false given.
  34     Parameter #1 $stream of function ftell expects resource, resource|false given.
  35     Parameter #1 $stream of function fgets expects resource, resource|false given.
  35     Parameter #1 $string of function rtrim expects string, string|false given.
  42     Parameter #1 $stream of function fclose expects resource, resource|false given.
  46     Method RacingCar\TextConverter\HtmlPagesConverter::getFileName() has no return type specified.
 ------ -----------------------------------------------------------------------------------------------------------------------

 ------ -----------------------------------------------------------------------------------------------
  Line   src\TextConverter\HtmlTextConverter.php
 ------ -----------------------------------------------------------------------------------------------
  19     Parameter #1 $stream of function fgets expects resource, resource|false given.
  27     Method RacingCar\TextConverter\HtmlTextConverter::getFileName() has no return type specified.
 ------ -----------------------------------------------------------------------------------------------

 ------ ---------------------------------------------------------------------------------------------------------
  Line   src\TirePressureMonitoring\Sensor.php
 ------ ---------------------------------------------------------------------------------------------------------
  14     Method RacingCar\TirePressureMonitoring\Sensor::popNextPressurePsiValue() has no return type specified.
  21     Method RacingCar\TirePressureMonitoring\Sensor::getSamplePressure() has no return type specified.
 ------ ---------------------------------------------------------------------------------------------------------

 ------ -----------------------------------------------------------------------------------------------------
  Line   src\TurnTicketDispenser\TicketDispenser.php
 ------ -----------------------------------------------------------------------------------------------------
  9      Method RacingCar\TurnTicketDispenser\TicketDispenser::getTurnTicket() has no return type specified.
 ------ -----------------------------------------------------------------------------------------------------

 ------ ---------------------------------------------------------------------------------------------------
  Line   src\TurnTicketDispenser\TurnNumberSequence.php
 ------ ---------------------------------------------------------------------------------------------------
  11     Method RacingCar\TurnTicketDispenser\TurnNumberSequence::nextTurn() has no return type specified.
 ------ ---------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------
  Line   src\TurnTicketDispenser\TurnTicket.php
 ------ ------------------------------------------------------------------------------------------------
  14     Method RacingCar\TurnTicketDispenser\TurnTicket::getTurnNumber() has no return type specified.
 ------ ------------------------------------------------------------------------------------------------

 ------ ----------------------------------------------------------------------------------------------
  Line   tests\Leaderboard\LeaderboardTest.php
 ------ ----------------------------------------------------------------------------------------------
  37     Property Tests\Leaderboard\LeaderboardTest::$sampleLeaderboard2 is never read, only written.
         � See: https://phpstan.org/developing-extensions/always-read-written-properties
 ------ ----------------------------------------------------------------------------------------------

 [ERROR] Found 33 errors

Script phpstan analyse --ansi handling the phpstan event returned with error code 1
PhpStan is currently set to max, which is the strictest mode.

Both ECS and Rector have automatic code fixes, unfortunately, there are no PhpStan automatic fixes.

Most of the problems are no type specified (for a property) and no return type (returned from a method). It’s a case of looking at each file, one by one, and manually working out the types. I may run the test with coverage to check which test is covering the code and see the input parameters.

Example (Driver.php):

---public $name;
+++public ?string $name;

---public $country;
+++public string $country;

public function __construct(?string $name, string $country)
// ...

Oddly this wasn’t updated to php 8 standard, now the types have been added, Rector can be re-run.

vendor\bin\rector.bat
# or vendor/bin/rector
 class Driver implements \Stringable
 {
---    public ?string $name;
---
---    public string $country;
---
---    public function __construct(?string $name, string $country)
+++    public function __construct(public ?string $name, public string $country)
     {
---        $this->name = $name;
---        $this->country = $country;
     }

Nice! That class has now been updated.

ECS can be re-run to clean up any code standard issues.

Re-running ps.bat confirms we are down to 31 errors. I’ll look at the remaining no type specified:

src\Leaderboard\SelfDrivingCar.php line 9

The type was in the constructor:

---public $algorithmVersion;
+++public string $algorithmVersion;


public function __construct(string $algorithmVersion, string $company)

src\TelemetrySystem\TelemetryDiagnosticControls.php line 10

Based on the default value, the type is a string. Checking the usage $this->diagnosticInfo = $this->telemetryClient->receive(); confirm the return type from the method receive(): string is a string

---public $diagnosticInfo = '';
+++public string $diagnosticInfo = '';

Rinse and repeat #

Again Rector can be run, ECS and PHPUnit then re-run PhpStan.

Down to 29 errors!

Next, no return type is specified, this means each method has to be manually checked. If it has a return the type will need to be added, otherwise, it is void.

  • RacingCar\Leaderboard\Leaderboard::getDriverResults()

Is an array.

  • RacingCar\TelemetrySystem\TelemetryClient::getOnlineStatus()

Is returning $this->onlineStatus with is a bool

  • RacingCar\TextConverter\HtmlTextConverter::getFileName()

Is returning $this->fullFileNameWithPath, which is a string

  • RacingCar\TirePressureMonitoring\Sensor::popNextPressurePsiValue() is a float.
  • RacingCar\TirePressureMonitoring\Sensor::getSamplePressure() is a float.
  • RacingCar\TurnTicketDispenser\TicketDispenser::getTurnTicket(): TurnTicket
  • RacingCar\TurnTicketDispenser\TurnNumberSequence::nextTurn(): int
  • RacingCar\TurnTicketDispenser\TurnTicket::getTurnNumber(): int
  • RacingCar\TextConverter\HtmlPagesConverter::getFileName(): string
  • RacingCar\Leaderboard\Race::getPosition() has parameter $driver with no type specified. Is a Driver
  • public function getPoints(Driver $driver): int

Down to 20 errors! Only 5 files (Classes).

Next, we can look at the next highest item return type has no value type specified in iterable type array., this is purely a PhpStan error, it needs to know the types of the array so further static analysis can help us. We need to add some doc block information about the array, advising the structure.

  • src\Leaderboard\Leaderboard.php
/**
* @return array<string, int>
*/
public function getDriverResults(): array
//…

/**
* @return int[]|string[]
*/
public function getDriverRankings(): array
  • src\Leaderboard\Race.php
  /**
  * @var int[]
  */
  private static array $points = [25, 18, 15];

  /**
  * @var array<string, string|null>
  */
  private array $driverNames;

  /**
  * @return Driver[]
  */
  public function getResults(): array
  • src\TextConverter\HtmlPagesConverter.php
  /**
  * @var int[]
  */
  private array $breaks;

There are now 17 errors, as PhpStan “knows more” about the code, other errors have been generated:

  Line   src\Leaderboard\Race.php
 ------ --------------------------------------------------------------------------------------------------
  23     Property RacingCar\Leaderboard\Race::$name is never read, only written.
         � See: https://phpstan.org/developing-extensions/always-read-written-properties
  39     Method RacingCar\Leaderboard\Race::getPosition() should return int but returns int|string|false.
  57     Method RacingCar\Leaderboard\Race::getDriverName() should return string but returns string|null.
 ------ --------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------
  Line   src\TextConverter\HtmlPagesConverter.php
 ------ ------------------------------------------------------------------------------------------------------------
  19     Parameter #1 $stream of function fgets expects resource, resource|false given.
  22     Parameter #1 $stream of function ftell expects resource, resource|false given.
  23     Parameter #1 $stream of function ftell expects resource, resource|false given.
  23     Property RacingCar\TextConverter\HtmlPagesConverter::$breaks (array<int>) does not accept array<bool|int>.
  26     Parameter #1 $stream of function ftell expects resource, resource|false given.
  26     Property RacingCar\TextConverter\HtmlPagesConverter::$breaks (array<int>) does not accept array<bool|int>.
  27     Parameter #1 $stream of function fclose expects resource, resource|false given.
  40     Parameter #1 $stream of function fseek expects resource, resource|false given.
  42     Parameter #1 $stream of function ftell expects resource, resource|false given.
  43     Parameter #1 $stream of function fgets expects resource, resource|false given.
  43     Parameter #1 $string of function rtrim expects string, string|false given.
  50     Parameter #1 $stream of function fclose expects resource, resource|false given.
 ------ ------------------------------------------------------------------------------------------------------------

 ------ --------------------------------------------------------------------------------
  Line   src\TextConverter\HtmlTextConverter.php
 ------ --------------------------------------------------------------------------------
  19     Parameter #1 $stream of function fgets expects resource, resource|false given.
 ------ --------------------------------------------------------------------------------

 ------ ----------------------------------------------------------------------------------------------
  Line   tests\Leaderboard\LeaderboardTest.php
 ------ ----------------------------------------------------------------------------------------------
  37     Property Tests\Leaderboard\LeaderboardTest::$sampleLeaderboard2 is never read, only written.
         � See: https://phpstan.org/developing-extensions/always-read-written-properties
 ------ ----------------------------------------------------------------------------------------------

 [ERROR] Found 17 errors

Script phpstan analyse --ansi handling the phpstan event returned with error code 1

Running Rector, ECS and PHPUnit, everything is working and to the correct standard.

The starter code to a Kata, the description:

For each exercise, you should identify which SOLID principles are not being followed by the code. There is only one class you are interested in writing tests for right now. As a first step, try to get some kind of test in place before you change the class at all. If the tests are hard to write, is that because of the problems with SOLID principles?

This starter code needs to be refactored. I’m happy the class structure is now PHP 8 standard, it’s now ready for someone to start the exercise.

The remaining errors need the code to be refactored to check for errors such as the file existing.

I will, therefore, PR this code as the PHP starting point for the Kata.

Remove Rector #

Before I PR the code Rector and the rector.php config file needs to be removed:

composer remove rector/rector --dev
rm rector.php

Pull Request (PR) #

If you are interested the PR was successful, the full diff, of every change, can be seen Bump to PHP 8.0+

PHP Katas #

The eight Katas with PHP versions can be run by anyone (diff for information):

  1. Gilded Rose Refactoring Kata - (diff)
  2. Tennis Refactoring Kata - (diff)
  3. Parrot Refactoring Kata - (diff)
  4. The Supermarket Receipt Refactoring Kata - (diff)
  5. Theatrical Players Refactoring Kata - (diff)
  6. Yatzy Refactoring Kata - (diff)
  7. Lift Kata - (diff)
  8. Racing Car Katas - (diff)

Anyone new to Kata and refactoring I would recommend the Theatrical Players Refactoring Kata first. It is a code along with the first chapter of ‘Refactoring’ by Martin Fowler, 2nd Edition, using Javascript. I would even go so far as to say to do the Javascript version first, then redo the PHP version.

Then I would recommend the Parrot or Tennis or Yatzy Kata. One of the most time-consuming Kata is the Gilded Rose, as this starts with no tests, and then it needs refactoring before the new feature can be added.

I wish everyone good luck with these Kata, they helped me to level up my knowledge. The confidence tests give when refactoring code is immeasurable.

Conclusion #

In summary, I upgraded eight PHP katas to make them consistent, and easy for someone to clone and run. They are a great way to learn about refactoring code, especially when that code is well-tested, which then gives the confidence to refactor. I hope other PHP developers find these Kata’s useful, as I have.

This upgrade has given me valuable experience in how to upgrade an existing code base. I now have a better understanding of how Rector works. I use ECS, PhpStan and PHPUnit in my existing projects, thanks to exercises I did, like this, several years ago. When I have production code which requires an upgrade I will have more confidence to use Rector.

I hope this blog will help others to use the fantastic tools available to PHP developers, to help us write our code.