zvonchuk/phalcon-openapi

Automatic OpenAPI 3.1 spec generation for Phalcon PHP — zero annotations, convention-based inference

Maintainers

Package info

github.com/zvonchuk/phalcon-openapi

pkg:composer/zvonchuk/phalcon-openapi

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 3

Open Issues: 0

v1.0.0 2026-04-22 05:59 UTC

This package is not auto-updated.

Last update: 2026-04-22 18:52:59 UTC


README

Tests Latest Stable Version PHP Version License PHPStan

Automatic OpenAPI 3.1 spec generation for Phalcon PHP applications.

Generates a complete OpenAPI spec by reading your routes, controllers, models, and DTOs — no annotations, no YAML, no manual work. Just write your code and get Swagger UI for free.

Why This Package

Annotation-based approach phalcon-openapi
Routes Duplicated in annotations Read from Phalcon Router automatically
Schemas Separate schema definitions Built from your existing DTOs and Models
Validation Documentation only Same attributes for docs AND runtime
Status codes Manual per-endpoint Convention-based (201, 204, 422, 404)
Setup Custom controller + HTML Two lines of code

Requirements

  • PHP 8.1+
  • Phalcon 5.x

Installation

composer require zvonchuk/phalcon-openapi

Quick Start

use PhalconOpenApi\OpenApiModule;

$module = new OpenApiModule([
    'title'   => 'My API',
    'version' => '1.0.0',
]);
$module->registerServices($di);

Two endpoints are registered automatically:

  • GET /api/openapi.json — OpenAPI 3.1 JSON spec
  • GET /api/docs — Swagger UI

How It Works

The package automatically reads:

  • Router — all registered routes, HTTP methods, path patterns
  • Model MetaData — column types, nullable, primary keys
  • Reflection — controller action parameters, return types, docblocks
  • PHP 8 Attributes — optional tags, hidden endpoints, extra responses, security

Convention-Based Inference

Zero annotations needed for standard CRUD:

class UserController extends ApiController
{
    // GET /users → 200 with array of User, operationId: listUsers
    public function listAction(int $page = 1, int $limit = 20) { }

    // POST /users → 201 Created + 422 Validation Error, operationId: createUser
    public function createAction(CreateUserRequest $body) { }

    // GET /users/{id} → 200 + 404 Not Found, operationId: getUser
    public function getAction(int $id) { }

    // DELETE /users/{id} → 204 No Content + 404, operationId: deleteUser
    public function deleteAction(int $id) { }
}

The spec generator infers:

  • Status codes: create → 201, delete → 204, others → 200
  • 422 Validation Error: auto-added when endpoint has a DTO body parameter
  • 404 Not Found: auto-added when route has path parameters
  • operationId: generated from controller + action name
  • Tags: from controller name (UserControllerUsers)
  • Schemas: from Phalcon Model metadata or DTO class properties

Attributes Reference

All attributes are optional — use only when conventions aren't enough.

Endpoint Attributes

use PhalconOpenApi\Attribute\{ApiTag, ApiIgnore, ApiResponse, ApiDescription, ApiSecurity, ApiPaginated};

#[ApiTag('Users')]              // Group endpoints (class or method level)
#[ApiSecurity('bearerAuth')]    // Require auth (class or method level)
class UserController extends ApiController
{
    #[ApiDescription(
        summary: 'List all users',
        description: 'Returns a paginated list of users with optional filtering'
    )]
    #[ApiPaginated]             // Wraps response in {data, total, page, per_page}
    public function listAction(int $page = 1) { }

    #[ApiIgnore]                // Hide from spec
    public function internalAction() { }

    #[ApiResponse(409, ConflictResponse::class)]  // Extra response code
    public function createAction(CreateUserRequest $body) { }
}

Validation Attributes

Used for both runtime validation (via DtoValidator) and OpenAPI schema generation:

use PhalconOpenApi\Attribute\{Email, Min, Max, StringLength, Format, Pattern, Enum, Url, NotBlank};

class CreateUserRequest
{
    #[NotBlank]
    #[StringLength(min: 1, max: 255)]
    public string $name;

    #[Email]
    public string $email;

    public ?string $phone = null;       // nullable → type: ["string", "null"]

    #[Min(1), Max(150)]
    public int $age;

    #[Enum(['active', 'inactive'])]
    public string $status = 'active';   // → enum in OpenAPI schema

    #[Url]
    public ?string $website = null;     // → format: uri in schema

    #[Format('date')]
    public ?string $birthDate = null;   // → format: date in schema

    #[Pattern('/^\+\d{10,15}$/')]       // PCRE delimiters stripped for OpenAPI
    public ?string $mobile = null;
}
Attribute Validates OpenAPI Schema
#[Email] Valid email format format: email
#[StringLength(min: 1, max: 255)] String length bounds minLength, maxLength
#[Min(1)], #[Max(150)] Numeric range minimum, maximum
#[Enum(['a', 'b'])] Allowed values enum: ["a", "b"]
#[Url] Valid URL format: uri
#[NotBlank] Rejects whitespace-only minLength: 1
#[Format('date')] Date/datetime/uuid/uri format: date
#[Pattern('/regex/')] PCRE regex match pattern: regex

Nested DTOs and Typed Arrays

Nested objects are validated recursively with dot-notation error paths:

class CreateOrderRequest
{
    #[StringLength(min: 1)]
    public string $orderNumber;

    public AddressDto $shippingAddress;  // → $ref + recursive validation

    /** @var OrderItemDto[] */
    public array $items = [];            // → array with $ref + per-item validation
}

class AddressDto
{
    #[NotBlank]
    public string $street;

    #[NotBlank]
    public string $city;

    #[Pattern('/^\d{5}$/')]
    public string $zip;
}

Validation errors for nested objects use dot-notation:

{
    "code": 422,
    "message": "Validation failed",
    "errors": [
        "shippingAddress.city is required",
        "items[0].zip must match pattern /^\\d{5}$/"
    ]
}

Security Configuration

$module = new OpenApiModule([
    'title'   => 'My API',
    'version' => '1.0.0',
    'security' => [
        'bearerAuth' => [
            'type'         => 'http',
            'scheme'       => 'bearer',
            'bearerFormat' => 'JWT',
        ],
    ],
]);

Then annotate controllers or methods with #[ApiSecurity('bearerAuth')].

Base Controller

Extend ApiController for automatic JSON body parsing, DTO validation, and convenience helpers:

use PhalconOpenApi\ApiController;

class UserController extends ApiController
{
    public function getAction(int $id)
    {
        $user = User::findFirst($id);
        if (!$user) {
            return $this->notFound('User not found');
        }
        return $this->json($user);
    }

    public function createAction(CreateUserRequest $body)
    {
        // $body is already validated and hydrated automatically
        $user = new User();
        $user->assign((array) $body);
        $user->save();
        return $this->json($user, 201);
    }
}

Configuration Options

$module = new OpenApiModule([
    'title'          => 'My API',           // required
    'version'        => '1.0.0',            // required
    'description'    => 'API description',  // optional
    'modelNamespace' => 'App\\Models',      // enables convention-based model inference
    'servers'        => [                   // optional
        ['url' => 'https://api.example.com'],
    ],
    'security'       => [ /* ... */ ],      // optional, see Security section
]);

Demo Application

See phalcon-openapi-demo for a complete working example with Docker and CRUD controllers.

Running Tests

vendor/bin/phpunit

License

MIT