Symfony 6: PHP 8 Native Types & Why we Need YOU

A very exciting time is coming with the biggest change for Symfony since Symfony 2.0: Symfony 6 has native PHP types on all its methods where it is possible. This will be a great push towards type safety in the PHP open source communities! Nicolas and Alexander have been working on and off for 2 years to create the best upgrade experience possible.
Now, 2.5 months before the stable release, it is YOUR time to shine! Especially if you maintain any open source project (not even directly linked to Symfony), we would love to hear from you to make sure the upgrade isn’t dramatically hard.

As of Symfony 5.4, the debug class loader (which is used in Symfony applications in the dev/test env) will check return type compatibility and warn you if a method is incompatible. Even more… it can fix it automatically for you! Read more about this feature in the Symfony docs.

Symfony Applications Upgrade Plan

The upgrade plan is slightly different for open source packages and applications. This section describes your upgrade plan if you’re building applications using the Symfony framework (or only some components).

The techniques and tools created to make this experience as smooth as possible are brand-new. Please install 5.4-dev now and let us know about any issue.

1
2
3
$ composer config minimum-stability dev
# now, modify all Symfony constraints to "^5.4" in "composer.json", then:
$ composer update 'symfony/*'

When upgrading to Symfony 5.4

First, upgrade to Symfony 5.4. Besides fixing all “normal” deprecations, you also need to fix some type declarations.

In PHP, it is possible to define a return type when the parent declaration doesn’t define it (yet):

1
2
3
4
5
6
7
8
9
10
11
12
13
interface UserInterface
{
    public function getRoles();
}

class User implements UserInterface
{
    // valid!
    public function getRoles(): array
    {
        // ...
    }
}

At the other hand, you must define a return type if your parent does (remember, Symfony 6 will define return types!). This means that 5.4 is your time to add return types to all methods (especially those overriding or implementing methods from Symfony).

Symfony provides a small utility in the symfony/error-handler component to automatically add the required return types. First, generate a complete classmap of your application using Composer. Then, run the utility:

1
2
3
4
5
6
7
# (1) Make sure "exclude-from-classmap" is not set in your "composer.json"

# (2) Generate the classmap ("-o" is important!)
$ composer dump-autoload -o

# (3) Run the patch script
$ ./vendor/bin/patch-type-declarations

Once you’ve fixed them (and running this script above will do it all for you), you’re ready to upgrade to Symfony 6!

When using Symfony 6.x

After upgrading to Symfony 6.0, you can start adding parameter types. Parameter type compatibility is inverted compared to return types: a child may not set the type if the parent already does, but a child cannot set the type before the parent:

1
2
3
4
5
6
7
8
9
10
11
12
13
interface FormTypeGuesserInterface
{
    public function guessType($class, $property);
}

class CustomTypeGuesser implements UserInterface
{
    // error!
    public function guessType(string $class, string $property)
    {
        // ...
    }
}

For this reason, you cannot add these parameter types in 5.4.

Symfony Bundle Maintainers Compatibility Plan

Unfortunately, open source maintainers can experience some more trouble when adding return types. This is because you probably care about not breaking backwards compatibility. And, as noted before, defining a return type means all users overriding/implementing the method must define it as well.

Document the Return Type

First, you can add return types only on safe methods. These are methods that should not be extended by users of your package. The patch tool from Symfony defines safe methods as any method that:

  • Is in the Tests namespace
  • Is final or @final (or its class)
  • Is @internal (or its class)
  • Is private

Use force=1 to only patch types for these safe methods:

1
2
3
# you can set the minimum PHP version, e.g. "static" won't be added
# as a return type for 7.4
$ SYMFONY_PATCH_TYPE_DECLARATIONS="force=1&php=7.4" ./vendor/bin/patch-type-declarations

Now, you have to check if there are any methods that would produce an error in Symfony 6. The quickest way to do this is running the same script again. Any deprecation in the output is an incompatible method.

If you don’t see any deprecation: Congratulations! You are compatible with Symfony 6 and can allow ^6.0 to your Symfony dependencies without having to break compatibility for your users.

Fix all Return Types

Some bundles may discover that they need to add return types to methods that might be overridden or implemented. As this causes a backwards compatibility break, you probably don’t want to do this in a minor release.

Instead, make sure you properly document the correct return type using the @return PHPDoc. This will create the useful deprecations for your users to know they have to update their PHP return types. The patch script can add this PHPDoc for methods that implement/override from third party classes/interfaces:

1
$ SYMFONY_PATCH_TYPE_DECLARATIONS=force=phpdoc ./vendor/bin/patch-type-declarations

After this minor release with all deprecations, you can safely add all the missing return types in the next major version.

The downside of this process is that you cannot support Symfony 6 until this major release of your own package. Make sure you keep on reading to know how you can maybe even avoid this!

We need YOU to make everyone’s life better

As you can see in this post, adding return types can have quite an impact on the community. Some return types might cause many packages to release a new major version, potentially messing up any roadmaps or other processes.

This is why we welcome you to report any return type deprecation that is causing you to break compatibility in your package. You can do so in this GitHub issue.

We don’t promise anything, but based on the feedback we might postpone some return types to Symfony 7 (to give you 2 more years to release a new major version). We’ve already done so for some common methods found in the Symfony bundles (e.g. Command::execute(): int and VoterInterface::vote(): int).

Take Home’s