Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
* text=auto eol=lf
*.php text eol=lf
*.md text eol=lf
*.json text eol=lf
*.xml text eol=lf
*.yml text eol=lf
*.yaml text eol=lf

#perform LF normalization
* text eol=crlf
*.php text eol=crlf
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ With well-established PHP HTTP libraries available, you might wonder why this on
- **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
Expand Down
48 changes: 25 additions & 23 deletions WebFiori/Http/APIFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@ private static function applyBasicFilterOnly($def,$toBeFiltered) {
if (gettype($toBeFiltered) == 'array') {
return $toBeFiltered;
}

if (gettype($toBeFiltered) == 'boolean') {
return $toBeFiltered;
}
Expand Down Expand Up @@ -450,6 +451,29 @@ private function applyJsonBasicFilter(Json $extraClean, $toBeFiltered, $def) {
$extraClean->addNull($name);
}
}
/**
* Checks allowed values and pattern constraints on a filtered value.
*
* @param mixed $value The filtered value.
* @param RequestParameter $param The parameter definition.
*
* @return mixed The value if valid, or self::INVALID if constraints fail.
*/
private static function checkAllowedAndPattern($value, RequestParameter $param) {
$allowed = $param->getAllowedValues();

if (!empty($allowed) && !in_array($value, $allowed, true)) {
return self::INVALID;
}

$pattern = $param->getPattern();

if ($pattern !== null && is_string($value) && !preg_match($pattern, $value)) {
return self::INVALID;
}

return $value;
}
private function checkExtracted(Json $extraClean, $name, $defaultVal) {
$extractedVal = $extraClean->get($name);

Expand Down Expand Up @@ -555,6 +579,7 @@ private function cleanJsonStr($extraClean, $def, $toBeFiltered) {

if (strlen($cleaned) == 0 && $def['options']['options']['allow-empty'] === false) {
$extraClean->add($name, null);

return;
}

Expand Down Expand Up @@ -980,27 +1005,4 @@ private function setInputStreamHelper($trimmed, $mode) : bool {

return false;
}
/**
* Checks allowed values and pattern constraints on a filtered value.
*
* @param mixed $value The filtered value.
* @param RequestParameter $param The parameter definition.
*
* @return mixed The value if valid, or self::INVALID if constraints fail.
*/
private static function checkAllowedAndPattern($value, RequestParameter $param) {
$allowed = $param->getAllowedValues();

if (!empty($allowed) && !in_array($value, $allowed, true)) {
return self::INVALID;
}

$pattern = $param->getPattern();

if ($pattern !== null && is_string($value) && !preg_match($pattern, $value)) {
return self::INVALID;
}

return $value;
}
}
40 changes: 40 additions & 0 deletions WebFiori/Http/Annotations/Consumes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

/**
* This file is licensed under MIT License.
*
* Copyright (c) 2026-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
*/
namespace WebFiori\Http\Annotations;

use Attribute;

/**
* Declares the content types a method can consume (accept in request body).
*
* Used for per-method content type control — overrides the default allowed
* types (application/x-www-form-urlencoded, multipart/form-data, application/json)
* for POST and PUT requests.
*
* When a non-standard content type is consumed (one that is not form-encoded
* or JSON), parameter filtering/parsing is skipped and the raw body is
* available via php://input.
*
* Usage:
* ```php
* #[PostMapping]
* #[Consumes(MediaType::OCTET_STREAM)]
* public function uploadFile(): ResponseEntity { ... }
* ```
*/
#[Attribute(Attribute::TARGET_METHOD)]
class Consumes {
public readonly array $contentTypes;

public function __construct(string ...$contentTypes) {
$this->contentTypes = $contentTypes;
}
}
38 changes: 19 additions & 19 deletions WebFiori/Http/ErrorResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,25 @@ public static function missingServiceName() : array {

return ['json' => $json, 'code' => 404];
}
/**
* Generates a 406 Not Acceptable response.
*
* @param array $supported The content types the server can produce.
*
* @return array{json: Json, code: int} The response body and HTTP code.
*/
public static function notAcceptable(array $supported = []) : array {
$json = new Json();
$json->add('message', 'Not Acceptable');
$json->add('type', WebService::E);
$json->add('http-code', 406);

if (!empty($supported)) {
$json->add('more-info', new Json(['supported' => $supported]));
}

return ['json' => $json, 'code' => 406];
}
/**
* Generates a 404 response for unsupported service.
*
Expand Down Expand Up @@ -166,25 +185,6 @@ public static function unauthorized(?string $message = null) : array {

return ['json' => $json, 'code' => 401];
}
/**
* Generates a 406 Not Acceptable response.
*
* @param array $supported The content types the server can produce.
*
* @return array{json: Json, code: int} The response body and HTTP code.
*/
public static function notAcceptable(array $supported = []) : array {
$json = new Json();
$json->add('message', 'Not Acceptable');
$json->add('type', WebService::E);
$json->add('http-code', 406);

if (!empty($supported)) {
$json->add('more-info', new Json(['supported' => $supported]));
}

return ['json' => $json, 'code' => 406];
}
/**
* Formats an array of parameter names into a comma-separated quoted string.
*/
Expand Down
77 changes: 38 additions & 39 deletions WebFiori/Http/OpenAPI/OpenAPIGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,44 @@
* @author Ibrahim
*/
class OpenAPIGenerator {
/**
* Discovers WebService instances in a given namespace.
*
* Scans all declared classes for those belonging to the namespace,
* extending WebService, having #[RestController], and not being abstract.
*
* @param string $namespace The namespace to scan.
*
* @return WebService[] Array of instantiated service objects.
*/
public static function discoverServices(string $namespace) : array {
$namespace = rtrim($namespace, '\\').'\\';
$services = [];

foreach (get_declared_classes() as $class) {
if (!str_starts_with($class, $namespace)) {
continue;
}

if (!is_subclass_of($class, WebService::class)) {
continue;
}

$reflection = new \ReflectionClass($class);

if ($reflection->isAbstract()) {
continue;
}

if (empty($reflection->getAttributes(RestController::class))) {
continue;
}

$services[] = new $class();
}

return $services;
}
/**
* Generates an OpenAPI specification from an array of web services.
*
Expand Down Expand Up @@ -68,43 +106,4 @@ public function generateFromNamespace(string $namespace, string $description = '

return $this->generate($services, $description, $version, $basePath);
}

/**
* Discovers WebService instances in a given namespace.
*
* Scans all declared classes for those belonging to the namespace,
* extending WebService, having #[RestController], and not being abstract.
*
* @param string $namespace The namespace to scan.
*
* @return WebService[] Array of instantiated service objects.
*/
public static function discoverServices(string $namespace) : array {
$namespace = rtrim($namespace, '\\') . '\\';
$services = [];

foreach (get_declared_classes() as $class) {
if (!str_starts_with($class, $namespace)) {
continue;
}

if (!is_subclass_of($class, WebService::class)) {
continue;
}

$reflection = new \ReflectionClass($class);

if ($reflection->isAbstract()) {
continue;
}

if (empty($reflection->getAttributes(RestController::class))) {
continue;
}

$services[] = new $class();
}

return $services;
}
}
1 change: 1 addition & 0 deletions WebFiori/Http/OpenAPI/OpenAPIObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public function getDescription() : ?string {

public function setDescription(string $description) : static {
$this->description = $description;

return $this;
}
/**
Expand Down
10 changes: 5 additions & 5 deletions WebFiori/Http/OpenAPI/OpenAPISpecService.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@
*/
#[RestController(name: 'openapi', description: 'OpenAPI specification endpoint')]
class OpenAPISpecService extends WebService {
private string $namespace;
private string $apiBasePath;
private string $apiTitle;
private string $apiVersion;
private string $namespace;

/**
* Creates a new OpenAPI spec service.
Expand All @@ -55,10 +55,6 @@ public function __construct(string $namespace, string $basePath = '', string $ti
$this->apiVersion = $version;
}

public function isAuthorized(): bool {
return true;
}

#[GetMapping]
#[ResponseBody]
#[AllowAnonymous]
Expand All @@ -73,6 +69,10 @@ public function getSpec(): JsonI {
);
}

public function isAuthorized(): bool {
return true;
}

public function processRequest() {
}
}
Loading
Loading