Come sfruttare


durante lo sviluppo di un API in PHP

Alessandro Lai / @AlessandroLai

PUG Milano - 4 maggio 2022, HQ / Online

Chi sono?

  • Alessandro Lai
  • Engineering Manager @
  • @Jean85
  • @AlessandroLai
  • PUGMI Coordinator
  • PHP-FIG Secretary

APIs: the language of web apps

APIs are the "common language" of web development

  • Frontend to backend
  • Mobile to backend
  • Between different applications
  • Between different companies
    (SaaS, API monetization...)

Working between projects

If you don't have a monorepo, you'll need a way to describe
what your APIs does and how it does it

Design by contract ™

Design by contract is an approach for designing software.
software designers should define formal, precise and verifiable
interface specifications
for software components,

all client components that invoke
an operation on a server component will meet the preconditions

[Otherwise] the inverse approach is taken, meaning that the server component
tests that all relevant preconditions hold true.
From Wikipedia

Which language to use? Which format?

OpenAPI 3

OpenAPI specification heading
OpenAPI specification

OpenAPI (the old Swagger)

Swagger UI
Swagger UI

Beware, here be YAML

openapi: 3.0.3
  title: Test API for
  version: '1.0'
  description: Description of the APIs
    name: Alessandro Lai
  - url: ''
    description: Prod
      summary: Get the test information
          description: OK
                type: object
                  # ...

StopLight (Studio)

OpenAPI specification heading

Live demo!

Credits to this guy...

Phil Sturgeon Twitter profile site screenshot

The "documentation" pitfall

  • How do we synchronize code & spec?
  • What's stopping us from making the spec outdated?
  • Just committing your OpenAPI Spec is not enough:
    it's the same as having too many comments in the code

The "magic" approach

 * List the rewards of the specified user.
 * This call takes into account all confirmed awards, but not pending or refused awards.
 * @Route("/api/{user}/rewards", methods={"GET"})
 * @SWG\Response(
 *     response=200,
 *     description="Returns the rewards of an user",
 *     @SWG\Schema(
 *         type="array",
 *         @SWG\Items(ref=@Model(type=Reward::class, groups={"full"}))
 *     )
 * )
 * @SWG\Parameter(name="order", in="query", type="string", description="The field used to order rewards")
 * @SWG\Tag(name="rewards")
 * @Security(name="Bearer")
public function fetchUserRewardsAction(User $user)

The "magic" approach


OA Spec Code

code changes can accidentally alter the API contract!

A better approach:
involving the spec in the code

OA Spec Code

All code can be seen in action at

The validator

composer require league/openapi-psr7-validator
OpenAPI Spec + PSR-7 message = Valid

Http Foundation VS PSR-7

If you use Flex, the new psr7-pack (see symfony/recipes#911)
composer require psr7-pack
# same as
composer require symfony/psr-http-message-bridge nyholm/psr7
    autowire: true
    autoconfigure: true
sensio/framework-extra-bundle < 6.0
      enabled: true

Symfony service definition

$validatorBuilder = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)
    ->setCache(new Psr16(...))

    autowire: true
    autoconfigure: true

      - ['fromYamlFile', ['%kernel.project_dir%/openapi.yaml']]
      # cache pool from framework.cache.pools
      - ['setCache', ['@cache.openapivalidator']]
    - '@League\OpenAPIValidation\PSR7\ValidatorBuilder'
    - 'getRequestValidator'

Using the validator in tests (1/2)

class OpenApiClient extends KernelBrowser
    protected function doRequest($request): Response
        if ($this->validateRequest) {
            $psr7request = $this->psrHttpFactory->createRequest($request);

        $pathFinder = new PathFinder(

        $matchingOperations = $pathFinder->search();
        if (count($matchingOperations) !== 1) {
            throw new \RuntimeException(
                "Unexpected number of matches for {$request->getUri()}: " .
        } // ...

Using the validator in tests (2/2)

        // (continue)

        try {
        } catch (\... $exception) {
            // You should catch and decorate the exceptions 
            // to make the failures "prettier"

        return $response;

Using the spec in production!



use League\OpenAPIValidation\PSR7\RequestValidator as Psr7RequestValidator;
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
use Symfony\Component\HttpKernel\Event\ControllerEvent;

class RequestValidatorEventListener
    public function __construct(
        private PsrHttpFactory $psrHttpFactory,
        private RequestValidator $requestValidator,
    ) {}

    public function onKernelController(ControllerEvent $event): void
        $psr7request = $this->psrHttpFactory->createRequest($event->getRequest());


Using validation errors as responses


use League\OpenAPIValidation\PSR7\Exception\ValidationFailed;
use Symfony\Component\HttpFoundation\JsonResponse;

class ErrorEventListener
    public function onKernelException(ExceptionEvent $event): void
        // ...

    private function createFromThrowable(\Throwable $error): JsonResponse
        if ($error instanceof ValidationFailed) {
            return new JsonResponse([/* ... */], Response::HTTP_BAD_REQUEST);
        // ...

A suggestion: API Problem

RFC 7808 - Problem Details for HTTP APIs
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
Content-Language: en

    "type": "",
    "title": "You do not have enough credit.",
    "detail": "Your current balance is 30, but that costs 50.",
    "instance": "/account/12345/msgs/abc",
    // arbitrary extended properties:
    "balance": 30,
    "accounts": ["/account/12345", "/account/67890"]

PHP API Problem

composer require crell/api-problem
use Crell\ApiProblem\ApiProblem;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;

$problem = new ApiProblem("You do not have enough credit.", "");
// Defined properties in the API have their own setter methods.
  ->setDetail("Your current balance is 30, but that costs 50.")
// Additional properties can be used with array access
$problem['balance'] = 30;
$problem['accounts'] = ["", ""];

return new JsonResponse($problem->asArray(), Response::HTTP_BAD_REQUEST);

Pitfalls and limitations

  • Tools are constraints
    Penetration of new OAS version support is not immediate
  • Versioning issues are still there to be handled
    ... but you could leverage OAS to make it less painful

Grazie per l'attenzione!