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 - Per-Method Content Type Control:
#[Consumes]annotation to accept custom content types (e.g.application/octet-stream,application/xml) on specific methods - Object Mapping: Automatic mapping of request parameters to PHP objects
- Comprehensive Testing: Built-in testing utilities with
ServiceTestCaseclass - 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:
<?phpuseWebFiori\Http\WebService;
useWebFiori\Http\Annotations\RestController;
useWebFiori\Http\Annotations\GetMapping;
useWebFiori\Http\Annotations\PostMapping;
useWebFiori\Http\Annotations\RequestParam;
useWebFiori\Http\Annotations\ResponseBody;
useWebFiori\Http\Annotations\AllowAnonymous;
useWebFiori\Http\ParamType;
#[RestController('hello', 'A simple greeting service')]
class HelloService extends WebService {
#[GetMapping]
#[ResponseBody]
#[AllowAnonymous]
#[RequestParam('name', ParamType::STRING, true)]
publicfunctionsayHello(?string$name): string {
return$name ? "Hello, $name!" : "Hello, World!";
}
#[PostMapping]
#[ResponseBody]
#[AllowAnonymous]
#[RequestParam('message', ParamType::STRING)]
publicfunctioncustomGreeting(string$message): array {
return ['greeting' => $message, 'timestamp' => time()];
}
}For comparison, here's the traditional approach using constructor configuration:
<?phpuseWebFiori\Http\AbstractWebService;
useWebFiori\Http\RequestMethod;
useWebFiori\Http\ParamType;
useWebFiori\Http\ParamOption;
class HelloService extends AbstractWebService {
publicfunction__construct() {
parent::__construct('hello');
$this->setRequestMethods([RequestMethod::GET]);
$this->addParameters([
'name' => [
ParamOption::TYPE => ParamType::STRING,
ParamOption::OPTIONAL => true
]
]);
}
publicfunctionisAuthorized() {
returntrue;
}
publicfunctionprocessRequest() {
$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 = newRequestProcessor();
$processor->process(newHelloService());
// Legacy: register services in a manager$manager = newWebServicesManager();
$manager->addService(newHelloService());
$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:
<?phpuseWebFiori\Http\WebService;
useWebFiori\Http\Annotations\RestController;
useWebFiori\Http\Annotations\GetMapping;
useWebFiori\Http\Annotations\PostMapping;
useWebFiori\Http\Annotations\PutMapping;
useWebFiori\Http\Annotations\DeleteMapping;
useWebFiori\Http\Annotations\RequestParam;
useWebFiori\Http\Annotations\ResponseBody;
useWebFiori\Http\Annotations\RequiresAuth;
useWebFiori\Http\ParamType;
#[RestController('users', 'User management operations')]
#[RequiresAuth]
class UserService extends WebService {
#[GetMapping]
#[ResponseBody]
#[RequestParam('id', ParamType::INT, true)]
publicfunctiongetUser(?int$id): array {
return ['id' => $id ?? 1, 'name' => 'John Doe'];
}
#[PostMapping]
#[ResponseBody]
#[RequestParam('name', ParamType::STRING)]
#[RequestParam('email', ParamType::EMAIL)]
publicfunctioncreateUser(string$name, string$email): array {
return ['id' => 2, 'name' => $name, 'email' => $email];
}
#[PutMapping]
#[ResponseBody]
#[RequestParam('id', ParamType::INT)]
#[RequestParam('name', ParamType::STRING)]
publicfunctionupdateUser(int$id, string$name): array {
return ['id' => $id, 'name' => $name];
}
#[DeleteMapping]
#[ResponseBody]
#[RequestParam('id', ParamType::INT)]
publicfunctiondeleteUser(int$id): array {
return ['deleted' => $id];
}
}Every web service must extend AbstractWebService and implement the processRequest() method:
<?phpuseWebFiori\Http\AbstractWebService;
useWebFiori\Http\RequestMethod;
class MyService extends AbstractWebService {
publicfunction__construct() {
parent::__construct('my-service');
$this->setRequestMethods([RequestMethod::GET, RequestMethod::POST]);
$this->setDescription('A sample web service');
}
publicfunctionisAuthorized() {
// Implement authorization logicreturntrue;
}
publicfunctionprocessRequest() {
// 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 objectsuseWebFiori\Http\RequestParameter;
$param = newRequestParameter('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 logicif (strlen($basic) < 8) {
return APIFilter::INVALID;
}
// Additional password strength checksreturn$basic;
}
]
]);publicfunctionprocessRequest() {
$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)]
publicfunctiongetData(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];
}By default, sending an empty string for a string parameter results in a validation error. Use allowEmpty: true in the #[RequestParam] attribute to accept empty strings:
#[PostMapping]
#[ResponseBody]
#[RequestParam(name: 'notes', type: ParamType::STRING, optional: true, allowEmpty: true)]
publicfunctioncreate(?string$notes): array {
return ['notes' => $notes ?? ''];
}This is the attribute equivalent of ParamOption::EMPTY => true in the array-based approach.
Implement the ParameterSet interface to group related parameters:
class PaginationParams implements ParameterSet {
publicfunctiongetParameters(): 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)]
publicfunctionlistItems(int$page = 1, int$perPage = 20): array { ... }Or traditionally:
$this->addParameterSet(newPaginationParams());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)]
publicfunctionregister(string$password, string$passwordConfirm): array { ... }
privatefunctionvalidateRegistration(array$inputs): array {
$errors = [];
if ($inputs['password'] !== $inputs['password_confirm']) {
$errors['password_confirm'] = 'Passwords do not match.';
}
return$errors; // empty = pass
}publicfunctionvalidate(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:
useWebFiori\Http\ResponseEntity;
useWebFiori\Json\Json;
#[PostMapping]
#[ResponseBody]
#[AllowAnonymous]
#[RequestParam('username', ParamType::STRING)]
#[RequestParam('password', ParamType::STRING)]
publicfunctionlogin(string$username, string$password): ResponseEntity {
if ($username === 'admin' && $password === 'secret') {
return ResponseEntity::ok(newJson(['token' => 'abc123']));
}
return ResponseEntity::unauthorized(newJson(['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:
returnnewResponseEntity($body, 418, 'text/plain');<?phpuseWebFiori\Http\Test\ServiceTestCase;
class MyServiceTest extends ServiceTestCase {
publicfunctiontestGetRequest() {
$this->get(newMyService(), [
'param1' => 'value1',
'param2' => 'value2'
])
->assertOk()
->assertJson()
->assertBodyContains('success');
}
publicfunctiontestPostRequest() {
$this->post(newMyService(), [
'name' => 'John Doe',
'email' => 'john@example.com'
])
->assertOk()
->assertJson();
}
}<?phpuseWebFiori\Http\WebService;
useWebFiori\Http\Annotations\RestController;
useWebFiori\Http\Annotations\GetMapping;
useWebFiori\Http\Annotations\PostMapping;
useWebFiori\Http\Annotations\PutMapping;
useWebFiori\Http\Annotations\DeleteMapping;
useWebFiori\Http\Annotations\RequestParam;
useWebFiori\Http\Annotations\ResponseBody;
useWebFiori\Http\Annotations\AllowAnonymous;
useWebFiori\Http\ParamType;
#[RestController('tasks', 'Task management service')]
class TaskService extends WebService {
#[GetMapping]
#[ResponseBody]
#[AllowAnonymous]
publicfunctiongetTasks(): 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)]
publicfunctioncreateTask(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)]
publicfunctionupdateTask(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)]
publicfunctiondeleteTask(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:
useWebFiori\Http\Annotations\Produces;
useWebFiori\Http\MediaType;
useWebFiori\Http\ResponseEntity;
#[GetMapping]
#[ResponseBody]
#[Produces(MediaType::JSON, MediaType::XML)]
publicfunctiongetUser(int$id): ResponseEntity {
$type = $this->getNegotiatedContentType();
if ($type === MediaType::XML) {
returnnewResponseEntity('<user>...</user>', 200, MediaType::XML);
}
return ResponseEntity::ok(newJson(['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.