API Platform

third act.

Mathias Arlaud

@matarld
mtarld
les-tilleuls.coop

A spider was born.​

Act 1
/2015
JsonLdApiBundle

2015

2016

API Platform

Full CRUD, data validation, pagination, sorting, filtering, hypermedia entrypoint, ...

~200k installs

Webby's growing

Act 2
 
 






  Fix a ton of issues


  Various fixes

 

- Full refactoring
​- Use PHP 7
- Add support for content negotiation
- Add Swagger/OpenAPI support
- Add HAL support
- Full rewrite of the metadata system (annotations, YAML and XML formats support)
- Remove the event system in favor of the builtin Symfony kernel's events
- Use the ADR pattern
- Fix a ton of issues
- Properties mapping with XML/YAML is now possible
- Ability to configure and match exceptions with an HTTP status code
- Various fixes and improvements (SwaggerUI, filters, stricter property metadata)
- [...]

 

CHANGELOG

2017

2018

2019

2020

2021

2022

2.1: Subresources
2.2: ApiFilter annotation
2.3: Interfaces as resources
2.4: Messenger integration
2.5: PATCH support
2.6: PHP8 attributes

3.0,

What's new?

1. Operations

2. Subresources

3. DataProviders/DataPersisters

#[ApiResource(
  itemOperations: [...],
  collectionOperations: [...],
)]
final class Author
{
    // ...
}

Hey API Platform! Could you expose this like that?

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;

#[ApiResource(
    itemOperations: [
        'get',
        'patch' => [
            'normalizationContext' => ['groups' => 'admin'],
            'security' => 'is_granted("ROLE_ADMIN")',
        ],
    ],
    collectionOperations: [
        'get',
        'anonymize' => [
            'method' => 'POST',
            'path' => '/author/anonymize',
            'security' => 'is_granted("ROLE_ADMIN")',
            'normalization_context' => ['groups' => 'admin'],
            'inupt' => false,
        ],
    ],
)]
final class Author {}
'patch' => [
  'normalization_context' => ['groups' => 'admin'],
  'security' => 'is_granted("ROLE_ADMIN")',
]
new Patch(
  normalizationContext: ['groups' => 'admin'],
  security: 'is_granted("ROLE_ADMIN")',
)
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\{Get,GetCollection,Patch,Post};

#[ApiResource(
  operations: [
    new Get(),
    new GetCollection(),
    new Patch(
      normalizationContext: ['groups' => 'admin'],
      security: 'is_granted("ROLE_ADMIN")',
    ),
    new Post(
      uriTemplate: '/author/anonymize',
      security: 'is_granted("ROLE_ADMIN")',
      normalizationContext: ['groups' => 'admin'],
      input: false,
    ),
  ],
)]
final class Author {}
No more
item/collection
namespace App\Metadata;

use ApiPlatform\Metadata\HttpOperation;

final class AdminOperation extends HttpOperation 
{
  public function __construct(/* ... */)
  {
    $groups = ($normalizationContext['groups'] ?? [])['admin'];
    $normalizationContext['groups'] = $groups;
    
    parent::__construct(
        // ...
        security: 'is_granted("ROLE_ADMIN")',
        normalizationContext: $normalizationContext,
    );
  }
}
namespace App\Entity;

use App\Metadata\AdminOperation;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\{Get,GetCollection};

#[ApiResource(
  operations: [
    new Get(),
    new GetCollection(),
    new AdminOperation(
      method: 'PATCH',
    ),
    new AdminOperation(
      method: 'POST',
      uriTemplate: '/author/anonymize',
      input: false,
    ),
  ],
)]
final class Author {}
namespace App\Metadata;

use ApiPlatform\Metadata\HttpOperation;

final class AdminOperation extends HttpOperation 
{
  public function __construct(/* ... */)
  {
    $groups = ($normalizationContext['groups'] ?? [])['admin'];
    $normalizationContext['groups'] = $groups;
    
    parent::__construct(
        // ...
        security: 'is_granted("ROLE_ADMIN")',
        normalizationContext: $normalizationContext,
    );
  }
}
namespace App\Entity;

use App\Metadata\AdminOperation;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\{Get,GetCollection};

#[ApiResource]
#[Get]
#[GetCollection]
#[AdminOperation(
    method: 'PATCH',
    normalizationContext: ['groups' => 'read'],
)]
#[AdminOperation(
    method: 'POST',
    uriTemplate: '/author/anonymize',
    input: false,
)]
final class Author {}
namespace App\Entity;

use App\Metadata\AdminOperation;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\{Get,GetCollection};

#[ApiResource(
  operations: [
    new Get(),
    new GetCollection(),
    new AdminOperation(
      method: 'PATCH',
      normalizationContext: ['groups' => 'read'],
    ),
    new AdminOperation(
      method: 'POST',
      uriTemplate: '/author/anonymize',
      input: false,
    ),
  ],
)]
final class Author {}

=

PHP8.1

PHP8.0

2. Subresources

1. Operations

3. DataProviders/DataPersisters

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiSubresource;

#[ApiResource]
final class Author
{
  #[ApiSubresource]
  private Collection $books;
}

Author

Book

/books

/authors

/authors/{authorId}/books

/authors/{id}/books

ApiResource

ApiSubresource

ApiSubresource

namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Link;

#[ApiResource]
#[ApiResource(
    uriTemplate: '/authors/{authorId}/books',
    uriVariables: [
        'authorId' => new Link(fromClass: Author::class, toProperty: 'author'),
    ],
    operations: [new GetCollection(), new Post()],
)]
#[ApiResource(
    uriTemplate: '/authors/{authorId}/books/{id}',
    uriVariables: [
        'authorId' => new Link(fromClass: Author::class, toProperty: 'author'),
        'id' => new Link(fromClass: Book::class),
    ],
    operations: [new Get(), new Put(), new Patch(), new Delete()],
)]
final class Book
{
}
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Link;

#[ApiResource]
#[ApiResource(
    uriTemplate: '/authors/{authorId}/books',
    uriVariables: [
        'authorId' => new Link(fromClass: Author::class, toProperty: 'author'),
    ],
    operations: [new GetCollection(), new Post()],
)]








final class Book
{
}
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Link;

#[ApiResource]















final class Book
{
}

3. DataProviders/DataPersisters

1. Operations

2. Subresources

ItemDataProviderInterface

CollectionDataProviderInterface

SubresourceDataProviderInterface

RestrictedDataProviderInterface

ContextAwareItemDataProviderInterface

ContextAwareCollectionDataProviderInterface

DataPeristerInterface

ContextAwareDataPeristerInterface

ResumableDataPersisterInterface

ProviderInterface

ProcessorInterface

PATCH /books/1

FooItemDataProvider
DoctrineItemDataProvider
ElasticItemDataProvider
GraphqlItemDataProvider
FooDataPersister
GraphqlDataPersister
DoctrineDataPersister
namespace App\Entity;

use App\State\BookProcessor;
use App\State\BookProvider;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Patch;

#[ApiResource(
  operations: [
    new Patch(
      provider: BookProvider::class,
      processor: BookProcessor::class,
    ),
  ],
)]
final class Book {}
namespace App\Entity;

use ApiPlatform\Doctrine\Orm\State\ItemProvider;
use ApiPlatform\Doctrine\Common\State\PersistProcessor;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Patch;

#[ApiResource(
  operations: [
    new Patch(
      provider: ItemProvider::class,
      processor: PersistProcessor::class,
    ),
  ],
)]
final class Book {}
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Patch;

#[ApiResource(
  operations: [
    new Patch(),
  ],
)]
final class Book {}

=

namespace App\State;

use App\Dto\AnotherBookRepresentation;
use ApiPlatform\Metadata\CollectionOperationInterface;
use ApiPlatform\State\ProcessorInterface;

final class BookProvider implements ProviderInterface
{
  public function provide(Operation $operation, array $uriVariables = [], array $context = [])
  {
    if ($operation instanceof CollectionOperationInterface) {
      return [new Book(), new Book()];
    }
    
    return new Book();
  }
}
namespace App\State;

use App\Dto\AnotherBookRepresentation;
use ApiPlatform\Metadata\DeleteOperationInterface;
use ApiPlatform\State\ProcessorInterface;

final class BookProcessor implements ProcessorInterface
{
  public function process($data, Operation $operation, array $uriVariables = [], array $context = [])
  {
    if ($operation instanceof DeleteOperationInterface) {
      // delete the book
    }
    
    // persist the book
  }
}
'get_another_representation' => ['output' =>    ]
GET /books_other/{id}

final class BookDataTransformer
{
  public function transform(   )
  {
    return    ;
  }
}
final class BookItemDataProvider
{
  public function getItem(...)
  {
    return    ;
  }
}
new GetCollection(output:    )
GET /books_other/{id}

final class AnotherReprenstationProvider
{
  public function provide(...)
  {
    return    ;
  }
}

Road to the,

third act!

1. Install the 2.7 version

$ composer require api-platform/core:^2.7

./composer.json has been updated
Running composer update api-platform/core
Loading composer repositories with package information
Updating dependencies

Lock file operations: 0 installs, 1 update, 0 removals
  - Upgrading api-platform/core (v2.6.8 => v2.7.0-beta.1)
Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 0 installs, 1 update, 0 removals
  - Upgrading api-platform/core (v2.6.8 => v2.7.0-beta.1): Extracting archive

Generating optimized autoload files
60 packages you are using are looking for funding.

1. Install the 2.7 version

2. Take care of the deprecations

# config/packages/api_platform.yaml

api_platform:
    metadata_backward_compatibility_layer: false

3. Switch off the BC flag

4. Upgrade your metadata

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiSubresource;

#[ApiResource(
    itemOperations: ['get'],
)]
final class Author {
    #[ApiSubresource]
    public Collection $books;
}
namespace App\Entity;

use ApiPlatform\Metadata\{ApiResource,Get};

#[ApiResource(
  operations: [
    new Get(),
  ],
)]
final class Author {
    public Collection $books;
}

4. Upgrade your metadata (the lazy way)

 $ php bin/console api:upgrade-resource

-use ApiPlatform\Core\Annotation\ApiResource;
+use ApiPlatform\Metadata\Link;
+use ApiPlatform\Metadata\GetCollection;
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\Get;

 #[ApiResource(
-    iri: 'http://schema.org/Book',
+    types: ['http://schema.org/Book'],
-    itemOperations: ['get'],
-    collectionOperations: [],
+    operations: [new Get()],
 )]
+#[ApiResource(
+    uriTemplate: '/authors/{id}/books.{_format}',
+    uriVariables: ['id' => new Link(fromClass: Author::class, identifiers: ['id'])],
+    operations: [new GetCollection()],
+)]
 final class Book

Thanks!

[SymfonWorld] API Platform, third act

By Mathias Arlaud

[SymfonWorld] API Platform, third act

  • 2,278