webfiori / http
Basic library that can help in creating RESTful APIs using PHP.
Requires
- php: >=8.1
- ext-json: *
- ext-mbstring: *
- webfiori/jsonx: 5.0.x
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.92
- phpunit/phpunit: ^10.0
This package is auto-updated.
Last update: 2026-06-13 16:00:12 UTC
README
A powerful and flexible PHP library for creating RESTful web APIs with built-in input filtering, data validation, and comprehensive HTTP utilities. The library provides a clean, object-oriented approach to building web services with automatic parameter validation, authentication support, and JSON response handling.
Table of Contents
Motivation
With well-established PHP HTTP libraries available, you might wonder why this one exists.
Validation is not optional. In most frameworks, input validation is a separate step you wire up after defining your routes. Here, you cannot define an endpoint without declaring exactly what data it accepts, its type, and how it should be validated. The API contract is the code.
Minimal dependencies. The library has a single runtime dependency (webfiori/jsonx). No PSR-7 stack, no framework coupling, no transitive dependency tree. What you install is what you get.
One service, one unit. Each endpoint is a self-contained object with its own parameters, authorization logic, and processing — independently testable and self-documenting. Built-in OpenAPI spec generation is a natural result of this design.
Full control. Request parsing, header management, content negotiation, and response handling are all implemented internally. No hidden layers, no framework tax.
Supported PHP Versions
| Build Status |
|---|
Key Features
- RESTful API Development: Full support for creating REST services with JSON request/response handling
- Automatic Input Validation: Built-in parameter validation with support for multiple data types
- Custom Filtering: Ability to create user-defined input filters and validation rules
- Authentication Support: Built-in support for various authentication schemes (Basic, Bearer, etc.)
- HTTP Method Support: Support for all standard HTTP methods (GET, POST, PUT, DELETE, etc.)
- Content Type Handling: Support for
application/json,application/x-www-form-urlencoded, andmultipart/form-data - Object Mapping: Automatic mapping of request parameters to PHP objects
- Comprehensive Testing: Built-in testing utilities with
APITestCaseclass - Error Handling: Structured error responses with appropriate HTTP status codes
- Stream Support: Custom input/output stream handling for advanced use cases
Installation
Using Composer (Recommended)
composer require webfiori/http
Manual Installation
Download the latest release from GitHub Releases and include the autoloader:
require_once 'path/to/webfiori-http/vendor/autoload.php';
Quick Start
Modern Approach with Attributes (Recommended)
PHP 8+ attributes provide a clean, declarative way to define web services:
<?php use WebFiori\Http\WebService; use WebFiori\Http\Annotations\RestController; use WebFiori\Http\Annotations\GetMapping; use WebFiori\Http\Annotations\PostMapping; use WebFiori\Http\Annotations\RequestParam; use WebFiori\Http\Annotations\ResponseBody; use WebFiori\Http\Annotations\AllowAnonymous; use WebFiori\Http\ParamType; #[RestController('hello', 'A simple greeting service')] class HelloService extends WebService { #[GetMapping] #[ResponseBody] #[AllowAnonymous] #[RequestParam('name', ParamType::STRING, true)] public function sayHello(?string $name): string { return $name ? "Hello, $name!" : "Hello, World!"; } #[PostMapping] #[ResponseBody] #[AllowAnonymous] #[RequestParam('message', ParamType::STRING)] public function customGreeting(string $message): array { return ['greeting' => $message, 'timestamp' => time()]; } }
Traditional Approach
For comparison, here's the traditional approach using constructor configuration:
<?php use WebFiori\Http\AbstractWebService; use WebFiori\Http\RequestMethod; use WebFiori\Http\ParamType; use WebFiori\Http\ParamOption; class HelloService extends AbstractWebService { public function __construct() { parent::__construct('hello'); $this->setRequestMethods([RequestMethod::GET]); $this->addParameters([ 'name' => [ ParamOption::TYPE => ParamType::STRING, ParamOption::OPTIONAL => true ] ]); } public function isAuthorized() { return true; } public function processRequest() { $name = $this->getParamVal('name'); $this->sendResponse($name ? "Hello, $name!" : "Hello, World!"); } }
Both approaches work with RequestProcessor (recommended) or WebServicesManager:
// Recommended: process a single service directly $processor = new RequestProcessor(); $processor->process(new HelloService()); // Legacy: register services in a manager $manager = new WebServicesManager(); $manager->addService(new HelloService()); $manager->process();
Core Concepts
Terminology
| Term | Definition |
|---|---|
| Web Service | A single endpoint that implements a REST service, represented by AbstractWebService |
| Services Manager | An entity that manages multiple web services, represented by WebServicesManager |
| Request Parameter | A way to pass values from client to server, represented by RequestParameter |
| API Filter | A component that validates and sanitizes request parameters |
Architecture Overview
The library follows a service-oriented architecture:
- AbstractWebService: Base class for all web services
- WebServicesManager: Manages multiple services and handles request routing
- RequestParameter: Defines and validates individual parameters
- APIFilter: Handles parameter filtering and validation
- Request/Response: Utilities for handling HTTP requests and responses
Creating Web Services
Using Attributes (Recommended)
PHP 8+ attributes provide a modern, declarative approach:
<?php use WebFiori\Http\WebService; use WebFiori\Http\Annotations\RestController; use WebFiori\Http\Annotations\GetMapping; use WebFiori\Http\Annotations\PostMapping; use WebFiori\Http\Annotations\PutMapping; use WebFiori\Http\Annotations\DeleteMapping; use WebFiori\Http\Annotations\RequestParam; use WebFiori\Http\Annotations\ResponseBody; use WebFiori\Http\Annotations\RequiresAuth; use WebFiori\Http\ParamType; #[RestController('users', 'User management operations')] #[RequiresAuth] class UserService extends WebService { #[GetMapping] #[ResponseBody] #[RequestParam('id', ParamType::INT, true)] public function getUser(?int $id): array { return ['id' => $id ?? 1, 'name' => 'John Doe']; } #[PostMapping] #[ResponseBody] #[RequestParam('name', ParamType::STRING)] #[RequestParam('email', ParamType::EMAIL)] public function createUser(string $name, string $email): array { return ['id' => 2, 'name' => $name, 'email' => $email]; } #[PutMapping] #[ResponseBody] #[RequestParam('id', ParamType::INT)] #[RequestParam('name', ParamType::STRING)] public function updateUser(int $id, string $name): array { return ['id' => $id, 'name' => $name]; } #[DeleteMapping] #[ResponseBody] #[RequestParam('id', ParamType::INT)] public function deleteUser(int $id): array { return ['deleted' => $id]; } }
Traditional Class-Based Approach
Every web service must extend AbstractWebService and implement the processRequest() method:
<?php use WebFiori\Http\AbstractWebService; use WebFiori\Http\RequestMethod; class MyService extends AbstractWebService { public function __construct() { parent::__construct('my-service'); $this->setRequestMethods([RequestMethod::GET, RequestMethod::POST]); $this->setDescription('A sample web service'); } public function isAuthorized() { // Implement authorization logic return true; } public function processRequest() { // Implement service logic $this->sendResponse('Service executed successfully'); } }
Service Configuration
Setting Request Methods
// Single method $this->addRequestMethod(RequestMethod::POST); // Multiple methods $this->setRequestMethods([ RequestMethod::GET, RequestMethod::POST, RequestMethod::PUT ]);
Service Metadata
$this->setDescription('Creates a new user profile'); $this->setSince('1.2.0'); $this->addResponseDescription('Returns user profile data on success'); $this->addResponseDescription('Returns error message on failure');
Parameter Management
Parameter Types
The library supports various parameter types through ParamType:
ParamType::STRING // String values ParamType::INT // Integer values ParamType::DOUBLE // Float/double values ParamType::BOOL // Boolean values ParamType::EMAIL // Email addresses (validated) ParamType::URL // URLs (validated) ParamType::ARR // Arrays ParamType::JSON_OBJ // JSON objects
Adding Parameters
Simple Parameter Addition
use WebFiori\Http\RequestParameter; $param = new RequestParameter('username', ParamType::STRING); $this->addParameter($param);
Batch Parameter Addition
$this->addParameters([ 'username' => [ ParamOption::TYPE => ParamType::STRING, ParamOption::OPTIONAL => false ], 'age' => [ ParamOption::TYPE => ParamType::INT, ParamOption::OPTIONAL => true, ParamOption::MIN => 18, ParamOption::MAX => 120, ParamOption::DEFAULT => 25 ], 'email' => [ ParamOption::TYPE => ParamType::EMAIL, ParamOption::OPTIONAL => false ] ]);
Parameter Options
Available options through ParamOption:
ParamOption::TYPE // Parameter data type ParamOption::OPTIONAL // Whether parameter is optional ParamOption::DEFAULT // Default value for optional parameters ParamOption::MIN // Minimum value (numeric types) ParamOption::MAX // Maximum value (numeric types) ParamOption::MIN_LENGTH // Minimum length (string types) ParamOption::MAX_LENGTH // Maximum length (string types) ParamOption::EMPTY // Allow empty strings ParamOption::FILTER // Custom filter function ParamOption::DESCRIPTION // Parameter description ParamOption::ALLOWED_VALUES // Restrict to a set of allowed values ParamOption::PATTERN // Regex pattern for validation
Custom Validation
$this->addParameters([ 'password' => [ ParamOption::TYPE => ParamType::STRING, ParamOption::MIN_LENGTH => 8, ParamOption::FILTER => function($original, $basic) { // Custom validation logic if (strlen($basic) < 8) { return APIFilter::INVALID; } // Additional password strength checks return $basic; } ] ]);
Retrieving Parameter Values
public function processRequest() { $username = $this->getParamVal('username'); $age = $this->getParamVal('age'); $email = $this->getParamVal('email'); // Get all inputs as array $allInputs = $this->getInputs(); }
Positional Parameter Injection
When using #[ResponseBody], method parameters are matched positionally to #[RequestParam] attributes. The PHP variable names do not need to match the request parameter names:
#[GetMapping] #[ResponseBody] #[AllowAnonymous] #[RequestParam('app-id', ParamType::INT)] #[RequestParam('user-name', ParamType::STRING, true)] public function getData(int $id, ?string $name): array { // $id receives the value of 'app-id' (1st attribute → 1st parameter) // $name receives the value of 'user-name' (2nd attribute → 2nd parameter) return ['id' => $id, 'name' => $name]; }
Reusable Parameter Sets
Implement the ParameterSet interface to group related parameters:
class PaginationParams implements ParameterSet { public function getParameters(): array { return [ 'page' => [ParamOption::TYPE => ParamType::INT, ParamOption::OPTIONAL => true, ParamOption::DEFAULT => 1], 'per_page' => [ParamOption::TYPE => ParamType::INT, ParamOption::OPTIONAL => true, ParamOption::DEFAULT => 20], ]; } }
Use with attributes:
#[GetMapping] #[ResponseBody] #[UseParameterSet(PaginationParams::class)] public function listItems(int $page = 1, int $perPage = 20): array { ... }
Or traditionally:
$this->addParameterSet(new PaginationParams());
Cross-Field Validation
For validation rules that depend on multiple parameters together, use the #[Validate] attribute or override the validate() method:
Method-Specific Validation (Attribute)
#[PostMapping] #[ResponseBody] #[Validate('validateRegistration')] #[RequestParam('password', ParamType::STRING)] #[RequestParam('password_confirm', ParamType::STRING)] public function register(string $password, string $passwordConfirm): array { ... } private function validateRegistration(array $inputs): array { $errors = []; if ($inputs['password'] !== $inputs['password_confirm']) { $errors['password_confirm'] = 'Passwords do not match.'; } return $errors; // empty = pass }
Service-Wide Validation (Override)
public function validate(array $inputs): array { $errors = []; if (isset($inputs['end_date']) && $inputs['end_date'] <= $inputs['start_date']) { $errors['end_date'] = 'End date must be after start date.'; } return $errors; }
Both run if defined — service-wide first, then method-specific. Errors are merged. If any errors exist, the request returns 422 with the error details.
Dynamic Status Codes with ResponseEntity
The ResponseEntity class allows #[ResponseBody] methods to return different HTTP status codes based on runtime logic:
use WebFiori\Http\ResponseEntity; use WebFiori\Json\Json; #[PostMapping] #[ResponseBody] #[AllowAnonymous] #[RequestParam('username', ParamType::STRING)] #[RequestParam('password', ParamType::STRING)] public function login(string $username, string $password): ResponseEntity { if ($username === 'admin' && $password === 'secret') { return ResponseEntity::ok(new Json(['token' => 'abc123'])); } return ResponseEntity::unauthorized(new Json(['message' => 'Invalid credentials'])); }
Available Factory Methods
| Method | Status Code | Use Case |
|---|---|---|
ResponseEntity::ok($body) |
200 | Successful response |
ResponseEntity::created($body) |
201 | Resource created |
ResponseEntity::noContent() |
204 | Successful deletion |
ResponseEntity::badRequest($body) |
400 | Invalid input |
ResponseEntity::unauthorized($body) |
401 | Authentication failure |
ResponseEntity::forbidden($body) |
403 | Authorization failure |
ResponseEntity::notFound($body) |
404 | Resource not found |
ResponseEntity::error($body) |
500 | Server error |
You can also use the constructor directly for custom status codes:
return new ResponseEntity($body, 418, 'text/plain');
Testing
Using APITestCase
<?php use WebFiori\Http\APITestCase; class MyServiceTest extends APITestCase { public function testGetRequest() { $manager = new WebServicesManager(); $manager->addService(new MyService()); $response = $this->getRequest($manager, 'my-service', [ 'param1' => 'value1', 'param2' => 'value2' ]); $this->assertJson($response); $this->assertContains('success', $response); } public function testPostRequest() { $manager = new WebServicesManager(); $manager->addService(new MyService()); $response = $this->postRequest($manager, 'my-service', [ 'name' => 'John Doe', 'email' => 'john@example.com' ]); $this->assertJson($response); } }
Examples
Complete CRUD Service Example
<?php use WebFiori\Http\WebService; use WebFiori\Http\Annotations\RestController; use WebFiori\Http\Annotations\GetMapping; use WebFiori\Http\Annotations\PostMapping; use WebFiori\Http\Annotations\PutMapping; use WebFiori\Http\Annotations\DeleteMapping; use WebFiori\Http\Annotations\RequestParam; use WebFiori\Http\Annotations\ResponseBody; use WebFiori\Http\Annotations\AllowAnonymous; use WebFiori\Http\ParamType; #[RestController('tasks', 'Task management service')] class TaskService extends WebService { #[GetMapping] #[ResponseBody] #[AllowAnonymous] public function getTasks(): array { return [ 'tasks' => [ ['id' => 1, 'title' => 'Task 1', 'completed' => false], ['id' => 2, 'title' => 'Task 2', 'completed' => true] ], 'count' => 2 ]; } #[PostMapping] #[ResponseBody] #[AllowAnonymous] #[RequestParam('title', ParamType::STRING)] #[RequestParam('description', ParamType::STRING, true)] public function createTask(string $title, ?string $description): array { return [ 'id' => 3, 'title' => $title, 'description' => $description ?: '', 'completed' => false ]; } #[PutMapping] #[ResponseBody] #[AllowAnonymous] #[RequestParam('id', ParamType::INT)] #[RequestParam('title', ParamType::STRING, true)] public function updateTask(int $id, ?string $title): array { return [ 'id' => $id, 'title' => $title, 'updated_at' => date('Y-m-d H:i:s') ]; } #[DeleteMapping] #[ResponseBody] #[AllowAnonymous] #[RequestParam('id', ParamType::INT)] public function deleteTask(int $id): array { return [ 'id' => $id, 'deleted_at' => date('Y-m-d H:i:s') ]; } }
For more examples, check the examples directory in this repository.
Key Classes Documentation
AbstractWebService- Base class for web servicesWebServicesManager- Services managementRequestParameter- Parameter definition and validationAPIFilter- Input filtering and validationRequest- HTTP request utilitiesResponse- HTTP response utilitiesErrorResponse- Standardized error response generationOpenAPIGenerator- Standalone OpenAPI spec generation
Content Negotiation
Use #[Produces] to declare what content types a method can return. The framework matches against the client's Accept header:
use WebFiori\Http\Annotations\Produces; use WebFiori\Http\MediaType; use WebFiori\Http\ResponseEntity; #[GetMapping] #[ResponseBody] #[Produces(MediaType::JSON, MediaType::XML)] public function getUser(int $id): ResponseEntity { $type = $this->getNegotiatedContentType(); if ($type === MediaType::XML) { return new ResponseEntity('<user>...</user>', 200, MediaType::XML); } return ResponseEntity::ok(new Json(['id' => $id])); }
- No
#[Produces]→ always JSON (default, unchanged) Acceptheader doesn't match → 406 Not AcceptableAccept: */*or not set → server's first preference
Contributing
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Support
- Issues: GitHub Issues
- Examples: Examples Directory
Changelog
See CHANGELOG.md for a list of changes and version history.