Pular para o conteúdo principal

Runtime Parameters Validator

PHP Swagger Test can be used as a runtime validator in production applications to validate API requests and responses against your OpenAPI specification. This provides automatic validation without writing manual validation code.

Why Use Runtime Validation?​

Benefits:

  • Automatic validation - No need to write validation code manually
  • Always in sync - Validation rules come directly from your OpenAPI spec
  • Consistent - Same validation in tests and production
  • Documentation as code - Your spec is the single source of truth
  • Early error detection - Catch invalid data before processing

Use Cases:

  • Validate incoming API requests before processing
  • Validate outgoing API responses before sending
  • Validate data in API middleware
  • Validate webhook payloads
  • Integration with frameworks (Laravel, Symfony, Slim)

Basic Request Validation​

Validate an incoming request body before processing:

<?php
use ByJG\ApiTools\Base\Schema;

// Load your OpenAPI spec
$schema = Schema::fromFile(__DIR__ . '/openapi.json');

// Get the request definition
$path = '/api/users';
$method = 'POST';
$bodyRequestDef = $schema->getRequestParameters($path, $method);

// Validate the request body
$requestBody = json_decode(file_get_contents('php://input'), true);

try {
$bodyRequestDef->match($requestBody);
// Request is valid, continue processing
} catch (\ByJG\ApiTools\Exception\BaseException $e) {
// Request is invalid
http_response_code(400);
echo json_encode([
'error' => 'Invalid request',
'message' => $e->getMessage()
]);
exit;
}

Basic Response Validation​

Validate a response before sending it to the client:

<?php
use ByJG\ApiTools\Base\Schema;

$schema = Schema::fromFile(__DIR__ . '/openapi.json');

// Process your request and get the result
$result = $userService->createUser($userData);

// Validate the response
$path = '/api/users';
$method = 'POST';
$statusCode = 201;

$bodyResponseDef = $schema->getResponseParameters($path, $method, $statusCode);

try {
$bodyResponseDef->match($result);
// Response is valid
http_response_code($statusCode);
echo json_encode($result);
} catch (\ByJG\ApiTools\Exception\BaseException $e) {
// Response doesn't match spec - this is a server error
error_log("Response validation failed: " . $e->getMessage());
http_response_code(500);
echo json_encode(['error' => 'Internal server error']);
}

Middleware Integration​

PSR-15 Middleware​

<?php
use ByJG\ApiTools\Base\Schema;
use ByJG\ApiTools\Exception\BaseException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

class OpenApiValidationMiddleware implements MiddlewareInterface
{
private Schema $schema;

public function __construct(string $specPath)
{
$this->schema = Schema::fromFile($specPath);
}

public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface {
$path = $request->getUri()->getPath();
$method = $request->getMethod();

// Validate request
try {
$requestBody = json_decode($request->getBody()->getContents(), true);
$request->getBody()->rewind(); // getContents() leaves the stream at its end
$bodyRequestDef = $this->schema->getRequestParameters($path, $method);
$bodyRequestDef->match($requestBody);
} catch (BaseException $e) {
return new \Laminas\Diactoros\Response\JsonResponse([
'error' => 'Invalid request',
'message' => $e->getMessage()
], 400);
}

// Process request
$response = $handler->handle($request);

// Validate response (optional in production)
if (getenv('VALIDATE_RESPONSES') === 'true') {
try {
$responseBody = json_decode($response->getBody()->getContents(), true);
$response->getBody()->rewind(); // getContents() leaves the stream at its end
$bodyResponseDef = $this->schema->getResponseParameters(
$path,
$method,
$response->getStatusCode()
);
$bodyResponseDef->match($responseBody);
} catch (BaseException $e) {
error_log("Response validation failed: " . $e->getMessage());
// Don't fail in production, just log
}
}

return $response;
}
}

Usage:

$app->add(new OpenApiValidationMiddleware(__DIR__ . '/openapi.json'));

Framework Integration​

Laravel Integration​

See Contract Tests - Framework Integration for Laravel examples.

Symfony Integration​

See Contract Tests - Framework Integration for Symfony examples.

Slim Framework Integration​

<?php
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;
use ByJG\ApiTools\Base\Schema;
use ByJG\ApiTools\Exception\BaseException;

$app = AppFactory::create();

$schema = Schema::fromFile(__DIR__ . '/openapi.json');

// Validation middleware
$app->add(function (Request $request, $handler) use ($schema) {
$path = $request->getUri()->getPath();
$method = $request->getMethod();

try {
$body = json_decode($request->getBody()->getContents(), true);
$request->getBody()->rewind(); // getContents() leaves the stream at its end
$bodyRequestDef = $schema->getRequestParameters($path, $method);
$bodyRequestDef->match($body ?? []);
} catch (BaseException $e) {
$response = new \Slim\Psr7\Response();
$response->getBody()->write(json_encode([
'error' => 'Validation failed',
'message' => $e->getMessage()
]));
return $response->withStatus(400)->withHeader('Content-Type', 'application/json');
}

return $handler->handle($request);
});

$app->run();

Validating a Value Against a Schema​

The request and the response are not the only places where data enters the system. A command, a queue consumer or a service can create the same object without going through the API. matchDefinition() validates a value against a named schema of the specification, so those paths can use the same rules:

<?php
use ByJG\ApiTools\Base\Schema;
use ByJG\ApiTools\Exception\NotMatchedException;

$schema = Schema::fromFile(__DIR__ . '/openapi.json');

try {
// '#/definitions/Product' in a Swagger 2.0 specification
$schema->matchDefinition('#/components/schemas/Product', ['name' => 'Keyboard', 'price' => 49.9]);
} catch (NotMatchedException $e) {
echo "Invalid product: " . $e->getMessage();
}

The value is decoded JSON: an associative array for an object.

For an update that changes only some of the properties, pass true as the third argument. The schema's required list is then not checked, while every property present still has to match:

// Accepted: 'name' is missing, but this is an update.
$schema->matchDefinition('#/components/schemas/Product', ['price' => 10], true);

// Rejected: the name that is present is empty (minLength: 1).
$schema->matchDefinition('#/components/schemas/Product', ['name' => ''], true);

$partial applies to the schema named, not to the ones it refers to with $ref: a referenced object that is present is matched in full.

The value is one being written, so it follows the rules of a request: a property marked readOnly (for example an id the database generates) is not required, even when the schema lists it in required. A response still requires it.


Webhook Validation​

Validate incoming webhook payloads:

<?php
use ByJG\ApiTools\Base\Schema;

class WebhookHandler
{
private Schema $schema;

public function __construct()
{
$this->schema = Schema::fromFile(__DIR__ . '/webhook-spec.json');
}

public function handlePetUpdated(array $payload): void
{
// Validate webhook payload
try {
$bodyRequestDef = $this->schema->getRequestParameters(
'/webhooks/pet-updated',
'POST'
);
$bodyRequestDef->match($payload);
} catch (\ByJG\ApiTools\Exception\BaseException $e) {
error_log("Invalid webhook payload: " . $e->getMessage());
http_response_code(400);
return;
}

// Process valid webhook
$this->processPetUpdate($payload);
http_response_code(200);
}

private function processPetUpdate(array $payload): void
{
// Your business logic here
}
}

Performance Considerations​

Cache the Schema​

Load the schema once and reuse it:

<?php
class SchemaCache
{
private static ?Schema $schema = null;

public static function getSchema(): Schema
{
if (self::$schema === null) {
self::$schema = Schema::fromFile(__DIR__ . '/openapi.json');
}
return self::$schema;
}
}

// Use in your code
$schema = SchemaCache::getSchema();

Conditional Validation​

Only validate in development/staging:

<?php
if (getenv('APP_ENV') !== 'production') {
try {
$bodyRequestDef->match($requestBody);
} catch (BaseException $e) {
// Strict validation in non-production
throw $e;
}
} else {
// Optional: Log but don't fail in production
try {
$bodyRequestDef->match($requestBody);
} catch (BaseException $e) {
error_log("Validation warning: " . $e->getMessage());
// Continue processing
}
}

Error Handling Best Practices​

For detailed exception handling, see Exception Handling.

Return Detailed Errors in Development​

<?php
use ByJG\ApiTools\Exception\BaseException;
use ByJG\ApiTools\Exception\RequiredArgumentNotFound;
use ByJG\ApiTools\Exception\NotMatchedException;

try {
$bodyRequestDef->match($requestBody);
} catch (RequiredArgumentNotFound $e) {
http_response_code(400);
echo json_encode([
'error' => 'Missing required field',
'message' => $e->getMessage(),
'provided_body' => getenv('APP_DEBUG') ? $e->getBody() : null
]);
} catch (NotMatchedException $e) {
http_response_code(400);
echo json_encode([
'error' => 'Validation failed',
'message' => $e->getMessage(),
'provided_body' => getenv('APP_DEBUG') ? $e->getBody() : null
]);
} catch (BaseException $e) {
http_response_code(400);
echo json_encode([
'error' => 'Invalid request',
'message' => $e->getMessage()
]);
}

Sanitize Errors for Production​

<?php
function handleValidationError(BaseException $e): array
{
if (getenv('APP_ENV') === 'production') {
// Generic error for production
return [
'error' => 'Validation failed',
'message' => 'The request did not match the expected format'
];
}

// Detailed error for development
return [
'error' => 'Validation failed',
'message' => $e->getMessage(),
'body' => $e->getBody(),
'trace' => $e->getTraceAsString()
];
}

Complete Example: API Endpoint​

Here's a complete example of a validated API endpoint:

<?php
use ByJG\ApiTools\Base\Schema;
use ByJG\ApiTools\Exception\BaseException;

class UserApi
{
private Schema $schema;
private PDO $db;

public function __construct()
{
$this->schema = Schema::fromFile(__DIR__ . '/openapi.json');
$this->db = new PDO(/* ... */);
}

public function createUser(): void
{
// Get request body
$requestBody = json_decode(file_get_contents('php://input'), true);

// Validate request
try {
$bodyRequestDef = $this->schema->getRequestParameters('/users', 'POST');
$bodyRequestDef->match($requestBody);
} catch (BaseException $e) {
$this->sendError(400, 'Invalid request', $e->getMessage());
return;
}

// Process request
try {
$stmt = $this->db->prepare(
'INSERT INTO users (name, email) VALUES (:name, :email)'
);
$stmt->execute([
'name' => $requestBody['name'],
'email' => $requestBody['email']
]);

$userId = $this->db->lastInsertId();

// Prepare response
$responseBody = [
'id' => (int)$userId,
'name' => $requestBody['name'],
'email' => $requestBody['email'],
'created_at' => date('c')
];

// Validate response
try {
$bodyResponseDef = $this->schema->getResponseParameters('/users', 'POST', 201);
$bodyResponseDef->match($responseBody);
} catch (BaseException $e) {
error_log("Response validation failed: " . $e->getMessage());
// Log but continue in production
}

// Send response
http_response_code(201);
header('Content-Type: application/json');
echo json_encode($responseBody);

} catch (\PDOException $e) {
$this->sendError(500, 'Database error', $e->getMessage());
}
}

private function sendError(int $code, string $error, string $message): void
{
http_response_code($code);
header('Content-Type: application/json');
echo json_encode([
'error' => $error,
'message' => getenv('APP_DEBUG') ? $message : 'An error occurred'
]);
}
}

// Handle request
$api = new UserApi();
$api->createUser();