Smooth Upgrade to PHP 8 in Diffs

April 2022 Update

Since Rector 0.12 a new RectorConfig is available with simpler and easier to use config methods.


PHP 8 was released more than 2 weeks ago. Do you want to know what is new? Check colorful post series about PHP 8 news by Brent.

Do you want to upgrade your project today? Continue reading...

In a Rush to Private Jet?

3 Steps to Upgrade in 5 mins

  1. Do it in 5 minutes:
composer require rector/rector --dev
# create "rector.php"
vendor/bin/rector init
  1. Update rector.php with PHP 8 set:
+use Rector\Set\ValueObject\SetList;
 use Rector\Config\RectorConfig;

 return function (RectorConfig $rectorConfig): void {
+    $rectorConfig->import(SetList::PHP_80);
 };
  1. Run Rector:
vendor/bin/rector process src

How does such upgrade look in practise? See one of real pull-requests created with Rector:


Smooth Upgrade?

This tutorial aims to prepare you for the expected required steps so that the upgrade will require the least effort possible. Follow the guide and get to PHP 8 like a walk in the park.

What Rector handles for You?

1. From switch() to match()

-switch ($this->lexer->lookahead['type']) {
-    case Lexer::T_SELECT:
-        $statement = $this->SelectStatement();
-        break;
-
-    default:
-        $this->syntaxError('SELECT, UPDATE or DELETE');
-        break;
-}
+$statement = match ($this->lexer->lookahead['type']) {
+    Lexer::T_SELECT => $this->SelectStatement(),
+    default => $this->syntaxError('SELECT, UPDATE or DELETE'),
+};

2. From get_class() to Faster X::class

-get_class($object);
+$object::class;

3. From Dummy Constructor to Promoted Properties

 class SomeClass
 {
-    public float $alcoholLimit;
-
-    public function __construct(float $alcoholLimit = 0.0)
+    public function __construct(public float $alcoholLimit = 0.0)
     {
-        $this->alcoholLimit = $alcoholLimit;
     }
 }

4. Private Final Methods are Not Allowed Anymore

 class SomeClass
 {
-    final private function getter()
+    private function getter()
     {
         return $this;
     }
 }

5. Replace Null Checks with Null Safe Calls

 class SomeClass
 {
     public function run($someObject)
     {
-        $someObject2 = $someObject->mayFail1();
-        if ($someObject2 === null) {
-            return null;
-        }
-
-        return $someObject2->mayFail2();
+        return $someObject->mayFail1()?->mayFail2();
     }
 }

6. Unused $variable in catch() is not Needed Anymore

 final class SomeClass
 {
     public function run()
     {
         try {
-        } catch (Throwable $notUsedThrowable) {
+        } catch (Throwable) {
         }
     }
 }

7. New str_contains() Function

-$hasA = strpos('abc', 'a') !== false;
+$hasA = str_contains('abc', 'a');

8. New str_starts_with() Function

-$isMatch = substr($haystack, 0, strlen($needle)) === $needle;
+$isMatch = str_starts_with($haystack, $needle);

9. New str_ends_with() Function

-$isMatch = substr($haystack, -strlen($needle)) === $needle;
+$isMatch = str_ends_with($haystack, $needle);

10. New Stringable Interface for

-class Name
+class Name implements Stringable
 {
-    public function __toString()
+    public function __toString(): string
     {
         return 'I can stringz';
     }
 }

Class that implements Stringable can now be used in places, where string type is needed:

function run(string $anyString)
{
   // ...
}

$name = new Name('Kenny');
run($name);

11. From Union docblock types to Union PHP Declarations

 class SomeClass
 {
-    /**
-     * @param array|int $number
-     * @return bool|float
-     */
-    public function go($number)
+    public function go(array|int $number): bool|float
     {
         // ...
     }
 }

12. Symfony Annotations to Attributes

 use Symfony\Component\Routing\Annotation\Route;

 class SomeController
 {
-   /**
-    * @Route(path="blog/{postSlug}", name="post")
-    */
+    #[Route(path: 'blog/{postSlug}', name: 'post')]
     public function __invoke(): Response
     {
         // ...
     }
 }

13. From Doctrine Annotations to Attributes

-use Doctrine\Common\Annotations\Annotation\Target;
+use Attribute;
 use Symfony\Component\Validator\Constraint;

-/**
- * @Annotation
- * @Target({"PROPERTY", "ANNOTATION"})
- */
+#[Attribute(Attribute::TARGET_PROPERTY)]
 final class PHPConstraint extends Constraint
 {
 }

Then use in code with attributes:

 final class DemoFormData
 {
-    /**
-     * @PHPConstraint()
-     */
+    #[PHPConstraint]
     private string $content;
-
-    /**
-     * @PHPConstraint()
-     */
+    #[PHPConstraint]
     private string $config;

    // ...

Don't bother with any of the steps above. Let Rector handle it.

Update Dockerfile

Do you use Docker? Upgrade images to new PHP version:

 ####
 ## Base stage, to empower cache
 ####
-FROM php:7.4-apache as base
+FROM php:8.0-apache as base

GitHub Actions

Update shivammathur/setup-php@v2 in your workflows:

 jobs:
     unit_tests:
         runs-on: ubuntu-latest
         steps:
             -   uses: actions/checkout@v4
             -   uses: shivammathur/setup-php@v2
                 with:
+                    php-version: 7.4
-                    php-version: 8.0

Skip incompatible Coding Standard rules

These 3 rules are not compatible with PHP 8 yet. So better skip them in ecs.php:

  • PhpCsFixer\Fixer\ClassNotation\ClassAttributesSeparationFixer
  • PhpCsFixer\Fixer\Operator\BinaryOperatorSpacesFixer
  • PhpCsFixer\Fixer\Operator\TernaryOperatorSpacesFixer
  • SlevomatCodingStandard\Sniffs\Classes\DisallowMultiPropertyDefinitionSniff

Resolve Hard Conflicts With composer install

Some packages didn't get to update their composer.json. Be nice and help your fellow developers with a small pull-request:

 {
     "require": {
-        "php": "^7.3"
+        "php": "^7.3|^8.0"
     }
 }

Other packages block PHP 8 upgrades from their own maintainers' ideology, even though the code runs on PHP 8. Watch an excellent 15-min video about this by Nikita Popov, the most active PHP core developer, and Nikolas Grekas, the same just for Symfony.

But ideology is not why we're here. We want to upgrade our project to PHP 8. Thanks to Composer 2, this can be easily solved:

-composer install
+composer update --ignore-platform-req php

Upgrade your CI workflows and Docker build scripts, and you're ready to go.


Happy coding!