OpenAPI v3 Code Generator for Laravel.
Generates routes, controllers, form requests, response classes, and DTOs from an OpenAPI v3 specification. Built as a fork of ogen-go/ogen — reuses the battle-tested OpenAPI parsing and IR pipeline, replacing the output layer with Laravel/PHP templates.
go install github.com/grnsv/lcodegen/cmd/lcodegen@latestOr via the l-codegen Composer package:
composer require --dev grnsv/l-codegen
php artisan l-codegen:installRun from your Laravel project root:
vendor/bin/lcodegen openapi.ymlUse --target to specify the Laravel project directory:
vendor/bin/lcodegen --target ./my-laravel-app openapi.yml- Full Laravel integration — generates idiomatic Laravel code: routes, controllers, Form Requests, Responsable responses, DTOs
- Two-class pattern — base classes (always regenerated) + user classes (created once, never overwritten), so re-generation doesn't destroy your code
- Validation from spec — OpenAPI constraints (
required,minLength,maxLength,pattern,minimum,maximum,enum,format) become Laravel validation rules automatically - Typed DTOs —
readonlyclasses withJsonSerializable,fromArray()factory, proper optional field handling - Optional wrappers —
OptString,OptInt, etc. to distinguish "not set" fromnull - Operation grouping — operations grouped into controllers via
x-ogen-operation-groupor path-based inference - OpenAPI extensions —
x-ogen-name,x-ogen-operation-group,x-ogen-propertiesfor fine-grained control
Given the Petstore Expanded spec, lcodegen produces:
routes/openapi.php # API routes
app/Http/Controllers/OpenApi/PetController.php # Base controller (abstract)
app/Http/Controllers/PetController.php # User controller (your code here)
app/Http/Requests/OpenApi/AddPetRequest.php # Base form request with validation
app/Http/Requests/AddPetRequest.php # User form request
app/Http/Responses/OpenApi/AddPetResponse.php # Response class
app/Http/Responses/OpenApi/ErrorResponse.php # Error response
app/Http/Dto/OpenApi/Pet.php # DTO
app/Http/Dto/OpenApi/NewPet.php # DTO
app/Http/Dto/OpenApi/OptString.php # Optional wrapper
...
<?php
// Code generated by lcodegen, DO NOT EDIT.
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\PetController;
// Pet routes
Route::post('/pets', [PetController::class, 'addPet'])
->name('pet.add_pet');
Route::delete('/pets/{id}', [PetController::class, 'deletePet'])
->name('pet.delete_pet');
Route::get('/pets/{id}', [PetController::class, 'findPetById'])
->name('pet.find_pet_by_id');
Route::get('/pets', [PetController::class, 'findPets'])
->name('pet.find_pets');<?php
// Code generated by lcodegen, DO NOT EDIT.
namespace App\Http\Controllers\OpenApi;
use App\Http\Controllers\Controller;
use App\Http\Requests\AddPetRequest;
use App\Http\Requests\FindPetsRequest;
use App\Http\Responses\OpenApi\AddPetResponse;
use App\Http\Responses\OpenApi\DeletePetResponse;
use App\Http\Responses\OpenApi\FindPetByIdResponse;
use App\Http\Responses\OpenApi\FindPetsResponse;
use App\Http\Responses\OpenApi\ErrorResponse;
abstract class PetController extends Controller
{
/** POST /pets — Creates a new pet in the store. */
abstract public function addPet(
AddPetRequest $request,
): AddPetResponse|ErrorResponse;
/** DELETE /pets/{id} — Deletes a single pet based on the ID supplied. */
abstract public function deletePet(
int $id,
): DeletePetResponse|ErrorResponse;
/** GET /pets/{id} */
abstract public function findPetById(
int $id,
): FindPetByIdResponse|ErrorResponse;
/** GET /pets — Returns all pets from the system. */
abstract public function findPets(
FindPetsRequest $request,
): FindPetsResponse|ErrorResponse;
}Created once, never overwritten — this is where your business logic goes:
<?php
// This file can be edited. It will not be overwritten by the generator.
namespace App\Http\Controllers;
use App\Http\Controllers\OpenApi\PetController as BasePetController;
use App\Http\Requests\AddPetRequest;
use App\Http\Responses\OpenApi\AddPetResponse;
use App\Http\Responses\OpenApi\ErrorResponse;
final class PetController extends BasePetController
{
public function addPet(
AddPetRequest $request,
): AddPetResponse|ErrorResponse
{
// TODO: Implement AddPet
throw new \BadMethodCallException('Not implemented');
}
// ...
}<?php
// Code generated by lcodegen, DO NOT EDIT.
namespace App\Http\Requests\OpenApi;
use App\Http\Dto\OpenApi\NewPet;
use Illuminate\Foundation\Http\FormRequest;
abstract class AddPetRequest extends FormRequest
{
public function rules(): array
{
return [
'name' => ['required', 'string'],
'tag' => ['sometimes', 'string'],
];
}
public function validated($key = null, $default = null): mixed
{
$validated = parent::validated($key, $default);
if ($key === null) {
return NewPet::fromArray($validated);
}
return $validated;
}
}<?php
// Code generated by lcodegen, DO NOT EDIT.
namespace App\Http\Dto\OpenApi;
use JsonSerializable;
final readonly class Pet implements JsonSerializable
{
public function __construct(
public string $name,
public OptString $tag,
public int $id,
) {}
public function jsonSerialize(): array
{
$result = [];
$result['name'] = $this->name;
if ($this->tag->set) {
$result['tag'] = $this->tag->value;
}
$result['id'] = $this->id;
return $result;
}
public static function fromArray(array $data): self
{
return new self(
name: $data['name'],
tag: \array_key_exists('tag', $data)
? OptString::some($data['tag'])
: OptString::none(),
id: $data['id'],
);
}
}<?php
// Code generated by lcodegen, DO NOT EDIT.
namespace App\Http\Responses\OpenApi;
use Illuminate\Contracts\Support\Responsable;
use Illuminate\Http\JsonResponse;
use App\Http\Dto\OpenApi as Dto;
final readonly class AddPetResponse implements Responsable
{
private const HTTP_STATUS = 200;
public function __construct(
private Dto\Pet $data,
) {}
public function toResponse($request): JsonResponse
{
return response()->json($this->data, self::HTTP_STATUS);
}
}<?php
// Code generated by lcodegen, DO NOT EDIT.
namespace App\Http\Dto\OpenApi;
final readonly class OptString
{
public function __construct(
public ?string $value,
public bool $set,
) {}
public static function none(): self
{
return new self(null, false);
}
public static function some(string $value): self
{
return new self($value, true);
}
}The generator separates generated code from user code:
| Layer | Base class (OpenApi/ subdir) |
User class (parent dir) |
|---|---|---|
| Controllers | Abstract, defines method signatures | final, extends base, contains your logic |
| Form Requests | Validation rules from spec, validated() returns typed DTO |
Authorization, custom rules |
Base classes are always regenerated from the spec. User classes are created once and never overwritten — safe to edit.
Config file (ogen.yml, ogen.yaml, .ogen.yml, .ogen.yaml):
generator:
ignore_not_implemented: ["all"]
parser:
infer_types: true
allow_remote: true| Extension | Scope | Description |
|---|---|---|
x-ogen-operation-group |
path / operation | Group operations into controllers |
x-ogen-name |
schema | Custom type name |
x-ogen-properties |
schema | Custom field names |
x-ogen-server-name |
server | Custom server name |
Example — grouping operations into controllers:
paths:
/pets:
x-ogen-operation-group: Pet
get:
operationId: findPets
post:
operationId: addPet
/users:
x-ogen-operation-group: User
get:
operationId: listUsersThis generates PetController and UserController instead of a single controller.
OpenAPI YAML/JSON
→ ogen.Parse() # Parse & validate spec, resolve $ref
→ gen.NewGenerator() # Build IR
→ g.WriteSource() # Render PHP via Go text/template
→ Laravel PHP files
The intermediate representation drives code generation. PHP/Laravel specifics live in templates and supporting generator code.