php-openapi-generator

Laravel Target

FrameworkTarget::Laravel generates framework-aware code optimized for Laravel applications:

What Gets Generated

generated/
├── Api/
│   ├── PetsApiInterface.php      ← Interface defining all operations
│   ├── PetsApiController.php     ← Abstract controller
│   └── PetsApiRoutes.php         ← Route registration helper
└── Model/
    ├── Pet.php                   ← Full-featured models with serialization
    ├── NewPet.php
    ├── PetStatus.php             ← Enums for type constraints
    └── ApiError.php

The Abstract Controller

The generated controller integrates with Laravel’s request/response handling:

namespace App\Api;

use App\Model\NewPet;
use App\Model\Pet;

abstract class PetsApiController implements PetsApiInterface
{
    public function listPetsAction(\Illuminate\Http\Request $request): \Illuminate\Http\JsonResponse
    {
        $result = $this->listPets(
            limit: $this->parseParameter($this->queryParameterValues($request, 'limit'), 'limit', 'int', false),
        );
        return response()->json($result, 200);
    }

    public function createPetAction(\Illuminate\Http\Request $request): \Illuminate\Http\JsonResponse
    {
        $body = $this->parseJsonRequestBody($request, static fn(array $data): NewPet => NewPet::fromRequestArray($data));
        $result = $this->createPet(
            body: $body,
        );
        return response()->json($result->toResponseArray(), 201);
    }

    public function showPetByIdAction(\Illuminate\Http\Request $request): \Illuminate\Http\JsonResponse
    {
        $result = $this->showPetById(
            petId: $this->parseParameter($request->route('petId'), 'petId', 'int', true),
        );
        return response()->json($result->toArray(), 200);
    }

    // ... other operations

    // Abstract domain methods — you implement these
    abstract public function listPets(?int $limit): array;
    abstract public function createPet(NewPet $body): Pet;
    // ...
}

The Routes Helper

Easily register all routes for an API group:

namespace App\Api;

use Illuminate\Support\Facades\Route;

final class PetsApiRoutes
{
    private function __construct()
    {
    }

    public static function register(string $controller = PetsApiController::class): void
    {
        Route::get('/pets', [$controller, 'listPetsAction']);
        Route::post('/pets', [$controller, 'createPetAction']);
        Route::get('/pets/{petId}', [$controller, 'showPetByIdAction']);
        Route::put('/pets/{petId}', [$controller, 'upsertPetAction']);
        Route::delete('/pets/{petId}', [$controller, 'deletePetAction']);
    }
}

Generated actions convert path, query, header and cookie values to the declared PHP types (int, float, bool, enums, DateTimeImmutable for date / date-time, and arrays following the OpenAPI style/explode rules) through the protected parseParameter() / queryParameterValues() helpers. Missing required or invalid values throw a BadRequestHttpException (HTTP 400). JSON request bodies are read by the protected parseJsonRequestBody() helper: a missing required body, invalid JSON, a non-object body or a body that cannot be hydrated into the DTO also throws a BadRequestHttpException (HTTP 400); optional bodies are passed as null when the request has no content. With validateServerRequest enabled, failed request validation throws an UnprocessableEntityHttpException (HTTP 422) for NativeMethod and SymfonyConstraints, and Laravel’s ValidationException (HTTP 422 with the usual errors structure) for LaravelValidation. Response validation failures stay an InvalidArgumentException (HTTP 500) because they indicate a server bug. Operations whose domain method is not overridden throw a BadMethodCallException. Union return types respond with the status code of the returned model, and scalar results are returned as JSON unchanged.

Implementation Pattern

Extend the abstract controller and implement the domain methods:

<?php

declare(strict_types=1);

namespace App\Http\Controllers\Api;

use App\Api\PetsApiController;
use App\Model\NewPet;
use App\Model\Pet;
use App\Repository\PetRepository;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;

final class PetController extends PetsApiController
{
    public function __construct(private readonly PetRepository $repository)
    {
    }

    // PATTERN 1: Implement domain method (simple case, 95% of operations)
    public function createPet(NewPet $body): Pet
    {
        return $this->repository->create($body->name, $body->tag);
    }

    public function listPets(?int $limit): array
    {
        return $this->repository->findAll($limit);
    }

    public function showPetById(string $petId): Pet
    {
        $pet = $this->repository->find((int) $petId);
        if ($pet === null) {
            abort(404, "Pet $petId not found");
        }
        return $pet;
    }

    // PATTERN 2: Override *Action method for full HTTP control (edge case)
    #[\Override]
    public function upsertPetAction(Request $request): JsonResponse
    {
        // Custom logic with conditional status code
        $petId = $request->route('petId');
        $body = NewPet::fromArray($request->all());
        $existing = $this->repository->find((int) $petId);

        if ($existing !== null) {
            return response()->json(
                $this->repository->update($existing, $body->tag)->toArray(),
                200 // Update
            );
        }

        return response()->json(
            $this->repository->create($body->name, $body->tag)->toArray(),
            201 // Create
        );
    }

    // ... implement other domain methods ...

    public function deletePet(string $petId): void
    {
        $pet = $this->repository->find((int) $petId);
        if ($pet === null) {
            abort(404, "Pet $petId not found");
        }
        $this->repository->delete($pet);
    }
}

Route Registration

Option A: Use the Generated Routes Helper

In routes/api.php:

<?php

use App\Api\PetsApiRoutes;
use App\Http\Controllers\Api\PetController;
use Illuminate\Support\Facades\Route;

Route::middleware('api')->group(function () {
    PetsApiRoutes::register(PetController::class);
});

Option B: Manual Route Registration

<?php

use App\Http\Controllers\Api\PetController;
use Illuminate\Support\Facades\Route;

Route::middleware('api')->group(function () {
    Route::get('/pets', [PetController::class, 'listPetsAction']);
    Route::post('/pets', [PetController::class, 'createPetAction']);
    Route::get('/pets/{petId}', [PetController::class, 'showPetByIdAction']);
    Route::put('/pets/{petId}', [PetController::class, 'upsertPetAction']);
    Route::delete('/pets/{petId}', [PetController::class, 'deletePetAction']);
});

Integration with Laravel

Dependency Injection via Container

Use Laravel’s service container for automatic injection:

final class PetController extends PetsApiController
{
    public function __construct(
        private readonly PetRepository $repository,
        private readonly Logger $logger,
    ) {}
}

Error Handling

Use abort() or exception handling with Laravel’s exception handler:

public function createPet(NewPet $body): Pet
{
    if (empty($body->name)) {
        abort(400, 'Name is required');
    }

    if ($this->repository->nameExists($body->name)) {
        abort(422, 'Pet name already exists');
    }

    return $this->repository->create($body->name, $body->tag);
}

Custom Response Headers

Override the *Action method for custom headers:

#[\Override]
public function listPetsAction(Request $request): JsonResponse
{
    $result = $this->listPets(
        limit: $this->parseParameter($this->queryParameterValues($request, 'limit'), 'limit', 'int', false),
    );
    return response()
        ->json($result, 200)
        ->header('X-Total-Count', count($result))
        ->header('Cache-Control', 'max-age=3600');
}

Request Validation

use Illuminate\Support\Facades\Validator;

public function createPet(NewPet $body): Pet
{
    // Validation happens before reaching domain method
    // But you can add defensive checks:
    
    if (!$this->isValidName($body->name)) {
        abort(400, 'Invalid pet name');
    }

    return $this->repository->create($body->name, $body->tag);
}

Events & Listeners

Leverage Laravel’s event system:

use App\Events\PetCreated;

public function createPet(NewPet $body): Pet
{
    $pet = $this->repository->create($body->name, $body->tag);
    
    event(new PetCreated($pet));
    
    return $pet;
}

Database Transactions

use Illuminate\Support\Facades\DB;

public function createPet(NewPet $body): Pet
{
    return DB::transaction(function () use ($body) {
        return $this->repository->create($body->name, $body->tag);
    });
}

Best Practices

  1. Use constructor injection for dependencies (repositories, services, loggers)
  2. Implement domain methods (the 95% case) to keep code focused
  3. Override *Action methods only when HTTP behavior conflicts with OpenAPI spec
  4. Use abort() or throw exceptions for error cases
  5. Keep domain logic testable — test domain methods independently from HTTP
  6. Use Laravel’s event system for cross-cutting concerns (logging, auditing, etc.)
  7. Leverage query builders and Eloquent models for data access

Testing

use Tests\TestCase;

class PetControllerTest extends TestCase
{
    public function test_list_pets(): void
    {
        $response = $this->getJson('/api/pets?limit=10');

        $response->assertStatus(200)
                 ->assertJsonIsArray();
    }

    public function test_create_pet_without_name(): void
    {
        $response = $this->postJson('/api/pets', ['tag' => 'fluffy']);

        $response->assertStatus(400);
    }

    public function test_delete_nonexistent_pet(): void
    {
        $response = $this->deleteJson('/api/pets/999');

        $response->assertStatus(404);
    }
}

API Documentation

Serve your OpenAPI spec and use:

In routes/web.php:

Route::get('/docs', function () {
    return view('swagger', [
        'spec' => asset('api/openapi.yaml'),
    ]);
});

See Also