Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Upgrading to PHP 8

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $10.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Let's keep track of our goal. Now that we've upgraded to Symfony 5.4, as soon as we remove all of these deprecations, we can safely upgrade to Symfony 6. But Symfony 6 requires PHP 8, and I've been building this project in PHP 7. So the next step is to update our code to be PHP 8 compatible. In practice, that means updating parts of our code to use some cool new PHP 8 features. Woo! And this is another spot where Rector can help us.

Rector Upgrading To PHP 8!

Start by opening up rector.php and removing the three Symfony upgrade lines. Replace these with LevelSetList::UP_TO_PHP_80. Just like with Symfony, you can upgrade specifically to PHP 7.3 or 7.4, but they have these nice UP_TO_[...] statements that will upgrade our code across all versions of PHP up to PHP 8.0.

31 lines rector.php
... lines 1 - 12
return static function (ContainerConfigurator $containerConfigurator): void {
... lines 14 - 22
$containerConfigurator->import(LevelSetList::UP_TO_PHP_80);
... lines 24 - 29
};

And... that's all we need!

Over at your terminal, I've committed all of my changes, except for the one we just made. So now we can run:

vendor/bin/rector process src

Cool! Let's walk through some of these changes. If you want to go deeper, search for a getrector.org blog post, which shows you how to do what we just did... but also gives you more information about what Rector did and why.

For example, one of the changes that it makes is replacing switch() statements with a new PHP 8 match() function. This explains that... and many other changes. Oh, and the vast majority of these changes aren't required: you don't have to do them to upgrade to PHP 8. They're just nice.

PHP 8 Property Promotion

The most important change, which is coincidentally the most common, is something called "Promoted Properties". This is one of my favorite features in PHP 8, and you can see it right here. In PHP 8, you can add a private, public, or protected keyword right before an argument in the constructor... and that will both create that property and set it to this value. So you no longer need to add a property manually or set it below. Just add private and... done!

... lines 1 - 8
class MarkdownExtension extends AbstractExtension
{
public function __construct(private MarkdownHelper $markdownHelper)
{
}
... lines 14 - 28
}

The vast majority of the changes in this file are exactly that... here's another example in MarkdownHelper. Most of the other changes are minor. It altered some callback functions to use the new short => syntax, which is actually from PHP 7.4.

... lines 1 - 8
class MarkdownHelper
{
... lines 11 - 14
public function parse(string $source): string
{
... lines 17 - 24
return $this->cache->get('markdown_'.md5($source), fn() => $this->markdownParser->transformMarkdown($source));
}
}

You can also see, down here, an example of refactoring switch() statements to use the new match() function.

... lines 1 - 11
class QuestionVoter extends Voter
{
... lines 14 - 24
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool
{
... lines 27 - 40
return match ($attribute) {
'EDIT' => $user === $subject->getOwner(),
default => false,
};
}
}

All of this is optional, but it's nice that our code has been updated to use some of the new features. If I scroll down just a little more, you'll see more of these.

Entity Property Types?

Oh, and inside of our entities, notice that, in some cases, it added property types! For $roles, this property is initialized to an array. Rector realized that... so it added the array type.

... lines 1 - 15
class User implements UserInterface
{
... lines 18 - 29
/**
* @ORM\Column(type="json")
*/
private array $roles = [];
... lines 34 - 226
}

In other cases, like $password, it saw that we have PHPDoc above it, so it added the type there as well.

... lines 1 - 15
class User implements UserInterface
{
... lines 18 - 34
/**
* @var string The hashed password
* @ORM\Column(type="string")
*/
private string $password;
... lines 40 - 226
}

Though, this is a little questionable. The $password could also be null.

Open up src/Entity/User.php and scroll down to $password. Rector gave this a string type... but that's wrong! If you look at the constructor down here, we don't initialize $password to any value... which means it will start null. So the correct type for this is a nullable ?string. The reason Rector did this wrong is... well.. because I had a bug in my documentation!. This should be string|null

... lines 1 - 15
class User implements UserInterface
{
... lines 18 - 34
/**
* @var string|null The hashed password
* @ORM\Column(type="string")
*/
private ?string $password = null;
... lines 40 - 226
}

One of the biggest changes that I've been doing in my code over the past year or so since PHP 7.3 was released, has been adding property types like this, both in my entity classes and also my service classes. If this was a little confusing, don't worry. We're going to talk more about property types inside of entities in a few minutes. You can see that Rector added some, but a lot of our properties are still missing them.

Setting PHP 8 in composer.json

Okay, our code should now be ready for PHP 8. Yay! So let's go upgrade our dependencies for PHP 8. In composer.json, under the require key, it currently says that my project works with PHP 7.4 or 8. I'm going to change that to just say "php": "^8.0.2", which is the minimum version for Symfony 6.0.

111 lines composer.json
{
... lines 2 - 5
"require": {
"php": "^8.0.2",
... lines 8 - 45
},
... lines 47 - 109
}

By the way, Symfony 6.1 requires PHP 8.1. So if you're going to upgrade to that pretty soon, you could jump straight to 8.1.

There's one other thing I have down here near the bottom. Let's see... here we go. On config, platform, I have PHP set to 7.4. That ensures that if someone is using PHP 8, Composer will still make sure it downloads dependencies compatible with PHP 7.4. Change this to 8.0.2.

111 lines composer.json
{
... lines 2 - 56
"config": {
... lines 58 - 61
"platform": {
"php": "8.0.2"
},
... lines 65 - 68
},
... lines 70 - 109
}

Sweet! And now, because we're using PHP 8 in our project, there's a good chance some dependencies will be eligible for updates. Run:

composer up

And... yeah! There are several. It looks like psr/cache, psr/log, and symfony/event-dispatcher-contracts all upgraded. Most likely all of these new versions require PHP 8. We couldn't upgrade before, but now we can. If we go over to our page and reload... everything still works!

Updating Symfony Flex

One other thing in composer.json is Symfony Flex itself. Flex uses its own version scheme, and the latest version is 2.1. At this moment, Flex version 2 and Flex version 1 are identical... except that Flex 2 requires PHP 8. Since we're using that, let's upgrade! Change the version to ^2.1... then head back to your terminal and run:

composer up

one more time. Beautiful!

All right, team! Our project is now using PHP 8. To celebrate, let's refactor from using annotations to PHP 8 native attributes. OOOoo. I love this change... in part because Rector makes it super easy.

Leave a comment!

4
Login or Register to join the conversation
Stéphane B. Avatar
Stéphane B. Avatar Stéphane B. | posted 4 months ago

If you have this error => Attempted to call an undefined method named "getCacheDriver" of class "Doctrine\Bundle\DoctrineBundle\Mapping\ClassMetadataFactory" and you version of SF is 5.4.8. Do composer req doctrine/persistence ^2.4 and the problem is solve

2 Reply

Hey DrTactile,

Thanks for sharing this tip with others!

Cheers!

1 Reply
Default user avatar
Default user avatar Enrique Olivares | posted 2 months ago

The rector.php file in Chapter 3 is different than the one in Chapter 9. Should I really only be replacing the one line or the entire file?

Reply

Hey Enrique,

I double-checked the code block in this chapter https://symfonycasts.com/sc... - it looks exactly like the code in the video, but also contains a few comments below. But because the commented code below isn't executed anyway, the actual code matches the video. So, you can basically use the whole code block's code for this chapter.

Cheers!

1 Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^8.0.2",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.6", // v3.6.1
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.5
        "doctrine/annotations": "^1.13", // 1.13.2
        "doctrine/dbal": "^3.3", // 3.3.5
        "doctrine/doctrine-bundle": "^2.0", // 2.6.2
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.0", // 2.11.2
        "knplabs/knp-markdown-bundle": "^1.8", // 1.10.0
        "knplabs/knp-time-bundle": "^1.18", // v1.18.0
        "pagerfanta/doctrine-orm-adapter": "^3.6", // v3.6.1
        "pagerfanta/twig": "^3.6", // v3.6.1
        "sensio/framework-extra-bundle": "^6.0", // v6.2.6
        "sentry/sentry-symfony": "^4.0", // 4.2.8
        "stof/doctrine-extensions-bundle": "^1.5", // v1.7.0
        "symfony/asset": "6.0.*", // v6.0.7
        "symfony/console": "6.0.*", // v6.0.7
        "symfony/dotenv": "6.0.*", // v6.0.5
        "symfony/flex": "^2.1", // v2.1.7
        "symfony/form": "6.0.*", // v6.0.7
        "symfony/framework-bundle": "6.0.*", // v6.0.7
        "symfony/mailer": "6.0.*", // v6.0.5
        "symfony/monolog-bundle": "^3.0", // v3.7.1
        "symfony/property-access": "6.0.*", // v6.0.7
        "symfony/property-info": "6.0.*", // v6.0.7
        "symfony/proxy-manager-bridge": "6.0.*", // v6.0.6
        "symfony/routing": "6.0.*", // v6.0.5
        "symfony/runtime": "6.0.*", // v6.0.7
        "symfony/security-bundle": "6.0.*", // v6.0.5
        "symfony/serializer": "6.0.*", // v6.0.7
        "symfony/stopwatch": "6.0.*", // v6.0.5
        "symfony/twig-bundle": "6.0.*", // v6.0.3
        "symfony/ux-chartjs": "^2.0", // v2.1.0
        "symfony/validator": "6.0.*", // v6.0.7
        "symfony/webpack-encore-bundle": "^1.7", // v1.14.0
        "symfony/yaml": "6.0.*", // v6.0.3
        "symfonycasts/verify-email-bundle": "^1.7", // v1.10.0
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.8
        "twig/string-extra": "^3.3", // v3.3.5
        "twig/twig": "^2.12|^3.0" // v3.3.10
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.1
        "phpunit/phpunit": "^9.5", // 9.5.20
        "rector/rector": "^0.12.17", // 0.12.20
        "symfony/debug-bundle": "6.0.*", // v6.0.3
        "symfony/maker-bundle": "^1.15", // v1.38.0
        "symfony/var-dumper": "6.0.*", // v6.0.6
        "symfony/web-profiler-bundle": "6.0.*", // v6.0.6
        "zenstruck/foundry": "^1.16" // v1.18.0
    }
}