How to migrate from Api Platform v2 to v3?
Tags: php, api, symfony, tutorial
Published: 2023-11-29 14:00:00

> This article was co-authored with @sebastientouze 

The API Platform team recently announced that it would be dropping support for version 2.x to focus on v3. Although it has opened a [Crowdfunding to offer Long Term Support on version 2.7](https://opencollective.com/api-platform/projects/27-lts), with Symfony 6.4 being released it is a good idea to prepare to migrate to the latest LTS versions of both Symfony (6.4) and APIPlatform (3.x). 

This article aims to share feedback from the API platform upgrades we've carried out at Theodo, so that you can do so with complete peace of mind. This article is a complement to the update guide and focuses on evolutions to be made on most code bases. For a more in-depth look, you can find the complete list of BC breaks in the [CHANGELOG](https://api-platform.com/docs/changelog/).

## Migrate Api Platform to v2.7 and prepare for v3

### Ensure no regression after the upgrade

Before updating Api Platform, you should make sure that your Api Platform routes are fully covered by tests. You can define integration tests with [PHPUnit](https://phpunit.de/), [Behat](https://docs.behat.org/en/latest/) or [Pest](https://pestphp.com/docs/writing-tests), depending on the tool you’re most comfortable with.

Here is an example of a PHPUnit test that checks that the `/api/books` route returns a `200` HTTP status code:

```php
<?php

namespace App\Tests;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class BookTest extends WebTestCase{    
	public function testGetBooks()
    {
        $client = static::createClient();
        $client->request('GET', '/api/books');
	      $this->assertEquals(200, $client->getResponse()->getStatusCode());
        // Add additional assertions here depending on your needs
    }
}
```

For more information about testing Api Platform routes, see the [Testing](https://api-platform.com/docs/distribution/testing/) section of the documentation.


> Note: All your routes should be covered by tests before updating Api Platform. If you don’t have tests yet, we strongly recommend that you write them before updating Api Platform.



A good way to be sure all your routes are covered is to use the `--coverage` option for Pest, or `--coverage-clover` ([or any other format](https://docs.phpunit.de/en/10.4/textui.html#code-coverage)) for PHPUnit. Keep in mind that Api Platform routes are not directly defined in your code, but your custom filters, custom controllers and any other additional logic will be. 

To list all available routes, you can use the Symfony command `bin/console debug:router`.

### Follow Api Platform’s upgrade guide

Api Platform v2.7 is a transition version between v2 and v3. It contains all the features of v3 which you can enable using a flag. All the features that will be removed in v3 are marked as deprecated. It is a good way to migrate your application to v3 progressively.

Api Platform provides a [migration guide](https://api-platform.com/docs/core/upgrade-guide/). It contains a lot of useful information to migrate your application and lists most of the BC breaks and deprecations.

The key steps are:

- Update Api Platform to v2.7 in your `composer.json` file
- Run the command [api:upgrade-resource](https://api-platform.com/docs/core/upgrade-guide/#the-upgrade-command) to let Api Platform handle most of the BC breaks for you.
- Handle the remaining deprecations listed in the guide and fix the BC breaks

>    Note: You should not change the value of the <code>metadata_backward_compatibility_layer</code> to <code>false</code> yet, as it may break your application. You can change it to false once you have fixed all the BC breaks described below.

### Ensure all resources are correctly migrated

After running the upgrade command, you should check that all your resources are correctly migrated.

#### Check that all routes are still available

You can do so by running the command [api:openapi:export](https://api-platform.com/docs/v2.7/core/openapi/#using-the-openapi-command) (with the option `--output=swagger_docs.json`) and comparing the generated output file with the previous version you had.

#### Check that all routes are declared in the right order

After the upgrade, you should check that all your routes are declared in the right order. With the new update, the order of the routes becomes important.

For example, if you had multiple routes defined for the same operation, with the following order:

```php
/**
 * @ApiResource(
 *     collectionOperations={
 *         "get",
 *         "export"": {
 *             "method": "GET",
 *             "path": "/invoices/generate-recap",
 *             "pagination_enabled": false,
 *             "controller": GenerateRecapInvoiceController::class,
 *         },
 *     },
 * )
 */
```

After running the upgrade command, the `export` route will become a `GetCollection` operation just like the `get` route. The `export` route will be declared after the `get` route and will therefore never be called:

```php
#[ApiResource(
    operations: [
        new GetCollection(),
        new GetCollection(
            uriTemplate: '/invoices/generate-recap',
            paginationEnabled: false,
            controller: GenerateRecapInvoiceController::class
        ),
    ],
)]
```

To fix this issue, you should declare the `export` route, with the most specific route pattern first:

```diff
#[ApiResource(
    operations: [
-       new GetCollection(),
        new GetCollection(
            uriTemplate: '/invoices/generate-recap',
            paginationEnabled: false,
            controller: GenerateRecapInvoiceController::class 
       ),
+       new GetCollection(),
    ],
)]
```

That error can be hard to spot, especially if you have a lot of routes. This is why integration tests are important for each of your routes.

#### Migrate your custom Api Platform Event Subscribers

If you had custom Api Platform Event Subscribers to implement some additional logic on your routes and relied on request attributes, you should update them to use the new behavior.

Attributes `_api_item_operation_name`, `_api_collection_operation_name`, `_api_subresource_operation_name` are deprecated and replaced by `_api_operation_name` and `_api_operation` and will become unavailable when you will switch the `metadata_backward_compatibility_layer` flag to `false`.

That logic is implemented in the `ApiPlatform\Util\AttributesExtractor.php` class, which has been updated and that you can use to retrieve the `operation_name` and the `operation`.

If you already used the `ApiPlatform\Core\Util\AttributesExtractor.php` class, you should update it to use the new one and pay attention to the following changes (from the apiplatform-core github repository):

```diff
// ApiPlatform\Util\AttributesExtractor.php (2.6 -> 2.7)
-        foreach (OperationType::TYPES as $operationType) {
-            $attribute = "_api_{$operationType}_operation_name";
-            if (isset($attributes[$attribute])) {
-                $result["{$operationType}_operation_name"] = $attributes[$attribute];
-                $hasRequestAttributeKey = true;
-                break;
+        if (isset($attributes['_api_operation_name'])) {
+            $hasRequestAttributeKey = true;
+            $result['operation_name'] = $attributes['_api_operation_name'];
+        }
+        if (isset($attributes['_api_operation'])) {
+            $result['operation'] = $attributes['_api_operation'];
+        }
+
+        // TODO: remove in 3.0
+        if (!isset($result['operation']) || ($result['operation']->getExtraProperties()['is_legacy_resource_metadata'] ?? false) || ($result['operation']->getExtraProperties()['is_legacy_subresource'] ?? false)) {
+            foreach (OperationType::TYPES as $operationType) {
+                $attribute = "_api_{$operationType}_operation_name";
+                if (isset($attributes[$attribute])) {
+                    $result["{$operationType}_operation_name"] = $attributes[$attribute];
+                    $hasRequestAttributeKey = true;
+                    break;
+                }
```

#### Route names

If you relied on route names, note that they have also been changed as part of this update. A route declared as `api_books_get_collection` in 2.6 is now declared as `_api_/books.{_format}_get_collection` in 2.7.

#### Migrate your custom route Controllers

If you had custom route Controllers and used a path parameter to retrieve the resource, you should update them to use the new behavior. The path parameter must now be named `id`.

```diff
#[ApiResource(
    operations: [
        new Post(
-            uriTemplate: '/authors/{selectedAuthor}/book',
+            uriTemplate: '/authors/{id}/book',
            controller: CreateBookLinkedToAuthorController::class,
        ),
    ],
)]
```

#### Migrate from DataProviders and DataPersisters to Providers and Processors

If you had custom DataProviders and DataPersisters, you should update them to use the new Providers and Processors. The same applies to custom DataTransformers, which can be replaced by a combination of a Processor and a Provider.

Refer to the [state Providers](https://api-platform.com/docs/v2.7/core/state-providers/) and [state Processors](https://api-platform.com/docs/v2.7/core/state-processors/) sections of the documentation for more information.

You can find an [example of a DataProvider implementation on our blog](https://blog.theodo.com/2023/04/filter-subresources-with-api-platform/).

#### Change the value of the `metadata_backward_compatibility_layer` flag

You’re all set to migrate to Api Platform v3! 

The last step is to update the `metadata_backward_compatibility_layer` flag to `false` in your `api_platform.yaml` file and ensure that all your tests are still passing.

## Migrate to Api Platform v3

Now, you are ready to migrate to Api Platform v3!

We recommend you migrate to the latest stable version of Api Platform v3. You can find the latest stable version and the latest changes in the [CHANGELOG](https://api-platform.com/docs/changelog/).

### Prerequisites

Api Platform v3 requires the latest version of PHP and Symfony, you will need to update both before switching to the latest API Platform version.

| Api Platform version | PHP Version | Symfony Version |
| --- | --- | --- |
| 2.7 | ≥ 7.1 | ^4.4 || ^5.1 || ^6.0 |
| 3.0, 3.1, 3.2 | ≥ 8.1 | ^6.1 |

While only version 8.1 for PHP and 6.1 for Symfony are required, we recommend updating to the latest version that offers Long Term Support. You can find information on actively supported versions here:

- [PHP Versions](https://www.php.net/supported-versions.php)
- [Symfony Versions](https://symfony.com/releases)

For instance, there are [cache problems on Api Platform v3.0](https://github.com/api-platform/core/issues/5285), so you should consider upgrading directly to the latest minor version available.

### Update the bundle declaration

Update the Api Platform bundle declaration in your `config/bundles.php` file:

```diff
return [
    // ...
-   ApiPlatform\Core\Bridge\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true],
+   ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true],
]
```

The bundle has been moved from `ApiPlatform\Core\Bridge\Symfony\Bundle\ApiPlatformBundle` to `ApiPlatform\Symfony\Bundle\ApiPlatformBundle`.

### Check all imports still work

Now that you have migrated to Api Platform v3, you should check that all your imports still work. Here are some imports that have changed and are not handled by the upgrade command:

```diff
- use ApiPlatform\Core\Action\NotFoundAction;
+ use ApiPlatform\Action\NotFoundAction;

- use ApiPlatform\Core\Annotation\ApiProperty;
+ use ApiPlatform\Metadata\ApiProperty;

- use ApiPlatform\Core\EventListener\EventPriorities;
+ use ApiPlatform\Symfony\EventListener\EventPriorities;

- use ApiPlatform\Core\JsonApi\Serializer\CollectionNormalizer;
+ use ApiPlatform\JsonApi\Serializer\CollectionNormalizer;
```

### Update types on Api Platform classes you extend

Some return types and parameter types have changed on Api Platform classes and / or on Symfony classes. You should update them in your classes that extend them.

For instance, in your custom filters, you should update the type of the `$value` parameter in the `filterProperty` method to `mixed`:

```diff
    /**
     * @param mixed[] $context
     */
    protected function filterProperty(
        string $property,
-        $value,
+        mixed $value,
        QueryBuilder $queryBuilder,
        QueryNameGeneratorInterface $queryNameGenerator,
        string $resourceClass,
-        string $operationName = null,
+        Operation $operation = null,
        array $context = []
    ): void {
        / ...
    }
```

### Update your frontend types

One of the main changes brought about by Api Platform v3 is that null is no longer one of the default return types. This may have consequences for your front-end, especially if you use TypeScript. You may need to update your types to take null values into account.

According to [Api Platform’s upgrade guide](https://api-platform.com/docs/core/upgrade-guide/), this follows a JSON Merge Patch RFC:


>    In 3.0, in conformance with the JSON Merge Patch RFC, the default value of the <code>skip_null_values</code> property is true which means that from now on null values are omitted during serialization. 


For instance, on a UserMetadata object, frontend types should be updated to the following:

```diff
export type UserMetadata = {
  id: string;
  fullName: string;
-  firstName: string | null;
-  lastName: string | null;
-  phoneNumber: string | null;
+  firstName?: string;
+  lastName?: string;
+  phoneNumber?: string;
};
```

## Summary of a successful migration from API Platform v2 to v3

Transitioning from API Platform v2 to v3 is a progressive process, which involves first upgrading to the transitional v2.7, ensuring all relevant routes are covered by tests, and then gradually migrating to v3. 

This process requires careful attention to changes in route names and imports, behavior of custom controllers, changes in Providers and Processors, bundle declarations, among other breaking changes. 

Once these steps are completed, it is also crucial to verify that front-end types are updated to account for the change in default return types. 

After completing this process, your API should be fully updated and ready to benefit from the features and improvements in API Platform v3.

This migration is also a good opportunity to check that all your routes are secure. To do so, we recommend reading our [article on Access Control](https://blog.theodo.com/2023/10/ensure-that-symfony-routes-have-access-control/), which is compatible with v3!