How to migrate from Api Platform v2 to v3?

greg-ab

Grégoire Abachin

Posted on November 29, 2023

How to migrate from Api Platform v2 to v3?

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, 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.

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, Behat or Pest, 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

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
    }
}
Enter fullscreen mode Exit fullscreen mode

For more information about testing Api Platform routes, see the 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) 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. 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 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 metadata_backward_compatibility_layer to false 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 (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:

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

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:

#[ApiResource(
    operations: [
        new GetCollection(),
        new GetCollection(
            uriTemplate: '/invoices/generate-recap',
            paginationEnabled: false,
            controller: GenerateRecapInvoiceController::class
        ),
    ],
)]
Enter fullscreen mode Exit fullscreen mode

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

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

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):

// 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;
+                }
Enter fullscreen mode Exit fullscreen mode

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.

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

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 and state Processors sections of the documentation for more information.

You can find an example of a DataProvider implementation on our blog.

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.

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
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:

For instance, there are cache problems on Api Platform v3.0, 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:

return [
    // ...
-   ApiPlatform\Core\Bridge\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true],
+   ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true],
]
Enter fullscreen mode Exit fullscreen mode

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:

- 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;
Enter fullscreen mode Exit fullscreen mode

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:

    /**
     * @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 {
        / ...
    }
Enter fullscreen mode Exit fullscreen mode

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, this follows a JSON Merge Patch RFC:

In 3.0, in conformance with the JSON Merge Patch RFC, the default value of the skip_null_values 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:

export type UserMetadata = {
  id: string;
  fullName: string;
-  firstName: string | null;
-  lastName: string | null;
-  phoneNumber: string | null;
+  firstName?: string;
+  lastName?: string;
+  phoneNumber?: string;
};
Enter fullscreen mode Exit fullscreen mode

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, which is compatible with v3!

💖 💪 🙅 🚩
greg-ab
Grégoire Abachin

Posted on November 29, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related