Repository files navigation

WebFiori HTTP

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, and multipart/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 ServiceTestCase class
  • 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:

<?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()];
}
}

Traditional Approach

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();

Core Concepts

Terminology

TermDefinition
Web ServiceA single endpoint that implements a REST service, represented by AbstractWebService
Services ManagerAn entity that manages multiple web services, represented by WebServicesManager
Request ParameterA way to pass values from client to server, represented by RequestParameter
API FilterA component that validates and sanitizes request parameters

Architecture Overview

The library follows a service-oriented architecture:

  1. AbstractWebService: Base class for all web services
  2. WebServicesManager: Manages multiple services and handles request routing
  3. RequestParameter: Defines and validates individual parameters
  4. APIFilter: Handles parameter filtering and validation
  5. Request/Response: Utilities for handling HTTP requests and responses

Creating Web Services

Using Attributes (Recommended)

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];
}
}

Traditional Class-Based Approach

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');
}
}

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

useWebFiori\Http\RequestParameter;
$param = newRequestParameter('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 logicif (strlen($basic) < 8) {
return APIFilter::INVALID;
}
// Additional password strength checksreturn$basic;
}
]
]);

Retrieving Parameter Values

publicfunctionprocessRequest() {
$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)]
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];
}

Allowing Empty Strings

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.

Reusable Parameter Sets

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());

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)]
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
}

Service-Wide Validation (Override)

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.

Dynamic Status Codes with ResponseEntity

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']));
}

Available Factory Methods

MethodStatus CodeUse Case
ResponseEntity::ok($body)200Successful response
ResponseEntity::created($body)201Resource created
ResponseEntity::noContent()204Successful deletion
ResponseEntity::badRequest($body)400Invalid input
ResponseEntity::unauthorized($body)401Authentication failure
ResponseEntity::forbidden($body)403Authorization failure
ResponseEntity::notFound($body)404Resource not found
ResponseEntity::error($body)500Server error

You can also use the constructor directly for custom status codes:

returnnewResponseEntity($body, 418, 'text/plain');

Testing

Using ServiceTestCase

<?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();
}
}

Examples

Complete CRUD Service Example

<?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.

Key Classes Documentation

Content Negotiation

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)
  • Accept header doesn't match → 406 Not Acceptable
  • Accept: */* 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

Changelog

See CHANGELOG.md for a list of changes and version history.

About

HTTP handling helper library of WebFiori Framework.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

7 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

WebFiori HTTP

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, and multipart/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 ServiceTestCase class
  • 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:

<?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()];
}
}

Traditional Approach

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();

Core Concepts

Terminology

TermDefinition
Web ServiceA single endpoint that implements a REST service, represented by AbstractWebService
Services ManagerAn entity that manages multiple web services, represented by WebServicesManager
Request ParameterA way to pass values from client to server, represented by RequestParameter
API FilterA component that validates and sanitizes request parameters

Architecture Overview

The library follows a service-oriented architecture:

  1. AbstractWebService: Base class for all web services
  2. WebServicesManager: Manages multiple services and handles request routing
  3. RequestParameter: Defines and validates individual parameters
  4. APIFilter: Handles parameter filtering and validation
  5. Request/Response: Utilities for handling HTTP requests and responses

Creating Web Services

Using Attributes (Recommended)

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];
}
}

Traditional Class-Based Approach

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');
}
}

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

useWebFiori\Http\RequestParameter;
$param = newRequestParameter('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 logicif (strlen($basic) < 8) {
return APIFilter::INVALID;
}
// Additional password strength checksreturn$basic;
}
]
]);

Retrieving Parameter Values

publicfunctionprocessRequest() {
$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)]
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];
}

Allowing Empty Strings

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.

Reusable Parameter Sets

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());

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)]
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
}

Service-Wide Validation (Override)

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.

Dynamic Status Codes with ResponseEntity

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']));
}

Available Factory Methods

MethodStatus CodeUse Case
ResponseEntity::ok($body)200Successful response
ResponseEntity::created($body)201Resource created
ResponseEntity::noContent()204Successful deletion
ResponseEntity::badRequest($body)400Invalid input
ResponseEntity::unauthorized($body)401Authentication failure
ResponseEntity::forbidden($body)403Authorization failure
ResponseEntity::notFound($body)404Resource not found
ResponseEntity::error($body)500Server error

You can also use the constructor directly for custom status codes:

returnnewResponseEntity($body, 418, 'text/plain');

Testing

Using ServiceTestCase

<?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();
}
}

Examples

Complete CRUD Service Example

<?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.

Key Classes Documentation

Content Negotiation

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)
  • Accept header doesn't match → 406 Not Acceptable
  • Accept: */* 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

Changelog

See CHANGELOG.md for a list of changes and version history.

About

HTTP handling helper library of WebFiori Framework.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

7 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

WebFiori HTTP

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, and multipart/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 ServiceTestCase class
  • 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:

<?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()];
}
}

Traditional Approach

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();

Core Concepts

Terminology

TermDefinition
Web ServiceA single endpoint that implements a REST service, represented by AbstractWebService
Services ManagerAn entity that manages multiple web services, represented by WebServicesManager
Request ParameterA way to pass values from client to server, represented by RequestParameter
API FilterA component that validates and sanitizes request parameters

Architecture Overview

The library follows a service-oriented architecture:

  1. AbstractWebService: Base class for all web services
  2. WebServicesManager: Manages multiple services and handles request routing
  3. RequestParameter: Defines and validates individual parameters
  4. APIFilter: Handles parameter filtering and validation
  5. Request/Response: Utilities for handling HTTP requests and responses

Creating Web Services

Using Attributes (Recommended)

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];
}
}

Traditional Class-Based Approach

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');
}
}

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

useWebFiori\Http\RequestParameter;
$param = newRequestParameter('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 logicif (strlen($basic) < 8) {
return APIFilter::INVALID;
}
// Additional password strength checksreturn$basic;
}
]
]);

Retrieving Parameter Values

publicfunctionprocessRequest() {
$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)]
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];
}

Allowing Empty Strings

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.

Reusable Parameter Sets

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());

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)]
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
}

Service-Wide Validation (Override)

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.

Dynamic Status Codes with ResponseEntity

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']));
}

Available Factory Methods

MethodStatus CodeUse Case
ResponseEntity::ok($body)200Successful response
ResponseEntity::created($body)201Resource created
ResponseEntity::noContent()204Successful deletion
ResponseEntity::badRequest($body)400Invalid input
ResponseEntity::unauthorized($body)401Authentication failure
ResponseEntity::forbidden($body)403Authorization failure
ResponseEntity::notFound($body)404Resource not found
ResponseEntity::error($body)500Server error

You can also use the constructor directly for custom status codes:

returnnewResponseEntity($body, 418, 'text/plain');

Testing

Using ServiceTestCase

<?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();
}
}

Examples

Complete CRUD Service Example

<?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.

Key Classes Documentation

Content Negotiation

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)
  • Accept header doesn't match → 406 Not Acceptable
  • Accept: */* 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

Changelog

See CHANGELOG.md for a list of changes and version history.

About

HTTP handling helper library of WebFiori Framework.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

7 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

WebFiori HTTP

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, and multipart/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 ServiceTestCase class
  • 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:

<?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()];
}
}

Traditional Approach

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();

Core Concepts

Terminology

TermDefinition
Web ServiceA single endpoint that implements a REST service, represented by AbstractWebService
Services ManagerAn entity that manages multiple web services, represented by WebServicesManager
Request ParameterA way to pass values from client to server, represented by RequestParameter
API FilterA component that validates and sanitizes request parameters

Architecture Overview

The library follows a service-oriented architecture:

  1. AbstractWebService: Base class for all web services
  2. WebServicesManager: Manages multiple services and handles request routing
  3. RequestParameter: Defines and validates individual parameters
  4. APIFilter: Handles parameter filtering and validation
  5. Request/Response: Utilities for handling HTTP requests and responses

Creating Web Services

Using Attributes (Recommended)

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];
}
}

Traditional Class-Based Approach

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');
}
}

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

useWebFiori\Http\RequestParameter;
$param = newRequestParameter('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 logicif (strlen($basic) < 8) {
return APIFilter::INVALID;
}
// Additional password strength checksreturn$basic;
}
]
]);

Retrieving Parameter Values

publicfunctionprocessRequest() {
$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)]
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];
}

Allowing Empty Strings

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.

Reusable Parameter Sets

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());

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)]
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
}

Service-Wide Validation (Override)

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.

Dynamic Status Codes with ResponseEntity

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']));
}

Available Factory Methods

MethodStatus CodeUse Case
ResponseEntity::ok($body)200Successful response
ResponseEntity::created($body)201Resource created
ResponseEntity::noContent()204Successful deletion
ResponseEntity::badRequest($body)400Invalid input
ResponseEntity::unauthorized($body)401Authentication failure
ResponseEntity::forbidden($body)403Authorization failure
ResponseEntity::notFound($body)404Resource not found
ResponseEntity::error($body)500Server error

You can also use the constructor directly for custom status codes:

returnnewResponseEntity($body, 418, 'text/plain');

Testing

Using ServiceTestCase

<?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();
}
}

Examples

Complete CRUD Service Example

<?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.

Key Classes Documentation

Content Negotiation

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)
  • Accept header doesn't match → 406 Not Acceptable
  • Accept: */* 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

Changelog

See CHANGELOG.md for a list of changes and version history.

About

HTTP handling helper library of WebFiori Framework.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

7 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

WebFiori HTTP

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, and multipart/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 ServiceTestCase class
  • 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:

<?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()];
}
}

Traditional Approach

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();

Core Concepts

Terminology

TermDefinition
Web ServiceA single endpoint that implements a REST service, represented by AbstractWebService
Services ManagerAn entity that manages multiple web services, represented by WebServicesManager
Request ParameterA way to pass values from client to server, represented by RequestParameter
API FilterA component that validates and sanitizes request parameters

Architecture Overview

The library follows a service-oriented architecture:

  1. AbstractWebService: Base class for all web services
  2. WebServicesManager: Manages multiple services and handles request routing
  3. RequestParameter: Defines and validates individual parameters
  4. APIFilter: Handles parameter filtering and validation
  5. Request/Response: Utilities for handling HTTP requests and responses

Creating Web Services

Using Attributes (Recommended)

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];
}
}

Traditional Class-Based Approach

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');
}
}

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

useWebFiori\Http\RequestParameter;
$param = newRequestParameter('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 logicif (strlen($basic) < 8) {
return APIFilter::INVALID;
}
// Additional password strength checksreturn$basic;
}
]
]);

Retrieving Parameter Values

publicfunctionprocessRequest() {
$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)]
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];
}

Allowing Empty Strings

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.

Reusable Parameter Sets

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());

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)]
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
}

Service-Wide Validation (Override)

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.

Dynamic Status Codes with ResponseEntity

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']));
}

Available Factory Methods

MethodStatus CodeUse Case
ResponseEntity::ok($body)200Successful response
ResponseEntity::created($body)201Resource created
ResponseEntity::noContent()204Successful deletion
ResponseEntity::badRequest($body)400Invalid input
ResponseEntity::unauthorized($body)401Authentication failure
ResponseEntity::forbidden($body)403Authorization failure
ResponseEntity::notFound($body)404Resource not found
ResponseEntity::error($body)500Server error

You can also use the constructor directly for custom status codes:

returnnewResponseEntity($body, 418, 'text/plain');

Testing

Using ServiceTestCase

<?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();
}
}

Examples

Complete CRUD Service Example

<?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.

Key Classes Documentation

Content Negotiation

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)
  • Accept header doesn't match → 406 Not Acceptable
  • Accept: */* 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

Changelog

See CHANGELOG.md for a list of changes and version history.

About

HTTP handling helper library of WebFiori Framework.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

7 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

WebFiori HTTP

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, and multipart/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 ServiceTestCase class
  • 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:

<?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()];
}
}

Traditional Approach

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();

Core Concepts

Terminology

TermDefinition
Web ServiceA single endpoint that implements a REST service, represented by AbstractWebService
Services ManagerAn entity that manages multiple web services, represented by WebServicesManager
Request ParameterA way to pass values from client to server, represented by RequestParameter
API FilterA component that validates and sanitizes request parameters

Architecture Overview

The library follows a service-oriented architecture:

  1. AbstractWebService: Base class for all web services
  2. WebServicesManager: Manages multiple services and handles request routing
  3. RequestParameter: Defines and validates individual parameters
  4. APIFilter: Handles parameter filtering and validation
  5. Request/Response: Utilities for handling HTTP requests and responses

Creating Web Services

Using Attributes (Recommended)

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];
}
}

Traditional Class-Based Approach

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');
}
}

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

useWebFiori\Http\RequestParameter;
$param = newRequestParameter('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 logicif (strlen($basic) < 8) {
return APIFilter::INVALID;
}
// Additional password strength checksreturn$basic;
}
]
]);

Retrieving Parameter Values

publicfunctionprocessRequest() {
$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)]
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];
}

Allowing Empty Strings

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.

Reusable Parameter Sets

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());

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)]
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
}

Service-Wide Validation (Override)

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.

Dynamic Status Codes with ResponseEntity

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']));
}

Available Factory Methods

MethodStatus CodeUse Case
ResponseEntity::ok($body)200Successful response
ResponseEntity::created($body)201Resource created
ResponseEntity::noContent()204Successful deletion
ResponseEntity::badRequest($body)400Invalid input
ResponseEntity::unauthorized($body)401Authentication failure
ResponseEntity::forbidden($body)403Authorization failure
ResponseEntity::notFound($body)404Resource not found
ResponseEntity::error($body)500Server error

You can also use the constructor directly for custom status codes:

returnnewResponseEntity($body, 418, 'text/plain');

Testing

Using ServiceTestCase

<?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();
}
}

Examples

Complete CRUD Service Example

<?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.

Key Classes Documentation

Content Negotiation

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)
  • Accept header doesn't match → 406 Not Acceptable
  • Accept: */* 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

Changelog

See CHANGELOG.md for a list of changes and version history.

About

HTTP handling helper library of WebFiori Framework.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

7 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

WebFiori HTTP

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, and multipart/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 ServiceTestCase class
  • 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:

<?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()];
}
}

Traditional Approach

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();

Core Concepts

Terminology

TermDefinition
Web ServiceA single endpoint that implements a REST service, represented by AbstractWebService
Services ManagerAn entity that manages multiple web services, represented by WebServicesManager
Request ParameterA way to pass values from client to server, represented by RequestParameter
API FilterA component that validates and sanitizes request parameters

Architecture Overview

The library follows a service-oriented architecture:

  1. AbstractWebService: Base class for all web services
  2. WebServicesManager: Manages multiple services and handles request routing
  3. RequestParameter: Defines and validates individual parameters
  4. APIFilter: Handles parameter filtering and validation
  5. Request/Response: Utilities for handling HTTP requests and responses

Creating Web Services

Using Attributes (Recommended)

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];
}
}

Traditional Class-Based Approach

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');
}
}

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

useWebFiori\Http\RequestParameter;
$param = newRequestParameter('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 logicif (strlen($basic) < 8) {
return APIFilter::INVALID;
}
// Additional password strength checksreturn$basic;
}
]
]);

Retrieving Parameter Values

publicfunctionprocessRequest() {
$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)]
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];
}

Allowing Empty Strings

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.

Reusable Parameter Sets

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());

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)]
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
}

Service-Wide Validation (Override)

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.

Dynamic Status Codes with ResponseEntity

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']));
}

Available Factory Methods

MethodStatus CodeUse Case
ResponseEntity::ok($body)200Successful response
ResponseEntity::created($body)201Resource created
ResponseEntity::noContent()204Successful deletion
ResponseEntity::badRequest($body)400Invalid input
ResponseEntity::unauthorized($body)401Authentication failure
ResponseEntity::forbidden($body)403Authorization failure
ResponseEntity::notFound($body)404Resource not found
ResponseEntity::error($body)500Server error

You can also use the constructor directly for custom status codes:

returnnewResponseEntity($body, 418, 'text/plain');

Testing

Using ServiceTestCase

<?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();
}
}

Examples

Complete CRUD Service Example

<?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.

Key Classes Documentation

Content Negotiation

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)
  • Accept header doesn't match → 406 Not Acceptable
  • Accept: */* 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

Changelog

See CHANGELOG.md for a list of changes and version history.

About

HTTP handling helper library of WebFiori Framework.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

7 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

WebFiori HTTP

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, and multipart/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 ServiceTestCase class
  • 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:

<?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()];
}
}

Traditional Approach

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();

Core Concepts

Terminology

TermDefinition
Web ServiceA single endpoint that implements a REST service, represented by AbstractWebService
Services ManagerAn entity that manages multiple web services, represented by WebServicesManager
Request ParameterA way to pass values from client to server, represented by RequestParameter
API FilterA component that validates and sanitizes request parameters

Architecture Overview

The library follows a service-oriented architecture:

  1. AbstractWebService: Base class for all web services
  2. WebServicesManager: Manages multiple services and handles request routing
  3. RequestParameter: Defines and validates individual parameters
  4. APIFilter: Handles parameter filtering and validation
  5. Request/Response: Utilities for handling HTTP requests and responses

Creating Web Services

Using Attributes (Recommended)

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];
}
}

Traditional Class-Based Approach

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');
}
}

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

useWebFiori\Http\RequestParameter;
$param = newRequestParameter('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 logicif (strlen($basic) < 8) {
return APIFilter::INVALID;
}
// Additional password strength checksreturn$basic;
}
]
]);

Retrieving Parameter Values

publicfunctionprocessRequest() {
$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)]
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];
}

Allowing Empty Strings

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.

Reusable Parameter Sets

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());

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)]
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
}

Service-Wide Validation (Override)

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.

Dynamic Status Codes with ResponseEntity

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']));
}

Available Factory Methods

MethodStatus CodeUse Case
ResponseEntity::ok($body)200Successful response
ResponseEntity::created($body)201Resource created
ResponseEntity::noContent()204Successful deletion
ResponseEntity::badRequest($body)400Invalid input
ResponseEntity::unauthorized($body)401Authentication failure
ResponseEntity::forbidden($body)403Authorization failure
ResponseEntity::notFound($body)404Resource not found
ResponseEntity::error($body)500Server error

You can also use the constructor directly for custom status codes:

returnnewResponseEntity($body, 418, 'text/plain');

Testing

Using ServiceTestCase

<?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();
}
}

Examples

Complete CRUD Service Example

<?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.

Key Classes Documentation

Content Negotiation

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)
  • Accept header doesn't match → 406 Not Acceptable
  • Accept: */* 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

Changelog

See CHANGELOG.md for a list of changes and version history.

About

HTTP handling helper library of WebFiori Framework.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

7 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages