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.
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.
| Build Status |
|---|
- 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
composer require webfiori/httpDownload the latest release from GitHub Releases and include the autoloader:
require_once 'path/to/webfiori-http/vendor/autoload.php';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()];
}
}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();| 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 |
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
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];
}
}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');
}
}// Single method
$this->addRequestMethod(RequestMethod::POST);
// Multiple methods
$this->setRequestMethods([
RequestMethod::GET,
RequestMethod::POST,
RequestMethod::PUT
]);$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');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 objectsuse WebFiori\Http\RequestParameter;
$param = new RequestParameter('username', ParamType::STRING);
$this->addParameter($param);$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
]
]);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$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;
}
]
]);public function processRequest() {
$username = $this->getParamVal('username');
$age = $this->getParamVal('age');
$email = $this->getParamVal('email');
// Get all inputs as array
$allInputs = $this->getInputs();
}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];
}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());For validation rules that depend on multiple parameters together, use the #[Validate] attribute or override the validate() method:
#[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
}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.
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']));
}| 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');<?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);
}
}<?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.
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
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
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.
This project is licensed under the MIT License - see the LICENSE file for details.
- Issues: GitHub Issues
- Examples: Examples Directory
See CHANGELOG.md for a list of changes and version history.