Upgrade to Symfony v7
Tags: php, symfony
Published: 2024-02-28 04:37:56

Symfony has just released [Symfony 7.0](https://symfony.com/blog/symfony-7-0-0-released), there are a lot of breaking changes introduced in this version.

In this post, we will use [Symfony Rest Example](https://github.com/hantsy/symfony-rest-sample) project as an example, and show how I upgrade to v7 step by step. 


## Backup Existing Codes

Firstly, let's create a GIT tag on the existing codes, and back up the project. 

```bash
git tag v6.x 
git push origin v6.x
```
For those still use Symfony 6.x, please check the tag `v6.x` or download a copy from the [project tag page](https://github.com/hantsy/symfony-rest-sample/tags). 

Create a new branch to prepare the upgrading work.

```bash
git checkout -b v7
```

## Upgrade Symfony Packages to v7

Import the project source codes into an IDE(PHPStorm or NetBeans IDE) or VSCode( with PHP support).

Open `composer.json` file, change all symfony packages version to `7.0.*`.

```json
{
  "name": "hantsy/symfony-rest-sample",
  "description": "Restful APIs examples built with Symfony 7 and PHP 8",
  "authors": [
    {
      "name": "Hantsy Bai",
      "email": "hantsy@gmail.com"
    }
  ],
  "type": "project",
  "license": "GPL-3.0-or-later",
  "minimum-stability": "stable",
  "prefer-stable": true,
  "require": {
    "php": ">=8.2",
    "ext-ctype": "*",
    "ext-iconv": "*",
    "composer/package-versions-deprecated": "^1.11.99.4",
    "doctrine/annotations": "^2.0",
    "doctrine/doctrine-bundle": "^2.4",
    "doctrine/doctrine-migrations-bundle": "^3.1",
    "doctrine/orm": "^2.9",
    "phpdocumentor/reflection-docblock": "^5.2",
    "symfony/asset": "7.0.*",
    "symfony/console": "7.0.*",
    "symfony/dotenv": "7.0.*",
    "symfony/expression-language": "7.0.*",
    "symfony/flex": "^2.0.1",
    "symfony/framework-bundle": "7.0.*",
    "symfony/monolog-bundle": "^3.7",
    "symfony/property-access": "7.0.*",
    "symfony/property-info": "7.0.*",
    "symfony/runtime": "7.0.*",
    "symfony/serializer": "7.0.*",
    "symfony/uid": "7.0.*",
    "symfony/validator": "7.0.*",
    "symfony/yaml": "7.0.*"
  },
  "config": {
    "optimize-autoloader": true,
    "preferred-install": {
      "*": "dist"
    },
    "allow-plugins": true,
    "sort-packages": true
  },
  "autoload": {
    "psr-4": {
      "App\\": "src/"
    }
  },
  "autoload-dev": {
    "psr-4": {
      "App\\Tests\\": "tests/"
    }
  },
  "replace": {
    "symfony/polyfill-ctype": "*",
    "symfony/polyfill-iconv": "*",
    "symfony/polyfill-php72": "*"
  },
  "scripts": {
    "auto-scripts": {
      "cache:clear": "symfony-cmd",
      "assets:install %PUBLIC_DIR%": "symfony-cmd"
    },
    "post-install-cmd": [
      "@auto-scripts"
    ],
    "post-update-cmd": [
      "@auto-scripts"
    ],
    "test": "php bin/phpunit"
  },
  "conflict": {
    "symfony/symfony": "*"
  },
  "extra": {
    "symfony": {
      "allow-contrib": false,
      "require": "7.0.*"
    }
  },
  "require-dev": {
    "dama/doctrine-test-bundle": "^8.0",
    "doctrine/doctrine-fixtures-bundle": "^3.4",
    "phpunit/phpunit": "^9.5",
    "symfony/browser-kit": "7.0.*",
    "symfony/css-selector": "7.0.*",
    "symfony/maker-bundle": "^1.34",
    "symfony/phpunit-bridge": "7.0.*"
  }
}
```

We remove `sensio/framework-extra-bundle` and `nelmio/cors-bundle` from the package list due to the compatibility with the new v7. 

* The former is deprecated in v7 and not recommended in new projects. We will use new API to replace them.
* The later does not release a Symfony v7 compatible version at the moment.

Remove `composer.lock`, try to run `composer install` command in a terminal to install new packages and rerun the built-in recipes for this project.

## Refresh Codes with New APIs

There are a few classes that need to align with the new APIs in Symfony v7. 

Firstly, we used `ArgumentResolver` in `sensio/framework-extra-bundle` to convert parameters to `Uuid`. In Symfony v7, the `ValueResolver` is the replacement. More details please check [Extending Action Argument Resolving](https://symfony.com/doc/current/controller/value_resolver.html#adding-a-custom-value-resolver). 

And there is a built-in [UidValueResolver](https://github.com/symfony/symfony/blob/7.0/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/UidValueResolver.php) can be used to convert the parameter to `Uuidv4`, so we can give up our custom Uuid converter, and switch to use the official one. 

Simply remove the existing `src/ParamConverter/UuidParamConverter`. 

Next, let's update the `BodyValueResolver` and `QueryParamValueResolver`. The original `ArgumentValueResolverInterface` is removed in v7, the replacement is `ValueResolverInterface`, which is similar to the old `ArgumentValueResolverInterface`, but there is no `supports` method to override.

We adjust the existing codes slightly to make it work seamlessly.

```php
class QueryParamValueResolver implements ValueResolverInterface, LoggerAwareInterface
{
    public function __construct()
    {
    }

    private LoggerInterface $logger;

    /**
     * @inheritDoc
     */
    public function resolve(Request $request, ArgumentMetadata $argument): iterable
    {
        if (!$this->supports($request, $argument)) return [];
        ...
    }    
```

We reuse `supports` method to determine if skip the execution of argument resolving. Similarly, we adjust the `BodyValueResolver` as expected.

Rerun `composer install`, we get an error like the following.

```bash
!!  Symfony\Component\ErrorHandler\Error\FatalError {#307
!!    #message: "Compile Error: Declaration of App\Annotation\Delete::getMethods() must be compatible with Symfony\Component\Routing\Attribute\Route::getMethods(): array"
!!    #code: 0
!!    #file: "D:\hantsylabs\symfony5-sample\src\Annotation\Delete.php"
!!    #line: 11
!!    -error: array:4 [
!!      "type" => 64
!!      "message" => "Declaration of App\Annotation\Delete::getMethods() must be compatible with Symfony\Component\Routing\Attribute\Route::getMethods(): array"
!!      "file" => "D:\hantsylabs\symfony5-sample\src\Annotation\Delete.php"
!!      "line" => 11
!!    ]
!!  }
!!  PHP Fatal error:  Declaration of App\Annotation\Delete::getMethods() must be compatible with Symfony\Component\Routing\Attribute\Route::getMethods(): array in D:\hantsylabs\symfony5-sample\src\Annotation\Delete.php on line 11
```

In Symfony v7, all methods and properties in classes require type declaration. For developers, it is good to write type safe codes.

Symfony provides a simple script to upgrade existing codes and apply type declaration automatically.

```bash 
vendor/bin/patch-type-declarations
```

More information about the type declaration, check [Symfony 7.0 Type Declarations](https://symfony.com/blog/symfony-7-0-type-declarations).

Rerun `composer install`, we get an new error like the following.

```bash
!!  In Loader.php line 63:
!!                                                                                 
!!    Cannot load resource "../../src/Controller/". Make sure there is a loader   
!!    supporting the "annotation" type.   
```

We should use the new attributes config to replace the legacy annotations config for loading controllers. 

1. Remove `config/routes/annotations.yaml`.
2. Create a new file `config/routes/attributes.yaml`, fill in the following content.

    ```yaml
    controllers:
        resource:
            path: ../../src/Controller/
            namespace: App\Controller
        type: attribute

    kernel:
        resource: App\Kernel
        type: attribute
    ```

More information about Route, check [Creating Routes as Attributes](https://symfony.com/doc/current/routing.html#creating-routes-as-attributes).

Now the `composer install` command should be executed successfully.

When running the tests, there are several deprecated warning info.

The `Get`, `Post`, `Put`, `Delete`, etc. that we created as examples of demonstrating attributes now raise warning info like *the `Route` could be final in future*. Thus means in a further version, these custom attributes will not work.

1. Remove the entire `src\Annotations` folder.
2. Open the `PostController`, use original `Route` attribute instead.

Note, if you use `Route` from `Symfony\Component\Routing\Annotation\Route`, change it to use the new `Symfony\Component\Routing\Attribute\Route` instead.

The left another two warning info are from Doctrine configuration.

Open `config/packages/doctrine.yaml`, add the following two lines.

```yaml
doctrine:
    dbal:
        url: '%env(resolve:DATABASE_URL)%'
        use_savepoints: true  // add this line
        ...
    orm:
        auto_generate_proxy_classes: true
        enable_lazy_ghost_objects: true // add this line
        ...
```

I have not researched the new changes in Doctrine, these configuration is provided in the warning info.

Now run the tests, all tests should be passed.

## Upgrade to PHPUnit 10

Run the following command to update PHPUnit to 10.x.

```bash
composer recipes:update symfony/phpunit-bridge
```

Then update the PHPUnit packages to `^10.0`.

```bash 
composer install
```

When running the tests, there are some deprecation information in the test result.

Run the following command to migrate the configuration to PHPUnit 10 compatible format.

```bash
vendor/bin/phpunit --migrate-configuration
```
You will find the Symfony test listener is removed by this script, because `listener` is not a valid element in PHPUnit 10 configuration. Let's wait for a new replacement in a further version.

Manually add a `source` element into the *phpunit.xml.dist*. 

```xml
  <source>
    <include>
        <directory suffix=".php">src</directory>
    </include>
  </source>
```

Now run the tests again, all tests should be passed.

## Update PHP versions on Github Actions

Finally, we should update the PHP version in Github actions workflow file, drop PHP 8.1 from the versoin list, and add PHP 8.3 as build environment. PHP 8.1 is no longer supported in the new Symfony v7.

The working codes is available via my Github, check out the [symfony-rest-sample](https://github.com/hantsy/symfony-rest-sample) and explore it yourself.