DEV Community

Daniel Schreiber
Daniel Schreiber

Posted on • Edited on

Define, Generate, and Implement: An API-First Approach with OpenAPI Generator and FlightPHP

Adopting an API-first strategy ensures that your server and client remain in sync, dramatically reducing integration issues. By defining your API contract upfront, you can automatically generate both server stubs and client SDKs. This not only minimizes manual work but also creates a "typesafe" bridge between your front-end and back-end -- something even PHP developers can appreciate ;-)

Defining Your API

Begin by describing your API in an OpenAPI specification (e.g., my_api.yaml). In your spec, define endpoints, request/response schemas, and authentication details. For instance, a simple user endpoint might be defined as follows:

openapi: 3.0.0
info:
  title: Example API
  version: 1.0.0
paths:
  /users/{id}:
    get:
      summary: Retrieve a user by ID
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: A user object
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
    put:
      summary: Update a user by ID
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/User'
      responses:
        '204':
          description: User updated
components:
  schemas:
    UserState:
      type: string
      enum:
        - active
        - disabled
        - pending
    User:
      type: object
      required:
        - id
        - state
      properties:
        id:
          type: integer
        name:
          type: string
        email:
          type: string
        state:
          $ref: '#/components/schemas/UserState'
Enter fullscreen mode Exit fullscreen mode

While this might seem bloated at first glance, bear with me — this approach makes subsequent implementations much easier and more robust!

Generating the Server Stub and Client SDK

With your API defined, use the OpenAPI Generator to generate your code automatically. The PHP Flight generator — documented here — was provided by the author and, although its status is still marked as "experimental", it has been my production workhorse for over a year.

Here’s an example shell script that automates the generation of a FlightPHP server stub:

#!/bin/bash
set -e

OPEN_API_GEN_VERSION="7.9.0"
GENERATOR_JAR="openapi-generator-cli-${OPEN_API_GEN_VERSION}.jar"

# Download the generator if needed:
if [ ! -f "$GENERATOR_JAR" ]; then
    curl -o "$GENERATOR_JAR" "https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/${OPEN_API_GEN_VERSION}/openapi-generator-cli-${OPEN_API_GEN_VERSION}.jar"
fi

# Clean previous outputs
rm -rf generated-server

# Generate a FlightPHP server stub using the php-flight generator
java -jar "$GENERATOR_JAR" generate -i my_api.yaml -g php-flight -o generated-server --additional-properties=invokerPackage=GeneratedApi,composerPackageName=myapi/server
Enter fullscreen mode Exit fullscreen mode

Of course, you can similarly generate your API (frontend) clients. This automated process ensures that any changes to your API spec are consistently propagated across all layers, keeping your integration robust and typesafe. You can choose whether to track these generated files in version control or simply re-generate them in your CI build.

Check Out the Generated Source

The generated files form a complete composer package, that you could package and use as dependency. Here we'll keep it in a subfolder and include it as a local composer repository. Assume you generated the following into a folder generated-server in you project:

├── Api
│   └── AbstractDefaultApi.php
├── Model
│   ├── User.php
│   └── UserState.php
├── README.md
├── RegisterRoutes.php
├── Test
│   └── RegisterRoutesTest.php
├── composer.json
└── phpunit.xml.dist
Enter fullscreen mode Exit fullscreen mode

Then you can require this in your composer.json as follows:

{
  // other properties ...
  "repositories": [
    {
      "type": "path",
      "url": "generated-server/"
    }
  ],
  "require": {
    "myapi/server": "*",
    // other dependencies ...
  }
}
Enter fullscreen mode Exit fullscreen mode

The User model, for example, looks like this (without comments for better readability):

<?php

namespace GeneratedApi\Model;

class User implements \JsonSerializable
{
    public int $id;
    public ?string $name;
    public ?string $email;
    public UserState $state;

    public function __construct(int $id, ?string $name, ?string $email, UserState $state)
    {
        $this->id = $id;
        $this->name = $name;
        $this->email = $email;
        $this->state = $state;
    }

    public static function fromArray(array $data): self
    {
        return new self(
            $data['id'] ?? null, 
            $data['name'] ?? null, 
            $data['email'] ?? null, 
            isset($data['state']) ? UserState::tryFrom($data['state']) : null, 
        );
    }

    public function jsonSerialize(): mixed {
        return [
            'id' => $this->id, 
            'name' => $this->name, 
            'email' => $this->email, 
            'state' => $this->state, 
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

Implementing Your API Endpoints

Once your server stub is generated, extend the generated abstract API classes to implement your business logic. The generated API classes contain method stubs with correct parameter and return types for every path from your API specification. An example implementation for the User API might look like this:

<?php

namespace MyApp\Api;

use GeneratedApi\Api\AbstractDefaultApi;
use GeneratedApi\Model\User;
use GeneratedApi\Model\UserState;
use Flight;

class UserApi extends AbstractDefaultApi
{
    #[\Override]
    public function usersIdGet(int $id): ?User {
        // Simulate fetching a user from a data source
        if ($id === 1) {
            return new User(1, 'Alice Smith', 'alice@example.com', UserState::PENDING);
        }
        Flight::halt(404, "User not found");
        return null;
    }

    #[\Override] 
    public function usersIdPut(int $id, User $user): void {
         // store user in DB - might use $user->jsonSerialize() for serialization
    }
}
Enter fullscreen mode Exit fullscreen mode

Note: You do not need to overwrite all methods! If you like to handle some routes manually, just ignore these method stubs (and manually register these routes with FlightPHP).

After implementing your API logic, register your endpoints with FlightPHP to integrate them into your application routing:

<?php

// In your Flight bootstrap file
use MyApp\Api\UserApi;
use GeneratedApi\RegisterRoutes;

RegisterRoutes::registerRoutes(new UserApi()); 
// this is the place you'd manually invoke `Flight::route()`

Flight::start();
Enter fullscreen mode Exit fullscreen mode

This ties your custom implementation to FlightPHP’s routing system, ensuring that your API behaves as defined. The RegisterRoutes::registerRoutes() method is generated by the generator and automatically checks which methods were implemented in UserApi and registers them with Flight:route() for the correct paths. Also it de-/serializes request/response objects for you.

Note all the things you don't need to do with this approach:

  • No manual de-/serialization needed (you get your parameters in correct type - e.g. userId as int!)
  • No manual checking if response/request is as expected (conforms to schema)
  • No manual set up of paths/routes

Final Thoughts

By following an API-first approach with OpenAPI Generator and FlightPHP, you ensure a consistent, typesafe contract between your front-end and back-end. While the initial setup might take a little effort, it quickly pays off during subsequent iterations. This strategy treats your API as first-class citizen and helps you maintaining this API with any upcoming changes!

I’d love to hear your thoughts or experiences with this approach. Please share your feedback or any questions you might have.

Top comments (1)

Collapse
 
n0nag0n profile image
n0nag0n

Thank you for this blog post (and for being the guy to get this in the OpenAPI repo)!!! This is so exciting!