diff --git a/.gitignore b/.gitignore
index 2a0b5bc..66d6865 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,3 +17,4 @@ php-cs-fixer.phar
/.vscode
tests/WebFiori/Tests/Http/output-stream.txt
/OpenAPI_files
+/examples/01-core/04-file-uploads/uploads
diff --git a/LICENSE b/LICENSE
index 8eda521..fc8854a 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
- Copyright 2019 Ibrahim BinAlshikh, WebFiori HTTP.
+ Copyright 2019-present, WebFiori Framework.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 89951df..4f49475 100644
--- a/README.md
+++ b/README.md
@@ -26,12 +26,18 @@ A powerful and flexible PHP library for creating RESTful web APIs with built-in
- [Key Features](#key-features)
- [Installation](#installation)
- [Quick Start](#quick-start)
+ - [Modern Approach with Attributes](#modern-approach-with-attributes)
+ - [Traditional Approach](#traditional-approach)
- [Core Concepts](#core-concepts)
- [Creating Web Services](#creating-web-services)
+ - [Using Attributes (Recommended)](#using-attributes-recommended)
+ - [Traditional Class-Based Approach](#traditional-class-based-approach)
- [Parameter Management](#parameter-management)
- [Authentication & Authorization](#authentication--authorization)
- [Request & Response Handling](#request--response-handling)
- [Advanced Features](#advanced-features)
+ - [Object Mapping](#object-mapping-1)
+ - [OpenAPI Documentation](#openapi-documentation)
- [Testing](#testing)
- [Examples](#examples)
- [API Documentation](#api-documentation)
@@ -77,19 +83,53 @@ require_once 'path/to/webfiori-http/vendor/autoload.php';
## Quick Start
-Here's a simple example to get you started:
+### Modern Approach with Attributes
+
+PHP 8+ attributes provide a clean, declarative way to define web services:
```php
$message, 'timestamp' => time()];
+ }
+}
+```
+### Traditional Approach
+
+The traditional approach using constructor configuration:
+
+```php
+getParamVal('name');
-
- if ($name !== null) {
- $this->sendResponse("Hello, $name!");
- } else {
- $this->sendResponse("Hello, World!");
- }
+ $this->sendResponse($name ? "Hello, $name!" : "Hello, World!");
}
}
+```
+
+Both approaches work with `WebServicesManager`:
-// Set up the services manager
+```php
$manager = new WebServicesManager();
$manager->addService(new HelloService());
$manager->process();
@@ -148,7 +185,60 @@ The library follows a service-oriented architecture:
## Creating Web Services
-### Basic Service Structure
+### Using Attributes (Recommended)
+
+PHP 8+ attributes provide a modern, declarative approach:
+
+```php
+ $id ?? 1, 'name' => 'John Doe'];
+ }
+
+ #[PostMapping]
+ #[ResponseBody]
+ #[Param('name', ParamType::STRING, 'User name', minLength: 2)]
+ #[Param('email', ParamType::EMAIL, 'User email')]
+ public function createUser(string $name, string $email): array {
+ return ['id' => 2, 'name' => $name, 'email' => $email];
+ }
+
+ #[PutMapping]
+ #[ResponseBody]
+ #[Param('id', ParamType::INT, 'User ID')]
+ #[Param('name', ParamType::STRING, 'User name')]
+ public function updateUser(int $id, string $name): array {
+ return ['id' => $id, 'name' => $name];
+ }
+
+ #[DeleteMapping]
+ #[ResponseBody]
+ #[Param('id', ParamType::INT, 'User ID')]
+ public function deleteUser(int $id): array {
+ return ['deleted' => $id];
+ }
+}
+```
+
+### Traditional Class-Based Approach
Every web service must extend `AbstractWebService` and implement the `processRequest()` method:
@@ -527,7 +617,7 @@ $customFilter = function($original, $filtered) {
// Additional processing
return strtoupper($filtered);
-};
+];
$this->addParameters([
'code' => [
@@ -537,6 +627,36 @@ $this->addParameters([
]);
```
+### OpenAPI Documentation
+
+Generate OpenAPI 3.1.0 specification for your API:
+
+```php
+$manager = new WebServicesManager();
+$manager->setVersion('1.0.0');
+$manager->setDescription('My REST API');
+
+// Add your services
+$manager->addService(new UserService());
+$manager->addService(new ProductService());
+
+// Generate OpenAPI specification
+$openApiObj = $manager->toOpenAPI();
+
+// Customize if needed
+$info = $openApiObj->getInfo();
+$info->setTermsOfService('https://example.com/terms');
+
+// Output as JSON
+header('Content-Type: application/json');
+echo $openApiObj->toJSON();
+```
+
+The generated specification can be used with:
+- Swagger UI for interactive documentation
+- Postman for API testing
+- Code generators for client SDKs
+
## Testing
### Using APITestCase
@@ -735,9 +855,6 @@ class FileUploadService extends AbstractWebService {
For more examples, check the [examples](examples/) directory in this repository.
-## API Documentation
-
-This library is part of the WebFiori Framework. For complete API documentation, visit: https://webfiori.com/docs/webfiori/http
### Key Classes Documentation
diff --git a/WebFiori/Http/APIFilter.php b/WebFiori/Http/APIFilter.php
index 19fddfa..0cfcfce 100644
--- a/WebFiori/Http/APIFilter.php
+++ b/WebFiori/Http/APIFilter.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2019 WebFiori Framework
+ * Copyright (c) 2019-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/http/blob/master/LICENSE
diff --git a/WebFiori/Http/APITestCase.php b/WebFiori/Http/APITestCase.php
index e64ffc6..05c565d 100644
--- a/WebFiori/Http/APITestCase.php
+++ b/WebFiori/Http/APITestCase.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2019 WebFiori Framework
+ * Copyright (c) 2019-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/http/blob/master/LICENSE
@@ -128,13 +128,13 @@ public function addFile(string $fileIdx, string $filePath, bool $reset = false)
* to mimic HTTP request headers. The keys of the array are names of headers
* and the value of each key represents the value of the header.
*
- * @param UserInterface|null $user Optional user to authenticate the request with.
+ * @param SecurityPrincipal|null $user Optional user to authenticate the request with.
* to mimic HTTP request headers. The keys of the array are names of headers
* and the value of each key represents the value of the header.
*
* @return string The method will return the output of the endpoint.
*/
- public function callEndpoint(WebServicesManager $manager, string $requestMethod, string $apiEndpointName, array $parameters = [], array $httpHeaders = [], ?UserInterface $user = null) : string {
+ public function callEndpoint(WebServicesManager $manager, string $requestMethod, string $apiEndpointName, array $parameters = [], array $httpHeaders = [], ?SecurityPrincipal $user = null) : string {
$method = strtoupper($requestMethod);
$serviceName = $this->resolveServiceName($apiEndpointName);
@@ -188,7 +188,7 @@ private function resolveServiceName(string $nameOrClass): string {
* to mimic HTTP request headers. The keys of the array are names of headers
* and the value of each key represents the value of the header.
*
- * @param UserInterface|null $user Optional user to authenticate the request with.
+ * @param SecurityPrincipal|null $user Optional user to authenticate the request with.
*/
private function setupRequest(string $method, string $serviceName, array $parameters, array $httpHeaders) {
putenv('REQUEST_METHOD=' . $method);
@@ -267,14 +267,14 @@ public function format(string $output) {
* to mimic HTTP request headers. The keys of the array are names of headers
* and the value of each key represents the value of the header.
*
- * @param UserInterface|null $user Optional user to authenticate the request with.
+ * @param SecurityPrincipal|null $user Optional user to authenticate the request with.
* to mimic HTTP request headers. The keys of the array are names of headers
* and the value of each key represents the value of the header.
*
* @return string The method will return the output that was produced by
* the endpoint as string.
*/
- public function deleteRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = [], ?UserInterface $user = null) : string {
+ public function deleteRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = [], ?SecurityPrincipal $user = null) : string {
return $this->callEndpoint($manager, RequestMethod::DELETE, $endpoint, $parameters, $httpHeaders, $user);
}
/**
@@ -290,7 +290,7 @@ public function deleteRequest(WebServicesManager $manager, string $endpoint, arr
* @return string The method will return the output that was produced by
* the endpoint as string.
*/
- public function getRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = [], ?UserInterface $user = null) : string {
+ public function getRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = [], ?SecurityPrincipal $user = null) : string {
return $this->callEndpoint($manager, RequestMethod::GET, $endpoint, $parameters, $httpHeaders, $user);
}
/**
@@ -307,14 +307,14 @@ public function getRequest(WebServicesManager $manager, string $endpoint, array
* to mimic HTTP request headers. The keys of the array are names of headers
* and the value of each key represents the value of the header.
*
- * @param UserInterface|null $user Optional user to authenticate the request with.
+ * @param SecurityPrincipal|null $user Optional user to authenticate the request with.
* to mimic HTTP request headers. The keys of the array are names of headers
* and the value of each key represents the value of the header.
*
* @return string The method will return the output that was produced by
* the endpoint as string.
*/
- public function postRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = [], ?UserInterface $user = null) : string {
+ public function postRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = [], ?SecurityPrincipal $user = null) : string {
return $this->callEndpoint($manager, RequestMethod::POST, $endpoint, $parameters, $httpHeaders, $user);
}
/**
@@ -331,14 +331,14 @@ public function postRequest(WebServicesManager $manager, string $endpoint, array
* to mimic HTTP request headers. The keys of the array are names of headers
* and the value of each key represents the value of the header.
*
- * @param UserInterface|null $user Optional user to authenticate the request with.
+ * @param SecurityPrincipal|null $user Optional user to authenticate the request with.
* to mimic HTTP request headers. The keys of the array are names of headers
* and the value of each key represents the value of the header.
*
* @return string The method will return the output that was produced by
* the endpoint as string.
*/
- public function putRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = [], ?UserInterface $user = null) : string {
+ public function putRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = [], ?SecurityPrincipal $user = null) : string {
return $this->callEndpoint($manager, RequestMethod::PUT, $endpoint, $parameters, $httpHeaders, $user);
}
/**
@@ -355,14 +355,14 @@ public function putRequest(WebServicesManager $manager, string $endpoint, array
* to mimic HTTP request headers. The keys of the array are names of headers
* and the value of each key represents the value of the header.
*
- * @param UserInterface|null $user Optional user to authenticate the request with.
+ * @param SecurityPrincipal|null $user Optional user to authenticate the request with.
* to mimic HTTP request headers. The keys of the array are names of headers
* and the value of each key represents the value of the header.
*
* @return string The method will return the output that was produced by
* the endpoint as string.
*/
- public function patchRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = [], ?UserInterface $user = null) : string {
+ public function patchRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = [], ?SecurityPrincipal $user = null) : string {
return $this->callEndpoint($manager, RequestMethod::PATCH, $endpoint, $parameters, $httpHeaders, $user);
}
/**
@@ -379,14 +379,14 @@ public function patchRequest(WebServicesManager $manager, string $endpoint, arra
* to mimic HTTP request headers. The keys of the array are names of headers
* and the value of each key represents the value of the header.
*
- * @param UserInterface|null $user Optional user to authenticate the request with.
+ * @param SecurityPrincipal|null $user Optional user to authenticate the request with.
* to mimic HTTP request headers. The keys of the array are names of headers
* and the value of each key represents the value of the header.
*
* @return string The method will return the output that was produced by
* the endpoint as string.
*/
- public function optionsRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = [], ?UserInterface $user = null) : string {
+ public function optionsRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = [], ?SecurityPrincipal $user = null) : string {
return $this->callEndpoint($manager, RequestMethod::OPTIONS, $endpoint, $parameters, $httpHeaders, $user);
}
/**
@@ -403,14 +403,14 @@ public function optionsRequest(WebServicesManager $manager, string $endpoint, ar
* to mimic HTTP request headers. The keys of the array are names of headers
* and the value of each key represents the value of the header.
*
- * @param UserInterface|null $user Optional user to authenticate the request with.
+ * @param SecurityPrincipal|null $user Optional user to authenticate the request with.
* to mimic HTTP request headers. The keys of the array are names of headers
* and the value of each key represents the value of the header.
*
* @return string The method will return the output that was produced by
* the endpoint as string.
*/
- public function headRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = [], ?UserInterface $user = null) : string {
+ public function headRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = [], ?SecurityPrincipal $user = null) : string {
return $this->callEndpoint($manager, RequestMethod::HEAD, $endpoint, $parameters, $httpHeaders, $user);
}
private function extractPathAndName($absPath): array {
diff --git a/WebFiori/Http/AbstractWebService.php b/WebFiori/Http/AbstractWebService.php
index 516be9a..997fd2f 100644
--- a/WebFiori/Http/AbstractWebService.php
+++ b/WebFiori/Http/AbstractWebService.php
@@ -1,4 +1,13 @@
getSettersMap() as $method => $paramName) {
if (is_callable([$instance, $method])) {
- try {
- if ($inputs instanceof Json) {
- $instance->$method($inputs->get($paramName));
- } else if (gettype($inputs) == 'array') {
- $instance->$method($inputs[$paramName]);
- }
- } catch (Throwable $ex) {
+ if ($inputs instanceof Json) {
+ $instance->$method($inputs->get($paramName));
+ } else if (gettype($inputs) == 'array') {
+ $instance->$method($inputs[$paramName]);
}
}
}
diff --git a/WebFiori/Http/OpenAPI/APIResponseDefinition.php b/WebFiori/Http/OpenAPI/APIResponseDefinition.php
index 6c28712..d50f712 100644
--- a/WebFiori/Http/OpenAPI/APIResponseDefinition.php
+++ b/WebFiori/Http/OpenAPI/APIResponseDefinition.php
@@ -1,4 +1,13 @@
name = $nameTrimmed;
+ // Check for reserved parameter names
+ if (in_array(strtolower($nameTrimmed), self::RESERVED_NAMES)) {
+ throw new \InvalidArgumentException("Parameter name '$nameTrimmed' is reserved and cannot be used. Reserved names: " . implode(', ', self::RESERVED_NAMES));
+ }
+
return true;
}
diff --git a/WebFiori/Http/RequestUri.php b/WebFiori/Http/RequestUri.php
index cacb70d..cd18310 100644
--- a/WebFiori/Http/RequestUri.php
+++ b/WebFiori/Http/RequestUri.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2025 WebFiori Framework
+ * Copyright (c) 2025-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/http/blob/master/LICENSE
diff --git a/WebFiori/Http/Response.php b/WebFiori/Http/Response.php
index 0977166..b2701a4 100644
--- a/WebFiori/Http/Response.php
+++ b/WebFiori/Http/Response.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2019 WebFiori Framework
+ * Copyright (c) 2019-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/http/blob/master/LICENSE
diff --git a/WebFiori/Http/ResponseMessage.php b/WebFiori/Http/ResponseMessage.php
index 333e151..ad2b2be 100644
--- a/WebFiori/Http/ResponseMessage.php
+++ b/WebFiori/Http/ResponseMessage.php
@@ -1,4 +1,13 @@
$targetMethod();
+ // Inject parameters into method call
+ $params = $this->getMethodParameters($targetMethod);
+ $result = $this->$targetMethod(...$params);
$this->handleMethodResponse($result, $targetMethod);
} catch (HttpException $e) {
// Handle HTTP exceptions automatically
@@ -332,23 +333,43 @@ public function checkMethodAuthorization(): bool {
$reflectionMethod = $reflection->getMethod($method);
+ // Check for conflicting annotations
+ $hasAllowAnonymous = !empty($reflectionMethod->getAttributes(\WebFiori\Http\Annotations\AllowAnonymous::class));
+ $hasRequiresAuth = !empty($reflectionMethod->getAttributes(\WebFiori\Http\Annotations\RequiresAuth::class));
+
+ if ($hasAllowAnonymous && $hasRequiresAuth) {
+ throw new \InvalidArgumentException(
+ "Method '$method' has conflicting annotations: #[AllowAnonymous] and #[RequiresAuth] cannot be used together"
+ );
+ }
+
// Check AllowAnonymous first
- if (!empty($reflectionMethod->getAttributes(\WebFiori\Http\Annotations\AllowAnonymous::class))) {
+ if ($hasAllowAnonymous) {
return true;
}
// Check RequiresAuth
- if (!empty($reflectionMethod->getAttributes(\WebFiori\Http\Annotations\RequiresAuth::class))) {
- if (!SecurityContext::isAuthenticated()) {
+ if ($hasRequiresAuth) {
+ // First call isAuthorized()
+ if (!$this->isAuthorized()) {
return false;
}
+
+ // Then check for PreAuthorize
+ $preAuthAttributes = $reflectionMethod->getAttributes(\WebFiori\Http\Annotations\PreAuthorize::class);
+ if (!empty($preAuthAttributes)) {
+ $preAuth = $preAuthAttributes[0]->newInstance();
+ return SecurityContext::evaluateExpression($preAuth->expression);
+ }
+
+ // If no PreAuthorize, continue based on isAuthorized (already passed)
+ return true;
}
- // Check PreAuthorize
+ // Check PreAuthorize without RequiresAuth
$preAuthAttributes = $reflectionMethod->getAttributes(\WebFiori\Http\Annotations\PreAuthorize::class);
if (!empty($preAuthAttributes)) {
$preAuth = $preAuthAttributes[0]->newInstance();
-
return SecurityContext::evaluateExpression($preAuth->expression);
}
@@ -439,6 +460,41 @@ private function methodHandlesHttpMethod(\ReflectionMethod $method, string $http
return false;
}
+ /**
+ * Get method parameters by extracting values from request.
+ *
+ * @param string $methodName The method name
+ * @return array Array of parameter values in correct order
+ */
+ private function getMethodParameters(string $methodName): array {
+ $reflection = new \ReflectionMethod($this, $methodName);
+ $params = [];
+
+ // Check for MapEntity attribute
+ $mapEntityAttrs = $reflection->getAttributes(\WebFiori\Http\Annotations\MapEntity::class);
+
+ if (!empty($mapEntityAttrs)) {
+ $mapEntity = $mapEntityAttrs[0]->newInstance();
+ $mappedObject = $this->getObject($mapEntity->entityClass, $mapEntity->setters);
+ $params[] = $mappedObject;
+ } else {
+ // Original parameter handling
+ foreach ($reflection->getParameters() as $param) {
+ $paramName = $param->getName();
+ $value = $this->getParamVal($paramName);
+
+ // Handle optional parameters with defaults
+ if ($value === null && $param->isDefaultValueAvailable()) {
+ $value = $param->getDefaultValue();
+ }
+
+ $params[] = $value;
+ }
+ }
+
+ return $params;
+ }
+
/**
* Handle method response by auto-converting return values to HTTP responses.
*
@@ -456,7 +512,15 @@ protected function handleMethodResponse(mixed $result, string $methodName): void
$responseBody = $responseBodyAttrs[0]->newInstance();
- // Auto-convert return value to response
+ // Handle custom content types
+ if ($responseBody->contentType !== 'application/json') {
+ // For non-JSON content types, send raw result
+ $content = is_string($result) ? $result : (is_array($result) || is_object($result) ? json_encode($result) : (string)$result);
+ $this->send($responseBody->contentType, $content, $responseBody->status);
+ return;
+ }
+
+ // Auto-convert return value to JSON response
if ($result === null) {
// Null return = empty response with configured status
$this->sendResponse('', $responseBody->status, $responseBody->type);
@@ -522,13 +586,19 @@ private function configureParametersFromMethod(\ReflectionMethod $method): void
foreach ($paramAttributes as $attribute) {
$param = $attribute->newInstance();
+ $options = [
+ \WebFiori\Http\ParamOption::TYPE => $this->mapParamType($param->type),
+ \WebFiori\Http\ParamOption::OPTIONAL => $param->optional,
+ \WebFiori\Http\ParamOption::DEFAULT => $param->default,
+ \WebFiori\Http\ParamOption::DESCRIPTION => $param->description
+ ];
+
+ if ($param->filter !== null) {
+ $options[\WebFiori\Http\ParamOption::FILTER] = $param->filter;
+ }
+
$this->addParameters([
- $param->name => [
- \WebFiori\Http\ParamOption::TYPE => $this->mapParamType($param->type),
- \WebFiori\Http\ParamOption::OPTIONAL => $param->optional,
- \WebFiori\Http\ParamOption::DEFAULT => $param->default,
- \WebFiori\Http\ParamOption::DESCRIPTION => $param->description
- ]
+ $param->name => $options
]);
}
}
@@ -617,6 +687,11 @@ public function addParameter($param) : bool {
}
if ($param instanceof RequestParameter && !$this->hasParameter($param->getName())) {
+ // Additional validation for reserved parameter names
+ if (in_array(strtolower($param->getName()), \WebFiori\Http\RequestParameter::RESERVED_NAMES)) {
+ throw new \InvalidArgumentException("Cannot add parameter '" . $param->getName() . "' to service '" . $this->getName() . "': parameter name is reserved. Reserved names: " . implode(', ', \WebFiori\Http\RequestParameter::RESERVED_NAMES));
+ }
+
$this->parameters[] = $param;
return true;
diff --git a/WebFiori/Http/WebServicesManager.php b/WebFiori/Http/WebServicesManager.php
index 1fe6c5f..c090446 100644
--- a/WebFiori/Http/WebServicesManager.php
+++ b/WebFiori/Http/WebServicesManager.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2019 WebFiori Framework
+ * Copyright (c) 2019-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/http/blob/master/LICENSE
@@ -160,6 +160,40 @@ public function getResponse() : Response {
public function addService(WebService $service) : WebServicesManager {
return $this->addAction($service);
}
+ /**
+ * Automatically discovers and registers web services from a directory.
+ *
+ * @param string $path The directory path to scan for service classes. Defaults to current directory.
+ *
+ * @return WebServicesManager Returns the same instance for method chaining.
+ */
+ public function autoDiscoverServices(string $path = null) : WebServicesManager {
+ if ($path === null) {
+ $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
+ $path = dirname($trace[0]['file']);
+ }
+
+ $files = glob($path . '/*.php');
+ $beforeClasses = get_declared_classes();
+
+ foreach ($files as $file) {
+ require_once $file;
+ }
+
+ $afterClasses = get_declared_classes();
+ $newClasses = array_diff($afterClasses, $beforeClasses);
+
+ foreach ($newClasses as $class) {
+ if (is_subclass_of($class, WebService::class)) {
+ $reflection = new \ReflectionClass($class);
+ if (!$reflection->isAbstract()) {
+ $this->addService(new $class());
+ }
+ }
+ }
+
+ return $this;
+ }
/**
* Sends a response message to indicate that request content type is
* not supported by the API.
@@ -393,7 +427,13 @@ public final function isContentTypeSupported() : bool {
$rm = $this->getRequest()->getMethod();
if ($c !== null && ($rm == RequestMethod::POST || $rm == RequestMethod::PUT)) {
- return in_array($c, self::POST_CONTENT_TYPES);
+ // Check if content type starts with any of the supported types
+ foreach (self::POST_CONTENT_TYPES as $supportedType) {
+ if (strpos($c, $supportedType) === 0) {
+ return true;
+ }
+ }
+ return false;
} else if ($c === null && ($rm == RequestMethod::POST || $rm == RequestMethod::PUT)) {
return false;
}
@@ -979,11 +1019,15 @@ private function filterInputsHelper() {
if ($reqMeth == RequestMethod::GET ||
$reqMeth == RequestMethod::DELETE ||
- ($reqMeth == RequestMethod::PUT && $contentType != 'application/json') ||
- $reqMeth == RequestMethod::HEAD ||
- $reqMeth == RequestMethod::PATCH) {
+ $reqMeth == RequestMethod::HEAD) {
$this->filter->filterGET();
- } else if ($reqMeth == RequestMethod::POST || ($reqMeth == RequestMethod::PUT && $contentType == 'application/json')) {
+ } else if ($reqMeth == RequestMethod::POST ||
+ $reqMeth == RequestMethod::PUT ||
+ $reqMeth == RequestMethod::PATCH) {
+ // Populate PUT/PATCH data before filtering
+ if ($reqMeth == RequestMethod::PUT || $reqMeth == RequestMethod::PATCH) {
+ $this->populatePutData($contentType);
+ }
$this->filter->filterPOST();
}
}
@@ -1001,6 +1045,11 @@ private function filterInputsHelper() {
* @deprecated since version 1.4.6 Use WebServicesManager::getCalledServiceName() instead.
*/
private function getAction() {
+ $services = $this->getServices();
+
+ if (count($services) == 1) {
+ return $services[array_keys($services)[0]]->getName();
+ }
$reqMeth = $this->getRequest()->getMethod();
$serviceIdx = ['action','service', 'service-name'];
@@ -1034,15 +1083,92 @@ private function getAction() {
$reqMeth == RequestMethod::TRACE ||
$reqMeth == RequestMethod::OPTIONS) && isset($_GET[$serviceNameIndex])) {
$retVal = filter_var($_GET[$serviceNameIndex]);
- } else if (($reqMeth == RequestMethod::POST ||
- $reqMeth == RequestMethod::PUT ||
- $reqMeth == RequestMethod::PATCH) && isset($_POST[$serviceNameIndex])) {
+ } else if ($reqMeth == RequestMethod::POST && isset($_POST[$serviceNameIndex])) {
$retVal = filter_var($_POST[$serviceNameIndex]);
+ } else if ($reqMeth == RequestMethod::PUT || $reqMeth == RequestMethod::PATCH) {
+ $this->populatePutData($contentType);
+ if (isset($_POST[$serviceNameIndex])) {
+ $retVal = filter_var($_POST[$serviceNameIndex]);
+ }
}
}
return $retVal;
}
+ private function populatePutData(string $contentType) {
+
+ $contentType = $_SERVER['CONTENT_TYPE'] ?? '';
+ $input = file_get_contents('php://input');
+
+ if (empty($input)) {
+ return;
+ }
+
+ // Handle application/x-www-form-urlencoded
+ if (strpos($contentType, 'application/x-www-form-urlencoded') === 0) {
+ parse_str($input, $_POST);
+ return;
+ }
+
+ // Handle multipart/form-data
+ if (strpos($contentType, 'multipart/form-data') === 0) {
+ // Extract boundary from content type
+ preg_match('/boundary=(.+)$/', $contentType, $matches);
+ if (!isset($matches[1])) {
+ return;
+ }
+
+ $boundary = '--' . $matches[1];
+ $parts = explode($boundary, $input);
+
+ foreach ($parts as $part) {
+ if (trim($part) === '' || trim($part) === '--') {
+ continue;
+ }
+
+ // Split headers and content
+ $sections = explode("\r\n\r\n", $part, 2);
+ if (count($sections) !== 2) {
+ continue;
+ }
+
+ $headers = $sections[0];
+ $content = rtrim($sections[1], "\r\n");
+
+ // Parse Content-Disposition header
+ if (preg_match('/name="([^"]+)"/', $headers, $nameMatch)) {
+ $fieldName = $nameMatch[1];
+
+ // Check if it's a file upload
+ if (preg_match('/filename="([^"]*)"/', $headers, $fileMatch)) {
+ // Handle file upload
+ $filename = $fileMatch[1];
+
+ // Extract content type if present
+ $fileType = 'application/octet-stream';
+ if (preg_match('/Content-Type:\s*(.+)/i', $headers, $typeMatch)) {
+ $fileType = trim($typeMatch[1]);
+ }
+
+ // Create temporary file
+ $tmpFile = tempnam(sys_get_temp_dir(), 'put_upload_');
+ file_put_contents($tmpFile, $content);
+
+ $_FILES[$fieldName] = [
+ 'name' => $filename,
+ 'type' => $fileType,
+ 'tmp_name' => $tmpFile,
+ 'error' => UPLOAD_ERR_OK,
+ 'size' => strlen($content)
+ ];
+ } else {
+ // Regular form field
+ $_POST[$fieldName] = $content;
+ }
+ }
+ }
+ }
+ }
private function isAuth(WebService $service) {
$isAuth = false;
diff --git a/examples/00-basic/01-hello-world/HelloService.php b/examples/00-basic/01-hello-world/HelloService.php
new file mode 100644
index 0000000..8ad02e5
--- /dev/null
+++ b/examples/00-basic/01-hello-world/HelloService.php
@@ -0,0 +1,23 @@
+setVersion('1.0.0');
+$manager->setDescription('Hello World API Example');
+
+// Auto-discover and register services
+$manager->autoDiscoverServices();
+
+// Process the incoming request
+$manager->process();
diff --git a/examples/00-basic/02-with-parameters/GreetingService.php b/examples/00-basic/02-with-parameters/GreetingService.php
new file mode 100644
index 0000000..0ca8698
--- /dev/null
+++ b/examples/00-basic/02-with-parameters/GreetingService.php
@@ -0,0 +1,29 @@
+setVersion('1.0.0');
+$manager->setDescription('Greeting API with Parameters');
+
+// Auto-discover and register services
+$manager->autoDiscoverServices();
+
+// Process the incoming request
+$manager->process();
diff --git a/examples/00-basic/03-multiple-methods/README.md b/examples/00-basic/03-multiple-methods/README.md
new file mode 100644
index 0000000..304e8d7
--- /dev/null
+++ b/examples/00-basic/03-multiple-methods/README.md
@@ -0,0 +1,81 @@
+# Multiple HTTP Methods
+
+Demonstrates handling different HTTP methods (GET, POST, PUT, DELETE) in a single service.
+
+## What This Example Demonstrates
+
+- Supporting multiple HTTP methods using separate mapping attributes
+- Method-specific parameters with `#[RequestParam]`
+- Different HTTP status codes per method (e.g., 201 for POST)
+- RESTful CRUD operations (Create, Read, Update, Delete)
+- Automatic service discovery
+
+## Files
+
+- [`TaskService.php`](TaskService.php) - Service supporting CRUD operations
+- [`index.php`](index.php) - Main application entry point
+
+## How to Run
+
+```bash
+php -S localhost:8080
+```
+
+## Testing
+
+```bash
+# GET - Retrieve tasks
+curl "http://localhost:8080"
+
+# POST - Create task
+curl -X POST -H "Content-Type: application/x-www-form-urlencoded" \
+ -d "title=Buy groceries&description=Get milk and bread" \
+ "http://localhost:8080"
+
+# PUT - Update task
+curl -X PUT -H "Content-Type: application/x-www-form-urlencoded" \
+ -d "id=1&title=Updated task" \
+ "http://localhost:8080"
+
+# DELETE - Delete task
+curl -X DELETE "http://localhost:8080?id=1"
+```
+
+**Expected Responses:**
+
+GET:
+```json
+{
+ "message": "Tasks retrieved",
+ "http-code": 200,
+ "data": {
+ "tasks": [],
+ "count": 0
+ }
+}
+```
+
+POST:
+```json
+{
+ "message": "Task created successfully",
+ "http-code": 201,
+ "data": {
+ "id": 1,
+ "title": "Buy groceries",
+ "description": "Get milk and bread"
+ }
+}
+```
+
+## Code Explanation
+
+- Multiple HTTP methods are supported using separate mapping attributes:
+ - `#[GetMapping]` for GET requests
+ - `#[PostMapping]` for POST requests
+ - `#[PutMapping]` for PUT requests
+ - `#[DeleteMapping]` for DELETE requests
+- Each method can have its own parameters defined with `#[RequestParam]`
+- **Parameters are automatically injected** into method arguments (e.g., `createTask(string $title, ?string $description)`)
+- `#[ResponseBody(status: 201)]` sets custom HTTP status code
+- Service is auto-discovered using `autoDiscoverServices()`
diff --git a/examples/00-basic/03-multiple-methods/TaskService.php b/examples/00-basic/03-multiple-methods/TaskService.php
new file mode 100644
index 0000000..db80826
--- /dev/null
+++ b/examples/00-basic/03-multiple-methods/TaskService.php
@@ -0,0 +1,79 @@
+ [],
+ 'count' => 0
+ ];
+ }
+
+ #[PostMapping]
+ #[ResponseBody(status: 201)]
+ #[AllowAnonymous]
+ #[RequestParam('title', 'string', false, null, 'Task title (1-100 chars)')]
+ #[RequestParam('description', 'string', true, null, 'Task description (max 500 chars)')]
+ public function createTask(string $title, ?string $description = null): array {
+ if (!$title) {
+ throw new \InvalidArgumentException('Title is required for creating a task');
+ }
+
+ return [
+ 'id' => 1,
+ 'title' => $title,
+ 'description' => $description ?: ''
+ ];
+ }
+
+ #[PutMapping]
+ #[ResponseBody]
+ #[AllowAnonymous]
+ #[RequestParam('id', 'int', false, null, 'Task ID')]
+ #[RequestParam('title', 'string', true, null, 'Updated task title')]
+ public function updateTask(int $id, ?string $title = null): array {
+ if (!$id) {
+ throw new \InvalidArgumentException('ID is required for updating a task');
+ }
+
+ return [
+ 'id' => $id,
+ 'title' => $title,
+ 'updated_at' => date('Y-m-d H:i:s')
+ ];
+ }
+
+ #[DeleteMapping]
+ #[ResponseBody]
+ #[AllowAnonymous]
+ #[RequestParam('id', 'int', false, null, 'Task ID to delete')]
+ public function deleteTask(int $id): array {
+ if (!$id) {
+ throw new \InvalidArgumentException('ID is required for deleting a task');
+ }
+
+ return [
+ 'id' => $id,
+ 'deleted_at' => date('Y-m-d H:i:s')
+ ];
+ }
+}
diff --git a/examples/00-basic/03-multiple-methods/index.php b/examples/00-basic/03-multiple-methods/index.php
new file mode 100644
index 0000000..a1ae0c0
--- /dev/null
+++ b/examples/00-basic/03-multiple-methods/index.php
@@ -0,0 +1,16 @@
+setVersion('1.0.0');
+$manager->setDescription('Task Management API');
+
+// Auto-discover and register services
+$manager->autoDiscoverServices();
+
+// Process the incoming request
+$manager->process();
diff --git a/examples/00-basic/04-simple-manager/InfoService.php b/examples/00-basic/04-simple-manager/InfoService.php
new file mode 100644
index 0000000..1744acb
--- /dev/null
+++ b/examples/00-basic/04-simple-manager/InfoService.php
@@ -0,0 +1,33 @@
+ 'Multi-Service API',
+ 'version' => '1.0.0',
+ 'description' => 'Demonstration of multiple services in one manager',
+ 'services' => ['info', 'users', 'products'],
+ 'endpoints' => [
+ 'GET /info' => 'Get API information',
+ 'GET /users' => 'Get all users',
+ 'GET /products' => 'Get all products'
+ ]
+ ];
+ }
+}
diff --git a/examples/00-basic/04-simple-manager/ProductService.php b/examples/00-basic/04-simple-manager/ProductService.php
new file mode 100644
index 0000000..520272f
--- /dev/null
+++ b/examples/00-basic/04-simple-manager/ProductService.php
@@ -0,0 +1,28 @@
+ [
+ ['id' => 1, 'name' => 'Laptop', 'price' => 999.99],
+ ['id' => 2, 'name' => 'Mouse', 'price' => 29.99]
+ ]
+ ];
+ }
+}
diff --git a/examples/00-basic/04-simple-manager/README.md b/examples/00-basic/04-simple-manager/README.md
new file mode 100644
index 0000000..5c38a77
--- /dev/null
+++ b/examples/00-basic/04-simple-manager/README.md
@@ -0,0 +1,57 @@
+# Simple Service Manager
+
+Demonstrates managing multiple services with a single WebServicesManager instance.
+
+## What This Example Demonstrates
+
+- Auto-discovering and registering multiple services
+- Service routing with `?service=` parameter
+- Manager configuration (version, description)
+- Multiple independent services in one application
+
+## Files
+
+- [`UserService.php`](UserService.php) - User management service
+- [`ProductService.php`](ProductService.php) - Product management service
+- [`InfoService.php`](InfoService.php) - API information service
+- [`index.php`](index.php) - Main application with multiple services
+
+## How to Run
+
+```bash
+php -S localhost:8080
+```
+
+## Testing
+
+```bash
+# Get API information
+curl "http://localhost:8080?service=info"
+
+# User service
+curl "http://localhost:8080?service=users"
+
+# Product service
+curl "http://localhost:8080?service=products"
+```
+
+**Expected Responses:**
+
+API Info:
+```json
+{
+ "message": "Success",
+ "type": "success",
+ "http-code": 200,
+ "more-info": [...]
+}
+```
+
+## Code Explanation
+
+- Multiple services are auto-discovered and registered with `autoDiscoverServices()`
+- Each service has its own unique name and functionality
+- The manager automatically routes requests to the appropriate service:
+ - **GET/DELETE**: `?service=` query parameter
+ - **POST/PUT/PATCH**: `service` in request body (form data or JSON attribute)
+- When multiple services exist, the `service` parameter is required for routing
diff --git a/examples/00-basic/04-simple-manager/UserService.php b/examples/00-basic/04-simple-manager/UserService.php
new file mode 100644
index 0000000..87ad758
--- /dev/null
+++ b/examples/00-basic/04-simple-manager/UserService.php
@@ -0,0 +1,28 @@
+ [
+ ['id' => 1, 'name' => 'John Doe'],
+ ['id' => 2, 'name' => 'Jane Smith']
+ ]
+ ];
+ }
+}
diff --git a/examples/00-basic/04-simple-manager/index.php b/examples/00-basic/04-simple-manager/index.php
new file mode 100644
index 0000000..4121220
--- /dev/null
+++ b/examples/00-basic/04-simple-manager/index.php
@@ -0,0 +1,16 @@
+setVersion('1.0.0');
+$manager->setDescription('Multi-Service API Example');
+
+// Auto-discover and register services
+$manager->autoDiscoverServices();
+
+// Process the incoming request
+$manager->process();
diff --git a/examples/00-basic/README.md b/examples/00-basic/README.md
new file mode 100644
index 0000000..d0bf3df
--- /dev/null
+++ b/examples/00-basic/README.md
@@ -0,0 +1,29 @@
+# Basic Examples
+
+This folder contains the most fundamental examples to get started with WebFiori HTTP library.
+
+## Examples
+
+1. **[01-hello-world](01-hello-world/)** - Minimal service implementation
+2. **[02-with-parameters](02-with-parameters/)** - Basic parameter handling
+3. **[03-multiple-methods](03-multiple-methods/)** - Supporting different HTTP methods
+4. **[04-simple-manager](04-simple-manager/)** - Basic service manager setup
+
+## Prerequisites
+
+- PHP 8.1+
+- WebFiori HTTP library installed via Composer
+
+## Running Examples
+
+Each example can be run independently:
+
+```bash
+cd 01-hello-world
+php -S localhost:8080
+```
+
+Then test with:
+```bash
+curl "http://localhost:8080?service=hello"
+```
diff --git a/examples/01-core/01-parameter-validation/README.md b/examples/01-core/01-parameter-validation/README.md
new file mode 100644
index 0000000..8fcba0d
--- /dev/null
+++ b/examples/01-core/01-parameter-validation/README.md
@@ -0,0 +1,62 @@
+# Parameter Validation
+
+Demonstrates comprehensive parameter validation including types, ranges, custom filters, and validation rules.
+
+## What This Example Demonstrates
+
+- Different parameter types (string, int, email, URL)
+- Validation rules (min/max length, ranges)
+- **Custom validation filters using the `filter` parameter**
+- Default values for optional parameters
+- Validation error handling
+- Automatic parameter injection
+
+## Files
+
+- [`ValidationService.php`](ValidationService.php) - Service with comprehensive validation
+- [`index.php`](index.php) - Main application entry point
+
+## How to Run
+
+```bash
+php -S localhost:8080
+```
+
+## Testing
+
+```bash
+# Valid request
+curl "http://localhost:8080?name=John&age=25&email=john@example.com&website=https://example.com"
+
+# Valid with custom username validation
+curl "http://localhost:8080?name=John&email=john@test.com&username=john123"
+
+# Invalid username (too short)
+curl "http://localhost:8080?name=John&email=john@test.com&username=ab"
+
+# Invalid username (special characters)
+curl "http://localhost:8080?name=John&email=john@test.com&username=john@123"
+
+# Invalid email format
+curl "http://localhost:8080?name=John&age=25&email=invalid-email"
+
+# Missing required parameter
+curl "http://localhost:8080?age=25"
+```
+
+## Code Explanation
+
+- Different parameter types are defined using `#[RequestParam]` attributes
+- Parameters are automatically injected into method arguments with proper types
+- **Custom validation** can be added using the `filter` parameter:
+ ```php
+ #[RequestParam('username', 'string', true, null, 'Description', filter: [ClassName::class, 'methodName'])]
+ ```
+- Validation happens automatically before method execution:
+ - Type validation (string, int, email, url, double)
+ - Range validation (min/max for numbers)
+ - Length validation (min/max for strings)
+ - Format validation (email, URL)
+ - Custom validation via filter functions
+- Invalid parameters automatically return 400 errors with details
+- Service is auto-discovered using `autoDiscoverServices()`
diff --git a/examples/01-core/01-parameter-validation/ValidationService.php b/examples/01-core/01-parameter-validation/ValidationService.php
new file mode 100644
index 0000000..46af84f
--- /dev/null
+++ b/examples/01-core/01-parameter-validation/ValidationService.php
@@ -0,0 +1,63 @@
+ [
+ 'name' => $name,
+ 'age' => $age,
+ 'email' => $email,
+ 'website' => $website,
+ 'score' => $score,
+ 'username' => $username
+ ],
+ 'validation_info' => [
+ 'name_length' => strlen($name),
+ 'age_valid' => $age >= 18 && $age <= 120,
+ 'email_domain' => substr(strrchr($email, '@'), 1),
+ 'website_protocol' => $website ? parse_url($website, PHP_URL_SCHEME) : null,
+ 'username_valid' => $username ? ctype_alnum($username) : null
+ ]
+ ];
+ }
+
+ /**
+ * Custom validation function for username
+ */
+ public static function validateUsername($original, $filtered, $param) {
+ // Must be alphanumeric and 3-20 characters
+ if (strlen($filtered) < 3 || strlen($filtered) > 20) {
+ return APIFilter::INVALID;
+ }
+
+ if (!ctype_alnum($filtered)) {
+ return APIFilter::INVALID;
+ }
+
+ return strtolower($filtered); // Normalize to lowercase
+ }
+}
diff --git a/examples/01-core/01-parameter-validation/index.php b/examples/01-core/01-parameter-validation/index.php
new file mode 100644
index 0000000..9a55138
--- /dev/null
+++ b/examples/01-core/01-parameter-validation/index.php
@@ -0,0 +1,16 @@
+setVersion('1.0.0');
+$manager->setDescription('Parameter Validation API');
+
+// Auto-discover and register services
+$manager->autoDiscoverServices();
+
+// Process the incoming request
+$manager->process();
diff --git a/examples/01-core/02-error-handling/ErrorService.php b/examples/01-core/02-error-handling/ErrorService.php
new file mode 100644
index 0000000..1817daf
--- /dev/null
+++ b/examples/01-core/02-error-handling/ErrorService.php
@@ -0,0 +1,77 @@
+handleSuccess();
+ case 'validate':
+ return $this->handleValidation($age);
+ case 'divide':
+ return $this->handleDivision($a, $b);
+ case 'not-found':
+ throw new \Exception('The requested resource was not found', 404);
+ case 'server-error':
+ throw new \Exception('Simulated server error for testing purposes', 500);
+ case 'unauthorized':
+ throw new \Exception('Access denied: insufficient permissions', 403);
+ default:
+ throw new \InvalidArgumentException("Unknown operation: $operation. Available: success, validate, divide, not-found, server-error, unauthorized");
+ }
+ }
+
+ private function handleSuccess(): array {
+ return [
+ 'timestamp' => date('Y-m-d H:i:s'),
+ 'status' => 'OK'
+ ];
+ }
+
+ private function handleValidation(?int $age): array {
+ if ($age === null) {
+ throw new \InvalidArgumentException('Age parameter is required for validation test');
+ }
+
+ if ($age < 18) {
+ throw new \InvalidArgumentException("Age must be 18 or older. Provided: $age");
+ }
+
+ return ['validated_age' => $age];
+ }
+
+ private function handleDivision(?float $a, ?float $b): array {
+ if ($a === null || $b === null) {
+ throw new \InvalidArgumentException('Both parameters a and b are required for division');
+ }
+
+ if ($b == 0) {
+ throw new \InvalidArgumentException('Division by zero is not allowed');
+ }
+
+ return [
+ 'operands' => ['a' => $a, 'b' => $b],
+ 'result' => $a / $b
+ ];
+ }
+}
diff --git a/examples/01-core/02-error-handling/README.md b/examples/01-core/02-error-handling/README.md
new file mode 100644
index 0000000..1535bff
--- /dev/null
+++ b/examples/01-core/02-error-handling/README.md
@@ -0,0 +1,49 @@
+# Error Handling
+
+Demonstrates custom error responses, validation error handling, and proper HTTP status codes.
+
+## What This Example Demonstrates
+
+- Custom error messages and status codes
+- Validation error handling
+- Exception handling in services
+- Structured error responses
+
+## Files
+
+- [`ErrorService.php`](ErrorService.php) - Service with comprehensive error handling
+- [`index.php`](index.php) - Main application entry point
+
+## How to Run
+
+```bash
+php -S localhost:8080
+```
+
+## Testing
+
+```bash
+# Valid request
+curl "http://localhost:8080?operation=success"
+
+# Validation error
+curl "http://localhost:8080?operation=validate&age=15"
+
+# Business logic error
+curl "http://localhost:8080?operation=divide&a=10&b=0"
+
+# Not found error
+curl "http://localhost:8080?operation=not-found"
+
+# Server error simulation
+curl "http://localhost:8080?operation=server-error"
+```
+
+## Code Explanation
+
+- Custom error responses with appropriate HTTP status codes
+- Exceptions are automatically caught and converted to error responses
+- Parameters are automatically injected into method arguments
+- Business logic errors handled with try-catch blocks or thrown exceptions
+- Consistent error response format across all error types
+- Service is auto-discovered using `autoDiscoverServices()`
diff --git a/examples/01-core/02-error-handling/index.php b/examples/01-core/02-error-handling/index.php
new file mode 100644
index 0000000..b419819
--- /dev/null
+++ b/examples/01-core/02-error-handling/index.php
@@ -0,0 +1,16 @@
+setVersion('1.0.0');
+$manager->setDescription('Error Handling API');
+
+// Auto-discover and register services
+$manager->autoDiscoverServices();
+
+// Process the incoming request
+$manager->process();
diff --git a/examples/01-core/03-json-requests/JsonService.php b/examples/01-core/03-json-requests/JsonService.php
new file mode 100644
index 0000000..2c7600b
--- /dev/null
+++ b/examples/01-core/03-json-requests/JsonService.php
@@ -0,0 +1,109 @@
+processJsonRequest($operation);
+ }
+
+ #[PutMapping]
+ #[ResponseBody]
+ #[AllowAnonymous]
+ #[RequestParam('operation', 'string', true, 'update', 'Operation to perform')]
+ public function processJsonPut(?string $operation = 'update'): array {
+ return $this->processJsonRequest($operation);
+ }
+
+ private function processJsonRequest(string $operation): array {
+ $inputs = $this->getInputs();
+
+ // Check if we received JSON data
+ if (!($inputs instanceof Json)) {
+ throw new \InvalidArgumentException('This service expects JSON data');
+ }
+
+ switch ($operation) {
+ case 'create':
+ return $this->handleCreate($inputs);
+ case 'update':
+ return $this->handleUpdate($inputs);
+ case 'validate':
+ return $this->handleValidate($inputs);
+ default:
+ return $this->handleGeneric($inputs, $operation);
+ }
+ }
+
+ private function handleCreate(Json $jsonData): array {
+ $user = $jsonData->get('user');
+ $preferences = $jsonData->get('preferences');
+
+ return [
+ 'operation' => 'create',
+ 'user_data' => $user,
+ 'preferences' => $preferences,
+ 'created_at' => date('Y-m-d H:i:s')
+ ];
+ }
+
+ private function handleUpdate(Json $jsonData): array {
+ $name = $jsonData->get('name');
+ $email = $jsonData->get('email');
+
+ return [
+ 'operation' => 'update',
+ 'updated_fields' => [
+ 'name' => $name,
+ 'email' => $email
+ ],
+ 'updated_at' => date('Y-m-d H:i:s')
+ ];
+ }
+
+ private function handleValidate(Json $jsonData): array {
+ $errors = [];
+
+ // Validate required fields
+ if (!$jsonData->hasKey('name') || empty($jsonData->get('name'))) {
+ $errors[] = 'Name is required';
+ }
+
+ if (!$jsonData->hasKey('email') || !filter_var($jsonData->get('email'), FILTER_VALIDATE_EMAIL)) {
+ $errors[] = 'Valid email is required';
+ }
+
+ if (!empty($errors)) {
+ throw new \InvalidArgumentException('Validation failed: ' . implode(', ', $errors));
+ }
+
+ return ['validated_data' => $jsonData->toArray()];
+ }
+
+ private function handleGeneric(Json $jsonData, string $operation): array {
+ return [
+ 'operation' => $operation,
+ 'received_json' => $jsonData->toArray(),
+ 'json_keys' => array_keys($jsonData->toArray()),
+ 'processed_at' => date('Y-m-d H:i:s')
+ ];
+ }
+}
diff --git a/examples/01-core/03-json-requests/README.md b/examples/01-core/03-json-requests/README.md
new file mode 100644
index 0000000..61e37c1
--- /dev/null
+++ b/examples/01-core/03-json-requests/README.md
@@ -0,0 +1,62 @@
+# JSON Request Handling
+
+Demonstrates handling JSON request bodies and automatic parsing.
+
+## What This Example Demonstrates
+
+- Processing JSON request bodies
+- Content-Type: application/json handling
+- Accessing JSON data in services
+- Mixed parameter sources (URL + JSON body)
+
+## Files
+
+- [`JsonService.php`](JsonService.php) - Service that processes JSON requests
+- [`index.php`](index.php) - Main application entry point
+
+## How to Run
+
+```bash
+php -S localhost:8080
+```
+
+## Testing
+
+```bash
+# JSON POST request
+curl -X POST "http://localhost:8080" \
+ -H "Content-Type: application/json" \
+ -d '{"user": {"name": "John", "age": 30}, "preferences": {"theme": "dark", "notifications": true}}'
+
+# JSON with operation parameter
+curl -X POST "http://localhost:8080?operation=update" \
+ -H "Content-Type: application/json" \
+ -d '{"name": "Updated Name", "email": "new@example.com"}'
+
+# JSON PUT request
+curl -X PUT "http://localhost:8080" \
+ -H "Content-Type: application/json" \
+ -d '{"name": "Updated", "email": "updated@example.com"}'
+```
+
+**Expected Response:**
+```json
+{
+ "message": "JSON data processed successfully",
+ "http-code": 200,
+ "data": {
+ "received_json": {...},
+ "url_params": {...},
+ "processed_at": "2024-01-01 12:00:00"
+ }
+}
+```
+
+## Code Explanation
+
+- JSON requests are automatically parsed when Content-Type is application/json
+- `getInputs()` returns the parsed JSON object
+- Parameters can be injected into method arguments
+- Different methods handle POST and PUT requests separately
+- Invalid JSON automatically returns 400 Bad Request
+- Service is auto-discovered using `autoDiscoverServices()`
diff --git a/examples/01-core/03-json-requests/index.php b/examples/01-core/03-json-requests/index.php
new file mode 100644
index 0000000..3dadb31
--- /dev/null
+++ b/examples/01-core/03-json-requests/index.php
@@ -0,0 +1,16 @@
+setVersion('1.0.0');
+$manager->setDescription('JSON Request Handling API');
+
+// Auto-discover and register services
+$manager->autoDiscoverServices();
+
+// Process the incoming request
+$manager->process();
diff --git a/examples/01-core/04-file-uploads/README.md b/examples/01-core/04-file-uploads/README.md
new file mode 100644
index 0000000..e7c7dff
--- /dev/null
+++ b/examples/01-core/04-file-uploads/README.md
@@ -0,0 +1,60 @@
+# File Upload Handling
+
+Demonstrates handling file uploads with multipart/form-data requests.
+
+## What This Example Demonstrates
+
+- File upload handling with $_FILES
+- File validation (size, type, extension)
+- Multiple file uploads
+- File storage and metadata
+
+## Files
+
+- [`UploadService.php`](UploadService.php) - File upload handling service
+- [`index.php`](index.php) - Main application entry point
+
+## How to Run
+
+```bash
+php -S localhost:8080
+```
+
+## Testing
+
+```bash
+# Single file upload
+curl -X POST "http://localhost:8080" \
+ -F "file=@/path/to/your/file.txt" \
+ -F "description=Test file upload"
+
+# Upload with metadata
+curl -X POST "http://localhost:8080?operation=with-metadata" \
+ -F "file=@/path/to/document.pdf" \
+ -F "title=Important Document" \
+ -F "category=documents"
+```
+
+**Expected Response:**
+```json
+{
+ "message": "File uploaded successfully",
+ "http-code": 200,
+ "data": {
+ "filename": "file.txt",
+ "size": 1024,
+ "type": "text/plain",
+ "upload_path": "uploads/file_20240101_120000.txt"
+ }
+}
+```
+
+## Code Explanation
+
+- File uploads are handled through $_FILES superglobal
+- Parameters are automatically injected into method arguments
+- File validation includes size, type, and extension checks
+- Files are moved to a secure upload directory
+- Metadata is stored alongside file information
+- Content type validation now properly handles multipart/form-data with boundary parameters
+- Service is auto-discovered using `autoDiscoverServices()`
diff --git a/examples/01-core/04-file-uploads/UploadService.php b/examples/01-core/04-file-uploads/UploadService.php
new file mode 100644
index 0000000..cd25400
--- /dev/null
+++ b/examples/01-core/04-file-uploads/UploadService.php
@@ -0,0 +1,192 @@
+handleSingleUpload($description);
+ case 'multiple':
+ return $this->handleMultipleUpload();
+ case 'with-metadata':
+ return $this->handleUploadWithMetadata($title, $category);
+ default:
+ throw new \InvalidArgumentException('Unknown upload operation');
+ }
+ }
+
+ private function handleSingleUpload(?string $description): array {
+ if (!isset($_FILES['file'])) {
+ throw new \InvalidArgumentException('No file uploaded. Expected field: file');
+ }
+
+ $file = $_FILES['file'];
+
+ $result = $this->processFile($file);
+
+ if (!$result['success']) {
+ throw new \InvalidArgumentException($result['error']);
+ }
+
+ return [
+ 'file_info' => $result['data'],
+ 'description' => $description,
+ 'uploaded_at' => date('Y-m-d H:i:s')
+ ];
+ }
+
+ private function handleMultipleUpload(): array {
+ if (!isset($_FILES['files'])) {
+ throw new \InvalidArgumentException('No files uploaded. Expected field: files[]');
+ }
+
+ $files = $_FILES['files'];
+ $results = [];
+ $errors = [];
+
+ for ($i = 0; $i < count($files['name']); $i++) {
+ $file = [
+ 'name' => $files['name'][$i],
+ 'type' => $files['type'][$i],
+ 'tmp_name' => $files['tmp_name'][$i],
+ 'error' => $files['error'][$i],
+ 'size' => $files['size'][$i]
+ ];
+
+ $result = $this->processFile($file);
+
+ if ($result['success']) {
+ $results[] = $result['data'];
+ } else {
+ $errors[] = [
+ 'filename' => $file['name'],
+ 'error' => $result['error']
+ ];
+ }
+ }
+
+ return [
+ 'uploaded_files' => $results,
+ 'errors' => $errors,
+ 'total_uploaded' => count($results),
+ 'total_errors' => count($errors)
+ ];
+ }
+
+ private function handleUploadWithMetadata(?string $title, ?string $category): array {
+ if (!isset($_FILES['file'])) {
+ throw new \InvalidArgumentException('No file uploaded');
+ }
+
+ $file = $_FILES['file'];
+
+ $result = $this->processFile($file);
+
+ if (!$result['success']) {
+ throw new \InvalidArgumentException($result['error']);
+ }
+
+ return [
+ 'title' => $title ?: $file['name'],
+ 'category' => $category ?: 'general',
+ 'uploaded_by' => 'anonymous',
+ 'upload_date' => date('Y-m-d H:i:s'),
+ 'file_info' => $result['data']
+ ];
+ }
+
+ private function processFile(array $file): array {
+ // Check for upload errors
+ if ($file['error'] !== UPLOAD_ERR_OK) {
+ return [
+ 'success' => false,
+ 'error' => 'File upload error',
+ 'details' => ['upload_error_code' => $file['error']]
+ ];
+ }
+
+ // Validate file size
+ if ($file['size'] > self::MAX_FILE_SIZE) {
+ return [
+ 'success' => false,
+ 'error' => 'File too large',
+ 'details' => [
+ 'max_size' => self::MAX_FILE_SIZE,
+ 'file_size' => $file['size']
+ ]
+ ];
+ }
+
+ // Validate file type
+ if (!in_array($file['type'], self::ALLOWED_TYPES)) {
+ return [
+ 'success' => false,
+ 'error' => 'File type not allowed',
+ 'details' => [
+ 'allowed_types' => self::ALLOWED_TYPES,
+ 'file_type' => $file['type']
+ ]
+ ];
+ }
+
+ // Generate unique filename
+ $extension = pathinfo($file['name'], PATHINFO_EXTENSION);
+ $filename = pathinfo($file['name'], PATHINFO_FILENAME);
+ $uniqueFilename = $filename . '_' . date('Ymd_His') . '.' . $extension;
+ $uploadPath = self::UPLOAD_DIR . $uniqueFilename;
+
+ // Move uploaded file
+ if (move_uploaded_file($file['tmp_name'], $uploadPath)) {
+ return [
+ 'success' => true,
+ 'data' => [
+ 'original_name' => $file['name'],
+ 'stored_name' => $uniqueFilename,
+ 'size' => $file['size'],
+ 'type' => $file['type'],
+ 'upload_path' => $uploadPath,
+ 'url' => '/' . $uploadPath
+ ]
+ ];
+ } else {
+ return [
+ 'success' => false,
+ 'error' => 'Failed to save file',
+ 'details' => ['upload_path' => $uploadPath]
+ ];
+ }
+ }
+}
diff --git a/examples/01-core/04-file-uploads/index.php b/examples/01-core/04-file-uploads/index.php
new file mode 100644
index 0000000..fb4fc16
--- /dev/null
+++ b/examples/01-core/04-file-uploads/index.php
@@ -0,0 +1,16 @@
+setVersion('1.0.0');
+$manager->setDescription('File Upload API');
+
+// Auto-discover and register services
+$manager->autoDiscoverServices();
+
+// Process the incoming request
+$manager->process();
diff --git a/examples/01-core/05-response-formats/JsonResponseService.php b/examples/01-core/05-response-formats/JsonResponseService.php
new file mode 100644
index 0000000..a9dad77
--- /dev/null
+++ b/examples/01-core/05-response-formats/JsonResponseService.php
@@ -0,0 +1,28 @@
+ 'Hello from WebFiori HTTP',
+ 'timestamp' => date('Y-m-d H:i:s'),
+ 'format' => 'json',
+ 'server_info' => [
+ 'php_version' => PHP_VERSION,
+ 'server_time' => time()
+ ]
+ ];
+ }
+}
diff --git a/examples/01-core/05-response-formats/README.md b/examples/01-core/05-response-formats/README.md
new file mode 100644
index 0000000..ad38df4
--- /dev/null
+++ b/examples/01-core/05-response-formats/README.md
@@ -0,0 +1,65 @@
+# Response Formats
+
+Demonstrates different response content types and formats beyond JSON.
+
+## What This Example Demonstrates
+
+- Different content types using `#[ResponseBody(contentType: '...')]`
+- Separate services for each format (cleaner architecture)
+- Unified service with format switching (single endpoint)
+- Custom response serialization (XML, Text)
+- Automatic content type handling
+
+## Files
+
+- [`ResponseService.php`](ResponseService.php) - Original unified service (demonstrates format switching)
+- [`JsonResponseService.php`](JsonResponseService.php) - JSON response service
+- [`XmlResponseService.php`](XmlResponseService.php) - XML response service
+- [`TextResponseService.php`](TextResponseService.php) - Plain text response service
+- [`index.php`](index.php) - Main application entry point
+
+## How to Run
+
+```bash
+php -S localhost:8080
+```
+
+## Testing
+
+```bash
+# JSON response
+curl "http://localhost:8080?service=json-response"
+
+# XML response
+curl "http://localhost:8080?service=xml-response"
+
+# Plain text response
+curl "http://localhost:8080?service=text-response"
+
+# Original unified service with format parameter
+curl "http://localhost:8080?service=response&format=json"
+curl "http://localhost:8080?service=response&format=xml"
+curl "http://localhost:8080?service=response&format=text"
+```
+
+## Code Explanation
+
+**Two approaches demonstrated:**
+
+1. **Separate Services** (Recommended for clean architecture):
+ - `JsonResponseService` - Returns JSON with default content type
+ - `XmlResponseService` - Uses `#[ResponseBody(contentType: 'application/xml')]`
+ - `TextResponseService` - Uses `#[ResponseBody(contentType: 'text/plain')]`
+ - Each service is focused and simple
+ - Auto-discovered and registered automatically
+
+2. **Unified Service** (Single endpoint with format switching):
+ - `ResponseService` - Handles multiple formats via parameter
+ - Uses `send()` method for custom content types
+ - More complex but provides single endpoint
+
+**Benefits of separate services:**
+- Cleaner, more maintainable code
+- Each service has single responsibility
+- Easier to test and modify
+- Better separation of concerns
diff --git a/examples/01-core/05-response-formats/ResponseService.php b/examples/01-core/05-response-formats/ResponseService.php
new file mode 100644
index 0000000..c32123a
--- /dev/null
+++ b/examples/01-core/05-response-formats/ResponseService.php
@@ -0,0 +1,214 @@
+getSampleData($data, $format);
+
+ // Dynamically set content type based on format
+ $reflection = new \ReflectionMethod($this, 'handleResponse');
+ $attrs = $reflection->getAttributes(\WebFiori\Http\Annotations\ResponseBody::class);
+
+ switch ($format) {
+ case 'xml':
+ // Override response to XML
+ $xml = $this->arrayToXml($sampleData, 'response');
+ $this->send('application/xml', $xml, 200);
+ return null;
+ case 'text':
+ // Override response to text
+ $text = $this->arrayToText($sampleData);
+ $this->send('text/plain', $text, 200);
+ return null;
+ default:
+ // Return array for JSON (handled by ResponseBody)
+ return $sampleData;
+ }
+ }
+
+ private function getSampleData(string $type, string $format): array {
+ switch ($type) {
+ case 'users':
+ return [
+ ['id' => 1, 'name' => 'John Doe', 'email' => 'john@example.com'],
+ ['id' => 2, 'name' => 'Jane Smith', 'email' => 'jane@example.com'],
+ ['id' => 3, 'name' => 'Bob Johnson', 'email' => 'bob@example.com']
+ ];
+ case 'products':
+ return [
+ ['id' => 1, 'name' => 'Laptop', 'price' => 999.99, 'category' => 'Electronics'],
+ ['id' => 2, 'name' => 'Book', 'price' => 19.99, 'category' => 'Education'],
+ ['id' => 3, 'name' => 'Coffee Mug', 'price' => 12.50, 'category' => 'Kitchen']
+ ];
+ default:
+ return [
+ 'message' => 'Hello from WebFiori HTTP',
+ 'timestamp' => date('Y-m-d H:i:s'),
+ 'format_requested' => $format,
+ 'server_info' => [
+ 'php_version' => PHP_VERSION,
+ 'server_time' => time()
+ ]
+ ];
+ }
+ }
+
+ private function arrayToXml(array $data, string $rootElement = 'root'): string {
+ $xml = "\n";
+ $xml .= "<$rootElement>\n";
+ $xml .= $this->arrayToXmlRecursive($data, 1);
+ $xml .= "$rootElement>";
+ return $xml;
+ }
+
+ private function arrayToXmlRecursive(array $data, int $indent = 0): string {
+ $xml = '';
+ $spaces = str_repeat(' ', $indent);
+
+ foreach ($data as $key => $value) {
+ $key = is_numeric($key) ? 'item' : $key;
+
+ if (is_array($value)) {
+ $xml .= "$spaces<$key>\n";
+ $xml .= $this->arrayToXmlRecursive($value, $indent + 1);
+ $xml .= "$spaces$key>\n";
+ } else {
+ $xml .= "$spaces<$key>" . htmlspecialchars($value) . "$key>\n";
+ }
+ }
+
+ return $xml;
+ }
+
+ private function arrayToText(array $data): string {
+ return $this->arrayToTextRecursive($data, 0);
+ }
+
+ private function arrayToTextRecursive(array $data, int $indent = 0): string {
+ $text = '';
+ $spaces = str_repeat(' ', $indent);
+
+ foreach ($data as $key => $value) {
+ if (is_array($value)) {
+ $text .= "$spaces$key:\n";
+ $text .= $this->arrayToTextRecursive($value, $indent + 1);
+ } else {
+ $text .= "$spaces$key: $value\n";
+ }
+ }
+
+ return $text;
+ }
+
+ private function arrayToHtml(array $data): string {
+ $html = "\n\n
\n";
+ $html .= "WebFiori HTTP Response\n";
+ $html .= "\n";
+ $html .= "\n\n";
+ $html .= "WebFiori HTTP Response
\n";
+ $html .= "Generated: " . date('Y-m-d H:i:s') . "
\n";
+ $html .= $this->arrayToHtmlTable($data);
+ $html .= "\n";
+ return $html;
+ }
+
+ private function arrayToHtmlTable(array $data): string {
+ if (empty($data)) {
+ return 'No data available
';
+ }
+
+ // Check if it's a list of objects/arrays
+ if (is_array($data[0] ?? null)) {
+ return $this->createHtmlTable($data);
+ }
+
+ // Simple key-value pairs
+ $html = "\n";
+ foreach ($data as $key => $value) {
+ $html .= "| " . htmlspecialchars($key) . " | ";
+ $html .= "" . htmlspecialchars(is_array($value) ? json_encode($value) : $value) . " |
\n";
+ }
+ $html .= "
\n";
+
+ return $html;
+ }
+
+ private function createHtmlTable(array $data): string {
+ if (empty($data)) {
+ return 'No data available
';
+ }
+
+ $html = "\n\n";
+
+ // Get headers from first row
+ $headers = array_keys($data[0]);
+ foreach ($headers as $header) {
+ $html .= "| " . htmlspecialchars($header) . " | ";
+ }
+ $html .= "
\n\n\n";
+
+ // Add data rows
+ foreach ($data as $row) {
+ $html .= "";
+ foreach ($headers as $header) {
+ $value = $row[$header] ?? '';
+ $html .= "| " . htmlspecialchars($value) . " | ";
+ }
+ $html .= "
\n";
+ }
+
+ $html .= "\n
\n";
+ return $html;
+ }
+
+ private function arrayToCsv(array $data): string {
+ if (empty($data)) {
+ return '';
+ }
+
+ $csv = '';
+
+ // Check if it's a list of objects/arrays
+ if (is_array($data[0] ?? null)) {
+ // Get headers
+ $headers = array_keys($data[0]);
+ $csv .= implode(',', $headers) . "\n";
+
+ // Add data rows
+ foreach ($data as $row) {
+ $csvRow = [];
+ foreach ($headers as $header) {
+ $value = $row[$header] ?? '';
+ $csvRow[] = '"' . str_replace('"', '""', $value) . '"';
+ }
+ $csv .= implode(',', $csvRow) . "\n";
+ }
+ } else {
+ // Simple key-value pairs
+ $csv .= "Key,Value\n";
+ foreach ($data as $key => $value) {
+ $csv .= '"' . str_replace('"', '""', $key) . '","' . str_replace('"', '""', $value) . "\"\n";
+ }
+ }
+
+ return $csv;
+ }
+}
diff --git a/examples/01-core/05-response-formats/TextResponseService.php b/examples/01-core/05-response-formats/TextResponseService.php
new file mode 100644
index 0000000..d86f417
--- /dev/null
+++ b/examples/01-core/05-response-formats/TextResponseService.php
@@ -0,0 +1,32 @@
+ 'Hello from WebFiori HTTP',
+ 'timestamp' => date('Y-m-d H:i:s'),
+ 'format' => 'text',
+ 'php_version' => PHP_VERSION,
+ 'server_time' => time()
+ ];
+
+ $text = '';
+ foreach ($data as $key => $value) {
+ $text .= "$key: $value\n";
+ }
+ return $text;
+ }
+}
diff --git a/examples/01-core/05-response-formats/XmlResponseService.php b/examples/01-core/05-response-formats/XmlResponseService.php
new file mode 100644
index 0000000..9701650
--- /dev/null
+++ b/examples/01-core/05-response-formats/XmlResponseService.php
@@ -0,0 +1,47 @@
+ 'Hello from WebFiori HTTP',
+ 'timestamp' => date('Y-m-d H:i:s'),
+ 'format' => 'xml',
+ 'server_info' => [
+ 'php_version' => PHP_VERSION,
+ 'server_time' => time()
+ ]
+ ];
+
+ return $this->arrayToXml($data);
+ }
+
+ private function arrayToXml(array $data, string $root = 'response'): string {
+ $xml = "\n<$root>\n";
+ foreach ($data as $key => $value) {
+ if (is_array($value)) {
+ $xml .= " <$key>\n";
+ foreach ($value as $k => $v) {
+ $xml .= " <$k>" . htmlspecialchars($v) . "$k>\n";
+ }
+ $xml .= " $key>\n";
+ } else {
+ $xml .= " <$key>" . htmlspecialchars($value) . "$key>\n";
+ }
+ }
+ $xml .= "$root>";
+ return $xml;
+ }
+}
diff --git a/examples/01-core/05-response-formats/index.php b/examples/01-core/05-response-formats/index.php
new file mode 100644
index 0000000..409c426
--- /dev/null
+++ b/examples/01-core/05-response-formats/index.php
@@ -0,0 +1,16 @@
+setVersion('1.0.0');
+$manager->setDescription('Response Formats API');
+
+// Auto-discover and register services
+$manager->autoDiscoverServices();
+
+// Process the incoming request
+$manager->process();
diff --git a/examples/01-core/README.md b/examples/01-core/README.md
new file mode 100644
index 0000000..2a76ddd
--- /dev/null
+++ b/examples/01-core/README.md
@@ -0,0 +1,24 @@
+# Core Features Examples
+
+This folder demonstrates core WebFiori HTTP features including parameter validation, error handling, and different content types.
+
+## Examples
+
+1. **[01-parameter-validation](01-parameter-validation/)** - Advanced parameter validation and filtering
+2. **[02-error-handling](02-error-handling/)** - Custom error responses and validation
+3. **[03-json-requests](03-json-requests/)** - Handling JSON request bodies
+4. **[04-file-uploads](04-file-uploads/)** - File upload handling with multipart forms
+5. **[05-response-formats](05-response-formats/)** - Different response content types
+
+## Prerequisites
+
+- Completed basic examples (00-basic)
+- Understanding of HTTP request/response concepts
+
+## Key Concepts
+
+- Parameter types and validation rules
+- Custom error messages and status codes
+- JSON request body parsing
+- File upload handling
+- Content negotiation
diff --git a/examples/02-security/01-basic-auth/BasicAuthService.php b/examples/02-security/01-basic-auth/BasicAuthService.php
new file mode 100644
index 0000000..a6c0202
--- /dev/null
+++ b/examples/02-security/01-basic-auth/BasicAuthService.php
@@ -0,0 +1,104 @@
+ 'password123',
+ 'user' => 'userpass',
+ 'guest' => 'guestpass'
+ ];
+
+ #[GetMapping]
+ #[ResponseBody]
+ #[RequiresAuth]
+ public function getSecureData(): array {
+ // Get authenticated user info
+ $authHeader = $this->getAuthHeader();
+ $credentials = base64_decode($authHeader->getCredentials());
+ [$username] = explode(':', $credentials, 2);
+
+ return [
+ 'user' => $username,
+ 'authenticated_at' => date('Y-m-d H:i:s'),
+ 'auth_method' => 'basic',
+ 'secure_data' => [
+ 'secret_key' => 'abc123xyz',
+ 'access_level' => $this->getUserAccessLevel($username),
+ 'session_id' => uniqid('sess_')
+ ]
+ ];
+ }
+
+ public function isAuthorized(): bool {
+ $authHeader = $this->getAuthHeader();
+
+ if ($authHeader === null) {
+ return false;
+ }
+
+ $scheme = $authHeader->getScheme();
+ $credentials = $authHeader->getCredentials();
+
+ // Check if it's Basic authentication
+ if ($scheme !== 'basic') {
+ return false;
+ }
+
+ // Decode base64 credentials
+ $decoded = base64_decode($credentials);
+
+ if ($decoded === false) {
+ return false;
+ }
+
+ // Split username and password
+ $parts = explode(':', $decoded, 2);
+
+ if (count($parts) !== 2) {
+ return false;
+ }
+
+ [$username, $password] = $parts;
+
+ // Validate credentials
+ return $this->validateUser($username, $password);
+ }
+
+ private function validateUser(string $username, string $password): bool {
+ if (!isset(self::USERS[$username])) {
+ return false;
+ }
+
+ if (self::USERS[$username] !== $password) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private function getUserAccessLevel(string $username): string {
+ switch ($username) {
+ case 'admin':
+ return 'administrator';
+ case 'user':
+ return 'standard_user';
+ case 'guest':
+ return 'read_only';
+ default:
+ return 'unknown';
+ }
+ }
+}
diff --git a/examples/02-security/01-basic-auth/README.md b/examples/02-security/01-basic-auth/README.md
new file mode 100644
index 0000000..445cdcf
--- /dev/null
+++ b/examples/02-security/01-basic-auth/README.md
@@ -0,0 +1,68 @@
+# Basic Authentication
+
+Demonstrates HTTP Basic authentication with username/password credentials.
+
+## What This Example Demonstrates
+
+- HTTP Basic authentication implementation
+- Base64 credential decoding
+- User credential validation
+- Custom authentication error messages
+
+## Files
+
+- [`BasicAuthService.php`](BasicAuthService.php) - Service with Basic auth
+- [`index.php`](index.php) - Main application entry point
+
+## How to Run
+
+```bash
+php -S localhost:8080
+```
+
+## Testing
+
+```bash
+# Without authentication (will fail)
+curl "http://localhost:8080"
+
+# With Basic authentication
+curl -u "admin:password123" "http://localhost:8080"
+
+# With invalid credentials
+curl -u "admin:wrongpassword" "http://localhost:8080"
+```
+
+**Expected Responses:**
+
+Without auth:
+```json
+{
+ "message": "Authentication required",
+ "type": "error",
+ "http-code": 401
+}
+```
+
+With valid auth:
+```json
+{
+ "message": "Access granted to secure resource",
+ "http-code": 200,
+ "data": {
+ "user": "admin",
+ "authenticated_at": "2024-01-01 12:00:00"
+ }
+}
+```
+
+## Code Explanation
+
+- `isAuthorized()` method implements custom authentication logic
+- `getAuthHeader()` retrieves the Authorization header
+- Basic auth credentials are base64 encoded as "username:password"
+- Custom user validation logic checks against predefined credentials
+- `#[RequiresAuth]` calls `isAuthorized()` first, then checks `#[PreAuthorize]` if present
+- **Authorization flow:** `isAuthorized()` → `#[PreAuthorize]` (if exists) → SecurityContext
+- Service is auto-discovered using `autoDiscoverServices()`
+- Different users get different access levels
diff --git a/examples/02-security/01-basic-auth/index.php b/examples/02-security/01-basic-auth/index.php
new file mode 100644
index 0000000..88b28bf
--- /dev/null
+++ b/examples/02-security/01-basic-auth/index.php
@@ -0,0 +1,16 @@
+setVersion('1.0.0');
+$manager->setDescription('Basic Authentication API');
+
+// Auto-discover and register services
+$manager->autoDiscoverServices();
+
+// Process the incoming request
+$manager->process();
diff --git a/examples/02-security/02-bearer-tokens/LoginService.php b/examples/02-security/02-bearer-tokens/LoginService.php
new file mode 100644
index 0000000..141e75f
--- /dev/null
+++ b/examples/02-security/02-bearer-tokens/LoginService.php
@@ -0,0 +1,50 @@
+ ['password' => 'password123', 'role' => 'admin'],
+ 'user' => ['password' => 'userpass', 'role' => 'user'],
+ 'guest' => ['password' => 'guestpass', 'role' => 'guest']
+ ];
+
+ #[PostMapping]
+ #[ResponseBody]
+ #[AllowAnonymous]
+ #[RequestParam('username', 'string', false, null, 'Username')]
+ #[RequestParam('password', 'string', false, null, 'Password')]
+ public function login(string $username, string $password): array {
+ // Validate credentials
+ if (!isset(self::USERS[$username]) || self::USERS[$username]['password'] !== $password) {
+ throw new \InvalidArgumentException('Invalid credentials');
+ }
+
+ // Generate token
+ $userData = [
+ 'username' => $username,
+ 'role' => self::USERS[$username]['role'],
+ 'login_time' => date('Y-m-d H:i:s')
+ ];
+
+ $token = TokenHelper::generateToken($userData);
+
+ return [
+ 'token' => $token,
+ 'user' => $userData,
+ 'expires_in' => 3600
+ ];
+ }
+
+}
diff --git a/examples/02-security/02-bearer-tokens/ProfileService.php b/examples/02-security/02-bearer-tokens/ProfileService.php
new file mode 100644
index 0000000..351d97b
--- /dev/null
+++ b/examples/02-security/02-bearer-tokens/ProfileService.php
@@ -0,0 +1,56 @@
+getAuthHeader();
+ $token = $authHeader->getCredentials();
+ $tokenData = TokenHelper::validateToken($token);
+ $user = $tokenData['user'];
+
+ return [
+ 'user' => $user,
+ 'permissions' => $this->getUserPermissions($user['role']),
+ 'last_access' => date('Y-m-d H:i:s')
+ ];
+ }
+
+ public function isAuthorized(): bool {
+ $authHeader = $this->getAuthHeader();
+
+ if ($authHeader === null || $authHeader->getScheme() !== 'bearer') {
+ return false;
+ }
+
+ $token = $authHeader->getCredentials();
+ $tokenData = TokenHelper::validateToken($token);
+
+ return $tokenData !== null;
+ }
+
+ private function getUserPermissions(string $role): array {
+ switch ($role) {
+ case 'admin':
+ return ['read', 'write', 'delete', 'admin'];
+ case 'user':
+ return ['read', 'write'];
+ case 'guest':
+ return ['read'];
+ default:
+ return [];
+ }
+ }
+}
diff --git a/examples/02-security/02-bearer-tokens/README.md b/examples/02-security/02-bearer-tokens/README.md
new file mode 100644
index 0000000..1a6ab51
--- /dev/null
+++ b/examples/02-security/02-bearer-tokens/README.md
@@ -0,0 +1,62 @@
+# Bearer Token Authentication
+
+Demonstrates Bearer token authentication with JWT-like tokens.
+
+## What This Example Demonstrates
+
+- Bearer token authentication
+- Token validation and parsing
+- Token expiration handling
+- User context from tokens
+
+## Files
+
+- [`TokenAuthService.php`](TokenAuthService.php) - Original unified service
+- [`LoginService.php`](LoginService.php) - Separate login service
+- [`ProfileService.php`](ProfileService.php) - Separate profile service
+- [`TokenHelper.php`](TokenHelper.php) - Token generation and validation utilities
+- [`index.php`](index.php) - Main application entry point
+
+## How to Run
+
+```bash
+php -S localhost:8080
+```
+
+## Testing
+
+```bash
+# Get a token first
+curl -X POST "http://localhost:8080?service=auth&action=login" \
+ -H "Content-Type: application/json" \
+ -d '{"username": "admin", "password": "password123"}'
+
+# Use the token (replace TOKEN with actual token from login)
+curl -H "Authorization: Bearer TOKEN" "http://localhost:8080?service=auth&action=profile"
+
+# Invalid token
+curl -H "Authorization: Bearer invalid_token" "http://localhost:8080?service=auth&action=profile"
+
+# Expired token simulation
+curl -H "Authorization: Bearer expired_token" "http://localhost:8080?service=auth&action=profile"
+```
+
+## Code Explanation
+
+**Two approaches demonstrated:**
+
+1. **Separate Services** (Recommended):
+ - `LoginService` - Handles authentication, returns token
+ - `ProfileService` - Protected resource requiring Bearer token
+ - Clean separation of concerns
+ - Each service has single responsibility
+
+2. **Unified Service**:
+ - `TokenAuthService` - Handles both login and protected operations
+ - Uses different HTTP methods (POST for login, GET for profile)
+
+- Bearer token authentication using Authorization header
+- Token validation and parsing utilities in TokenHelper
+- `#[AllowAnonymous]` for login endpoint
+- `#[RequiresAuth]` for protected endpoints
+- Service is auto-discovered using `autoDiscoverServices()`
diff --git a/examples/02-security/02-bearer-tokens/TokenAuthService.php b/examples/02-security/02-bearer-tokens/TokenAuthService.php
new file mode 100644
index 0000000..e1f804f
--- /dev/null
+++ b/examples/02-security/02-bearer-tokens/TokenAuthService.php
@@ -0,0 +1,153 @@
+ ['password' => 'password123', 'role' => 'admin'],
+ 'user' => ['password' => 'userpass', 'role' => 'user'],
+ 'guest' => ['password' => 'guestpass', 'role' => 'guest']
+ ];
+
+ private ?array $currentUser = null;
+
+ #[PostMapping]
+ #[ResponseBody]
+ #[AllowAnonymous]
+ #[RequestParam('operation', 'string', false, null, 'Operation: login')]
+ public function login(?string $operation = 'login'): array {
+ $inputs = $this->getInputs();
+
+ if (!($inputs instanceof Json)) {
+ throw new \InvalidArgumentException('JSON body required for login');
+ }
+
+ $username = $inputs->get('username');
+ $password = $inputs->get('password');
+
+ if (!$username || !$password) {
+ throw new \InvalidArgumentException('Username and password required');
+ }
+
+ // Validate credentials
+ if (!isset(self::USERS[$username]) || self::USERS[$username]['password'] !== $password) {
+ throw new \InvalidArgumentException('Invalid credentials');
+ }
+
+ // Generate token
+ $userData = [
+ 'username' => $username,
+ 'role' => self::USERS[$username]['role'],
+ 'login_time' => date('Y-m-d H:i:s')
+ ];
+
+ $token = TokenHelper::generateToken($userData);
+
+ return [
+ 'token' => $token,
+ 'user' => $userData,
+ 'expires_in' => 3600
+ ];
+ }
+
+ #[GetMapping]
+ #[ResponseBody]
+ #[RequiresAuth]
+ #[RequestParam('operation', 'string', false, null, 'Operation: profile, refresh')]
+ public function handleAuthenticatedAction(?string $operation = 'profile'): array {
+ // Extract user from token
+ $authHeader = $this->getAuthHeader();
+ $token = $authHeader->getCredentials();
+ $tokenData = TokenHelper::validateToken($token);
+ $this->currentUser = $tokenData['user'];
+
+ switch ($operation) {
+ case 'profile':
+ return $this->handleProfile();
+ case 'refresh':
+ return $this->handleRefresh();
+ default:
+ throw new \InvalidArgumentException('Unknown operation');
+ }
+ }
+
+ public function isAuthorized(): bool {
+ // All actions require Bearer token (login uses AllowAnonymous)
+ $authHeader = $this->getAuthHeader();
+
+ if ($authHeader === null) {
+ return false;
+ }
+
+ $scheme = $authHeader->getScheme();
+ $token = $authHeader->getCredentials();
+
+ if ($scheme !== 'bearer') {
+ return false;
+ }
+
+ // Validate token
+ $tokenData = TokenHelper::validateToken($token);
+
+ return $tokenData !== null;
+ }
+
+ private function handleProfile(): array {
+ if (!$this->currentUser) {
+ throw new \RuntimeException('User context not available');
+ }
+
+ return [
+ 'user' => $this->currentUser,
+ 'permissions' => $this->getUserPermissions($this->currentUser['role']),
+ 'last_access' => date('Y-m-d H:i:s')
+ ];
+ }
+
+ private function handleRefresh(): array {
+ if (!$this->currentUser) {
+ throw new \RuntimeException('User context not available');
+ }
+
+ // Generate new token with updated timestamp
+ $userData = $this->currentUser;
+ $userData['refresh_time'] = date('Y-m-d H:i:s');
+
+ $newToken = TokenHelper::generateToken($userData);
+
+ return [
+ 'token' => $newToken,
+ 'user' => $userData,
+ 'expires_in' => 3600
+ ];
+ }
+
+ private function getUserPermissions(string $role): array {
+ switch ($role) {
+ case 'admin':
+ return ['read', 'write', 'delete', 'admin'];
+ case 'user':
+ return ['read', 'write'];
+ case 'guest':
+ return ['read'];
+ default:
+ return [];
+ }
+ }
+}
diff --git a/examples/02-security/02-bearer-tokens/TokenHelper.php b/examples/02-security/02-bearer-tokens/TokenHelper.php
new file mode 100644
index 0000000..fecff73
--- /dev/null
+++ b/examples/02-security/02-bearer-tokens/TokenHelper.php
@@ -0,0 +1,69 @@
+ $userData,
+ 'iat' => time(),
+ 'exp' => time() + self::TOKEN_EXPIRY
+ ];
+
+ $header = base64_encode(json_encode(['typ' => 'JWT', 'alg' => 'HS256']));
+ $payload = base64_encode(json_encode($payload));
+ $signature = base64_encode(hash_hmac('sha256', "$header.$payload", self::SECRET_KEY, true));
+
+ return "$header.$payload.$signature";
+ }
+
+ public static function validateToken(string $token): ?array {
+ $parts = explode('.', $token);
+
+ if (count($parts) !== 3) {
+ return null;
+ }
+
+ [$header, $payload, $signature] = $parts;
+
+ // Verify signature
+ $expectedSignature = base64_encode(hash_hmac('sha256', "$header.$payload", self::SECRET_KEY, true));
+
+ if ($signature !== $expectedSignature) {
+ return null;
+ }
+
+ // Decode payload
+ $payloadData = json_decode(base64_decode($payload), true);
+
+ if (!$payloadData) {
+ return null;
+ }
+
+ // Check expiration
+ if (isset($payloadData['exp']) && $payloadData['exp'] < time()) {
+ return null; // Token expired
+ }
+
+ return $payloadData;
+ }
+
+ public static function isTokenExpired(string $token): bool {
+ $parts = explode('.', $token);
+
+ if (count($parts) !== 3) {
+ return true;
+ }
+
+ $payloadData = json_decode(base64_decode($parts[1]), true);
+
+ return isset($payloadData['exp']) && $payloadData['exp'] < time();
+ }
+}
diff --git a/examples/02-security/02-bearer-tokens/index.php b/examples/02-security/02-bearer-tokens/index.php
new file mode 100644
index 0000000..de2c248
--- /dev/null
+++ b/examples/02-security/02-bearer-tokens/index.php
@@ -0,0 +1,16 @@
+setVersion('1.0.0');
+$manager->setDescription('Bearer Token Authentication API');
+
+// Auto-discover and register services
+$manager->autoDiscoverServices();
+
+// Process the incoming request
+$manager->process();
diff --git a/examples/02-security/04-role-based-access/AdminService.php b/examples/02-security/04-role-based-access/AdminService.php
new file mode 100644
index 0000000..5d91a5f
--- /dev/null
+++ b/examples/02-security/04-role-based-access/AdminService.php
@@ -0,0 +1,50 @@
+ 'Welcome to admin panel',
+ 'user' => SecurityContext::getCurrentUser(),
+ 'admin_privileges' => [
+ 'can_delete_users' => true,
+ 'can_modify_system' => true,
+ 'can_view_logs' => true
+ ],
+ 'system_stats' => [
+ 'total_users' => 150,
+ 'active_sessions' => 23,
+ 'system_uptime' => '15 days'
+ ]
+ ];
+ }
+
+ public function isAuthorized(): bool {
+ $demoUser = new DemoUser(
+ id: 1,
+ name: 'Demo User',
+ roles: ['USER', 'ADMIN'],
+ authorities: ['USER_CREATE', 'USER_UPDATE', 'USER_DELETE', 'USER_READ', 'USER_MANAGE']
+ );
+
+ SecurityContext::setCurrentUser($demoUser);
+ return true;
+ }
+}
diff --git a/examples/02-security/04-role-based-access/DemoUser.php b/examples/02-security/04-role-based-access/DemoUser.php
new file mode 100644
index 0000000..af07032
--- /dev/null
+++ b/examples/02-security/04-role-based-access/DemoUser.php
@@ -0,0 +1,39 @@
+id;
+ }
+
+ public function getName(): string {
+ return $this->name;
+ }
+
+ public function getRoles(): array {
+ return $this->roles;
+ }
+
+ public function getAuthorities(): array {
+ return $this->authorities;
+ }
+
+ public function isActive(): bool {
+ return $this->active;
+ }
+}
diff --git a/examples/02-security/04-role-based-access/PublicService.php b/examples/02-security/04-role-based-access/PublicService.php
new file mode 100644
index 0000000..e2b3229
--- /dev/null
+++ b/examples/02-security/04-role-based-access/PublicService.php
@@ -0,0 +1,36 @@
+ 'This is public information - no authentication required',
+ 'timestamp' => date('Y-m-d H:i:s'),
+ 'server_info' => [
+ 'php_version' => PHP_VERSION,
+ 'authenticated' => SecurityContext::isAuthenticated()
+ ]
+ ];
+ }
+
+ public function isAuthorized(): bool {
+ return true; // Public service, no auth needed
+ }
+}
diff --git a/examples/02-security/04-role-based-access/README.md b/examples/02-security/04-role-based-access/README.md
new file mode 100644
index 0000000..cc49728
--- /dev/null
+++ b/examples/02-security/04-role-based-access/README.md
@@ -0,0 +1,68 @@
+# Role-Based Access Control (RBAC)
+
+Demonstrates role-based access control using SecurityContext and annotations.
+
+## What This Example Demonstrates
+
+- SecurityContext for user management
+- Role-based authorization with `@PreAuthorize`
+- Authority-based permissions
+- Method-level security annotations
+
+## Files
+
+- [`PublicService.php`](PublicService.php) - Public information (no auth required)
+- [`UserService.php`](UserService.php) - User profile service (requires authentication)
+- [`AdminService.php`](AdminService.php) - Admin panel service (requires ADMIN role)
+- [`UserManagerService.php`](UserManagerService.php) - User management (requires USER_MANAGE authority)
+- [`DemoUser.php`](DemoUser.php) - Demo user implementation
+- [`index.php`](index.php) - Main application entry point
+
+## How to Run
+
+```bash
+php -S localhost:8080
+```
+
+## Testing
+
+```bash
+# Public endpoint (no auth required)
+curl "http://localhost:8080?service=public"
+
+# User endpoint (requires authentication)
+curl "http://localhost:8080?service=user"
+
+# Admin endpoint (requires ADMIN role)
+curl "http://localhost:8080?service=admin"
+
+# User management endpoint (requires USER_MANAGE authority)
+curl "http://localhost:8080?service=user-manager"
+```
+
+**Expected Responses:**
+
+Public access:
+```json
+{
+ "message": "This is public information",
+ "http-code": 200
+}
+```
+
+With authentication:
+```json
+{
+ "user": {...},
+ "roles": ["USER", "ADMIN"],
+ "authorities": ["USER_CREATE", "USER_UPDATE"]
+}
+```
+
+## Code Explanation
+
+- `isAuthorized()` handles authentication only - validates credentials and sets `SecurityContext::setCurrentUser()`
+- `@PreAuthorize` annotations handle method-level authorization based on roles/authorities
+- `@AllowAnonymous` bypasses authentication requirements for public endpoints
+- `@RequiresAuth` ensures user is authenticated before method execution
+- Role and authority checks are evaluated at runtime using security expressions
diff --git a/examples/02-security/04-role-based-access/UserManagerService.php b/examples/02-security/04-role-based-access/UserManagerService.php
new file mode 100644
index 0000000..028e00c
--- /dev/null
+++ b/examples/02-security/04-role-based-access/UserManagerService.php
@@ -0,0 +1,50 @@
+ 'User management operation completed',
+ 'user' => SecurityContext::getCurrentUser(),
+ 'available_operations' => [
+ 'create_user' => SecurityContext::hasAuthority('USER_CREATE'),
+ 'update_user' => SecurityContext::hasAuthority('USER_UPDATE'),
+ 'delete_user' => SecurityContext::hasAuthority('USER_DELETE'),
+ 'view_users' => SecurityContext::hasAuthority('USER_READ')
+ ],
+ 'managed_users' => [
+ ['id' => 1, 'name' => 'John Doe', 'status' => 'active'],
+ ['id' => 2, 'name' => 'Jane Smith', 'status' => 'inactive']
+ ]
+ ];
+ }
+
+ public function isAuthorized(): bool {
+ $demoUser = new DemoUser(
+ id: 1,
+ name: 'Demo User',
+ roles: ['USER', 'ADMIN'],
+ authorities: ['USER_CREATE', 'USER_UPDATE', 'USER_DELETE', 'USER_READ', 'USER_MANAGE']
+ );
+
+ SecurityContext::setCurrentUser($demoUser);
+ return true;
+ }
+}
diff --git a/examples/02-security/04-role-based-access/UserService.php b/examples/02-security/04-role-based-access/UserService.php
new file mode 100644
index 0000000..e1ccb49
--- /dev/null
+++ b/examples/02-security/04-role-based-access/UserService.php
@@ -0,0 +1,43 @@
+ SecurityContext::getCurrentUser(),
+ 'roles' => SecurityContext::getRoles(),
+ 'authorities' => SecurityContext::getAuthorities(),
+ 'is_authenticated' => SecurityContext::isAuthenticated(),
+ 'access_time' => date('Y-m-d H:i:s')
+ ];
+ }
+
+ public function isAuthorized(): bool {
+ $demoUser = new DemoUser(
+ id: 1,
+ name: 'Demo User',
+ roles: ['USER', 'ADMIN'],
+ authorities: ['USER_CREATE', 'USER_UPDATE', 'USER_DELETE', 'USER_READ', 'USER_MANAGE']
+ );
+
+ SecurityContext::setCurrentUser($demoUser);
+ return true;
+ }
+}
diff --git a/examples/02-security/04-role-based-access/index.php b/examples/02-security/04-role-based-access/index.php
new file mode 100644
index 0000000..edc6990
--- /dev/null
+++ b/examples/02-security/04-role-based-access/index.php
@@ -0,0 +1,22 @@
+setVersion('1.0.0');
+$manager->setDescription('Role-Based Access Control API - Multiple Services');
+
+// Register all RBAC services
+$manager->addService(new PublicService());
+$manager->addService(new UserService());
+$manager->addService(new AdminService());
+$manager->addService(new UserManagerService());
+
+// Process the incoming request
+$manager->process();
diff --git a/examples/02-security/README.md b/examples/02-security/README.md
new file mode 100644
index 0000000..1b8181a
--- /dev/null
+++ b/examples/02-security/README.md
@@ -0,0 +1,24 @@
+# Security Examples
+
+This folder demonstrates authentication and authorization patterns in WebFiori HTTP.
+
+## Examples
+
+1. **[01-basic-auth](01-basic-auth/)** - HTTP Basic authentication
+2. **[02-bearer-tokens](02-bearer-tokens/)** - JWT/Bearer token authentication
+4. **[04-role-based-access](04-role-based-access/)** - Role-based access control (RBAC)
+5. **05-method-security** - Per-method authorization
+
+## Security Concepts
+
+- Authentication vs Authorization
+- HTTP authentication schemes
+- Token-based authentication
+- Role and permission systems
+- Security context management
+
+## Prerequisites
+
+- Understanding of HTTP authentication
+- Basic knowledge of JWT tokens
+- Familiarity with RBAC concepts
diff --git a/examples/04-advanced/01-object-mapping/GetObjectMappingService.php b/examples/04-advanced/01-object-mapping/GetObjectMappingService.php
new file mode 100644
index 0000000..454918a
--- /dev/null
+++ b/examples/04-advanced/01-object-mapping/GetObjectMappingService.php
@@ -0,0 +1,30 @@
+getObject(User::class);
+
+ return [
+ 'message' => 'User created with getObject mapping',
+ 'user' => $user->toArray(),
+ 'method' => 'getobject_mapping'
+ ];
+ }
+}
diff --git a/examples/04-advanced/01-object-mapping/ManualMappingService.php b/examples/04-advanced/01-object-mapping/ManualMappingService.php
new file mode 100644
index 0000000..2e54ac5
--- /dev/null
+++ b/examples/04-advanced/01-object-mapping/ManualMappingService.php
@@ -0,0 +1,37 @@
+getInputs();
+ $user = new User();
+
+ if ($inputs instanceof \WebFiori\Json\Json) {
+ if ($inputs->hasKey('name')) $user->setName($inputs->get('name'));
+ if ($inputs->hasKey('email')) $user->setEmail($inputs->get('email'));
+ if ($inputs->hasKey('age')) $user->setAge($inputs->get('age'));
+ }
+
+ return [
+ 'message' => 'User created with manual mapping',
+ 'user' => $user->toArray(),
+ 'method' => 'manual_mapping'
+ ];
+ }
+}
diff --git a/examples/04-advanced/01-object-mapping/MapEntityCustomMappingService.php b/examples/04-advanced/01-object-mapping/MapEntityCustomMappingService.php
new file mode 100644
index 0000000..b671623
--- /dev/null
+++ b/examples/04-advanced/01-object-mapping/MapEntityCustomMappingService.php
@@ -0,0 +1,30 @@
+ 'setFullName', 'email-address' => 'setEmailAddress', 'user-age' => 'setUserAge'])]
+ public function create(User $user): array {
+ return [
+ 'message' => 'User created with MapEntity + custom setters',
+ 'user' => $user->toArray(),
+ 'method' => 'mapentity_custom'
+ ];
+ }
+}
diff --git a/examples/04-advanced/01-object-mapping/MapEntityMappingService.php b/examples/04-advanced/01-object-mapping/MapEntityMappingService.php
new file mode 100644
index 0000000..aa1a5b5
--- /dev/null
+++ b/examples/04-advanced/01-object-mapping/MapEntityMappingService.php
@@ -0,0 +1,30 @@
+ 'User created with MapEntity attribute',
+ 'user' => $user->toArray(),
+ 'method' => 'mapentity_basic'
+ ];
+ }
+}
diff --git a/examples/04-advanced/01-object-mapping/README.md b/examples/04-advanced/01-object-mapping/README.md
new file mode 100644
index 0000000..7452e0b
--- /dev/null
+++ b/examples/04-advanced/01-object-mapping/README.md
@@ -0,0 +1,180 @@
+# Object Mapping
+
+Demonstrates all available approaches for mapping HTTP request parameters to PHP objects in WebFiori HTTP.
+
+## What This Example Demonstrates
+
+- **5 Different Object Mapping Approaches**
+- Parameter validation and type safety
+- Custom setter method mapping
+- Clean separation of data and logic
+- Modern attribute-based mapping (NEW)
+
+## Files
+
+- [`User.php`](User.php) - Data model class with validation
+- [`TraditionalMappingService.php`](TraditionalMappingService.php) - Traditional parameter mapping
+- [`ManualMappingService.php`](ManualMappingService.php) - Manual object mapping
+- [`GetObjectMappingService.php`](GetObjectMappingService.php) - getObject() mapping
+- [`MapEntityMappingService.php`](MapEntityMappingService.php) - MapEntity attribute mapping
+- [`MapEntityCustomMappingService.php`](MapEntityCustomMappingService.php) - MapEntity with custom setters
+- [`index.php`](index.php) - Main application entry point
+
+## How to Run
+
+```bash
+php -S localhost:8080
+```
+
+
+## Object Mapping Approaches
+
+### 1. Traditional Parameter Mapping
+**Method signature with individual parameters**
+```php
+#[RequestParam('name', 'string', false)]
+#[RequestParam('email', 'string', false)]
+#[RequestParam('age', 'int', false)]
+public function create(string $name, string $email, int $age): array
+```
+
+### 2. Manual Object Mapping
+**Manual parameter extraction and object creation**
+```php
+public function create(): array {
+ $inputs = $this->getInputs();
+ $user = new User();
+ if ($inputs->hasKey('name')) $user->setName($inputs->get('name'));
+ // ... manual mapping
+}
+```
+
+### 3. getObject() Mapping
+**Framework-assisted object mapping**
+```php
+public function create(): array {
+ $user = $this->getObject(User::class);
+ // Object automatically mapped from request
+}
+```
+
+### 4. MapEntity Attribute - Basic
+**Clean attribute-based mapping (NEW)**
+```php
+#[MapEntity(User::class)]
+public function create(User $user): array {
+ // $user automatically mapped and injected
+}
+```
+
+### 5. MapEntity Attribute - Custom Setters
+**Flexible parameter naming with custom setters (NEW)**
+```php
+#[MapEntity(User::class, setters: ['full-name' => 'setFullName', 'email-address' => 'setEmailAddress'])]
+public function create(User $user): array {
+ // Custom parameter mapping handled automatically
+}
+```
+
+## Testing All Approaches
+
+```bash
+# 1. Traditional Parameters
+curl -X POST "http://localhost:8080?service=traditional" \
+ -H "Content-Type: application/json" \
+ -d '{"name": "John Doe", "email": "john@example.com", "age": 30}'
+
+# 2. Manual Mapping
+curl -X POST "http://localhost:8080?service=manual" \
+ -H "Content-Type: application/json" \
+ -d '{"name": "John Doe", "email": "john@example.com", "age": 30}'
+
+# 3. getObject() Mapping
+curl -X POST "http://localhost:8080?service=getobject" \
+ -H "Content-Type: application/json" \
+ -d '{"name": "John Doe", "email": "john@example.com", "age": 30}'
+
+# 4. MapEntity Basic
+curl -X POST "http://localhost:8080?service=mapentity" \
+ -H "Content-Type: application/json" \
+ -d '{"name": "John Doe", "email": "john@example.com", "age": 30}'
+
+# 5. MapEntity Custom Setters
+curl -X POST "http://localhost:8080?service=mapentity-custom" \
+ -H "Content-Type: application/json" \
+ -d '{"full-name": "Jane Smith", "email-address": "jane@example.com", "user-age": 25}'
+```
+
+## Comparison of Approaches
+
+| Approach | Pros | Cons | Best For |
+|----------|------|------|----------|
+| **Traditional Parameters** | ✅ Explicit, IDE support | ❌ Verbose for many params | Simple endpoints |
+| **Manual Mapping** | ✅ Full control, flexible | ❌ Boilerplate code | Complex validation |
+| **getObject() Mapping** | ✅ Automatic, less code | ❌ Silent error handling | Standard mapping |
+| **MapEntity Basic** | ✅ Clean, type-safe | ❌ Less flexible | Modern development |
+| **MapEntity Custom** | ✅ Flexible + clean | ❌ Setup complexity | Legacy API integration |
+
+## Expected Results
+
+All approaches produce the same result structure:
+```json
+{
+ "message": "User created with [approach name]",
+ "user": {
+ "name": "John Doe",
+ "email": "john@example.com",
+ "age": 30,
+ "phone": null,
+ "address": null
+ },
+ "method": "[approach_identifier]"
+}
+```
+
+## Code Explanation
+
+### Evolution of Object Mapping
+
+**1. Traditional Approach** - Explicit parameter handling
+```php
+public function create(string $name, string $email, int $age): array {
+ $user = new User();
+ $user->setName($name);
+ $user->setEmail($email);
+ $user->setAge($age);
+}
+```
+
+**2. Manual Mapping** - Direct input processing
+```php
+public function create(): array {
+ $inputs = $this->getInputs();
+ $user = new User();
+ if ($inputs->hasKey('name')) $user->setName($inputs->get('name'));
+}
+```
+
+**3. Framework Mapping** - Automated object creation
+```php
+public function create(): array {
+ $user = $this->getObject(User::class);
+ // Framework handles mapping automatically
+}
+```
+
+**4. Modern Attribute Mapping** - Clean and type-safe
+```php
+#[MapEntity(User::class)]
+public function create(User $user): array {
+ // $user is automatically mapped and validated
+}
+```
+
+
+### When to Use Each Approach
+
+- **Traditional**: Simple endpoints with few parameters
+- **Manual**: Complex validation or transformation logic
+- **getObject()**: Standard mapping with framework control
+- **MapEntity**: Modern development with type safety (RECOMMENDED)
diff --git a/examples/04-advanced/01-object-mapping/TraditionalMappingService.php b/examples/04-advanced/01-object-mapping/TraditionalMappingService.php
new file mode 100644
index 0000000..ff219a3
--- /dev/null
+++ b/examples/04-advanced/01-object-mapping/TraditionalMappingService.php
@@ -0,0 +1,37 @@
+setName($name);
+ $user->setEmail($email);
+ $user->setAge($age);
+
+ return [
+ 'message' => 'User created with traditional parameters',
+ 'user' => $user->toArray(),
+ 'method' => 'traditional_parameters'
+ ];
+ }
+}
diff --git a/examples/04-advanced/01-object-mapping/User.php b/examples/04-advanced/01-object-mapping/User.php
new file mode 100644
index 0000000..71e83a4
--- /dev/null
+++ b/examples/04-advanced/01-object-mapping/User.php
@@ -0,0 +1,109 @@
+name = trim($name);
+ }
+
+ public function getName(): ?string {
+ return $this->name;
+ }
+
+ public function setEmail(string $email): void {
+ if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
+ throw new \InvalidArgumentException('Invalid email format');
+ }
+ $this->email = strtolower($email);
+ }
+
+ public function getEmail(): ?string {
+ return $this->email;
+ }
+
+ public function setAge(int $age): void {
+ if ($age < 0 || $age > 150) {
+ throw new \InvalidArgumentException('Age must be between 0 and 150');
+ }
+ $this->age = $age;
+ }
+
+ public function getAge(): ?int {
+ return $this->age;
+ }
+
+ public function setPhone(?string $phone): void {
+ if ($phone !== null) {
+ $this->phone = preg_replace('/[^0-9+\-\s]/', '', $phone);
+ }
+ }
+
+ public function getPhone(): ?string {
+ return $this->phone;
+ }
+
+ public function setAddress(?string $address): void {
+ if ($address !== null) {
+ $this->address = trim($address);
+ }
+ }
+
+ public function getAddress(): ?string {
+ return $this->address;
+ }
+
+ // Custom setters for alternative parameter names
+ public function setFullName(?string $name): void {
+ if ($name !== null) {
+ $this->setName($name);
+ }
+ }
+
+ public function setEmailAddress(?string $email): void {
+ if ($email !== null) {
+ $this->setEmail($email);
+ }
+ }
+
+ public function setUserAge(?int $age): void {
+ if ($age !== null) {
+ $this->setAge($age);
+ }
+ }
+
+ public function validate(): array {
+ $errors = [];
+
+ if (empty($this->name)) {
+ $errors[] = 'Name is required';
+ }
+
+ if (empty($this->email)) {
+ $errors[] = 'Email is required';
+ }
+
+ if ($this->age === null) {
+ $errors[] = 'Age is required';
+ }
+
+ return $errors;
+ }
+
+ public function toArray(): array {
+ return [
+ 'name' => $this->name,
+ 'email' => $this->email,
+ 'age' => $this->age,
+ 'phone' => $this->phone,
+ 'address' => $this->address
+ ];
+ }
+}
diff --git a/examples/04-advanced/01-object-mapping/index.php b/examples/04-advanced/01-object-mapping/index.php
new file mode 100644
index 0000000..8d6b168
--- /dev/null
+++ b/examples/04-advanced/01-object-mapping/index.php
@@ -0,0 +1,16 @@
+setVersion('1.0.0');
+$manager->setDescription('Complete Object Mapping API Examples');
+
+// Auto-discover and register all services
+$manager->autoDiscoverServices();
+
+// Process the incoming request
+$manager->process();
diff --git a/examples/04-advanced/03-manual-openapi/OpenAPIService.php b/examples/04-advanced/03-manual-openapi/OpenAPIService.php
new file mode 100644
index 0000000..3c644c8
--- /dev/null
+++ b/examples/04-advanced/03-manual-openapi/OpenAPIService.php
@@ -0,0 +1,23 @@
+setRequestMethods([RequestMethod::GET]);
+ }
+
+ public function processRequest() {
+ $openApiObj = $this->getManager()->toOpenAPI();
+ $info = $openApiObj->getInfo();
+ $info->setTermsOfService('https://example.com/terms');
+ $this->send('application/json', $openApiObj->toJSON(), 200);
+ }
+}
diff --git a/examples/04-advanced/03-manual-openapi/ProductService.php b/examples/04-advanced/03-manual-openapi/ProductService.php
new file mode 100644
index 0000000..1962b9c
--- /dev/null
+++ b/examples/04-advanced/03-manual-openapi/ProductService.php
@@ -0,0 +1,53 @@
+ 1, 'name' => 'Product A', 'category' => $category ?? 'Electronics'],
+ ['id' => 2, 'name' => 'Product B', 'category' => $category ?? 'Electronics']
+ ];
+ }
+
+ #[PostMapping]
+ #[ResponseBody]
+ #[AllowAnonymous]
+ #[Param('name', ParamType::STRING, 'Product name')]
+ #[Param('price', ParamType::DOUBLE, 'Product price', min: 0)]
+ public function createProduct(string $name, float $price): array {
+ return ['id' => 3, 'name' => $name, 'price' => $price];
+ }
+
+ #[PutMapping]
+ #[ResponseBody]
+ #[AllowAnonymous]
+ #[Param('id', ParamType::INT, 'Product ID')]
+ #[Param('name', ParamType::STRING, 'Product name')]
+ public function updateProduct(int $id, string $name): array {
+ return ['id' => $id, 'name' => $name];
+ }
+
+ #[DeleteMapping]
+ #[ResponseBody]
+ #[AllowAnonymous]
+ #[Param('id', ParamType::INT, 'Product ID')]
+ public function deleteProduct(int $id): array {
+ return ['deleted' => $id];
+ }
+}
diff --git a/examples/04-advanced/03-manual-openapi/README.md b/examples/04-advanced/03-manual-openapi/README.md
new file mode 100644
index 0000000..200d000
--- /dev/null
+++ b/examples/04-advanced/03-manual-openapi/README.md
@@ -0,0 +1,74 @@
+# Using OpenAPIObj with WebServicesManager
+
+This example demonstrates how to use the `OpenAPIObj` returned by `WebServicesManager::toOpenAPI()` to generate and serve OpenAPI 3.1.0 documentation for your REST API.
+
+## Files
+
+- `index.php` - Main entry point with WebServicesManager setup
+- `UserService.php` - Sample user management service
+- `ProductService.php` - Sample product catalog service
+- `OpenAPIService.php` - Service that returns OpenAPI specification
+
+## What This Example Shows
+
+- Using `WebServicesManager::toOpenAPI()` to generate OpenAPI specification
+- Creating a dedicated service endpoint to serve the OpenAPI documentation
+- Accessing and customizing the `OpenAPIObj` and its `InfoObj`
+- Automatic documentation generation from registered services
+
+## Running the Example
+
+```bash
+# Start PHP built-in server
+php -S localhost:8000
+
+# Access the OpenAPI documentation
+curl http://localhost:8000?service=openapi
+
+# Or visit in browser
+http://localhost:8000?service=openapi
+```
+
+## OpenAPIObj Structure
+
+```php
+$openApiObj = $manager->toOpenAPI();
+
+// Access components
+$info = $openApiObj->getInfo(); // InfoObj with API metadata
+$paths = $openApiObj->getPaths(); // PathsObj with all endpoints
+$version = $openApiObj->getOpenapi(); // OpenAPI spec version (3.1.0)
+```
+
+## Using the Output
+
+### With Swagger UI
+
+1. Copy the JSON output
+2. Visit https://editor.swagger.io/
+3. Paste the JSON
+4. View interactive documentation
+
+### With Postman
+
+1. Save output to `openapi.json`
+2. In Postman: Import → Upload Files
+3. Select the file
+4. All endpoints are imported
+
+## Customizing the Output
+
+```php
+// In OpenAPIService.php
+$openApiObj = $this->getManager()->toOpenAPI();
+
+// Customize info
+$info = $openApiObj->getInfo();
+$info->setTermsOfService('https://example.com/terms');
+$info->setSummary('My API Summary');
+```
+
+## Related Examples
+
+- **02-openapi-docs** - Basic OpenAPI generation setup
+- **01-object-mapping** - Request parameter mapping
diff --git a/examples/04-advanced/03-manual-openapi/UserService.php b/examples/04-advanced/03-manual-openapi/UserService.php
new file mode 100644
index 0000000..8edeb75
--- /dev/null
+++ b/examples/04-advanced/03-manual-openapi/UserService.php
@@ -0,0 +1,39 @@
+ $id ?? 1,
+ 'name' => 'John Doe',
+ 'email' => 'john@example.com'
+ ];
+ }
+
+ #[PostMapping]
+ #[ResponseBody]
+ #[AllowAnonymous]
+ #[Param('name', ParamType::STRING, 'User full name', minLength: 2, maxLength: 100)]
+ #[Param('email', ParamType::EMAIL, 'User email address')]
+ public function createUser(string $name, string $email): array {
+ return [
+ 'id' => 2,
+ 'name' => $name,
+ 'email' => $email
+ ];
+ }
+}
diff --git a/examples/04-advanced/03-manual-openapi/index.php b/examples/04-advanced/03-manual-openapi/index.php
new file mode 100644
index 0000000..1594910
--- /dev/null
+++ b/examples/04-advanced/03-manual-openapi/index.php
@@ -0,0 +1,16 @@
+setVersion('1.0.0');
+$manager->setDescription('OpenAPI Documentation Example');
+
+// Auto-discover and register all services
+$manager->autoDiscoverServices();
+
+// Process the incoming request
+$manager->process();
diff --git a/examples/04-advanced/README.md b/examples/04-advanced/README.md
new file mode 100644
index 0000000..4e46691
--- /dev/null
+++ b/examples/04-advanced/README.md
@@ -0,0 +1,26 @@
+# Advanced Patterns Examples
+
+This folder demonstrates advanced WebFiori HTTP patterns and techniques.
+
+## Examples
+
+1. **[01-object-mapping](01-object-mapping/)** - Request to object mapping with MapEntity attribute
+2. **02-custom-filters** - Custom parameter validation
+3. **03-exception-handling** - Global exception handling
+4. **04-middleware-pattern** - Request/response interceptors
+5. **05-streaming-responses** - Large data streaming
+3. **[03-manual-openapi](03-manual-openapi/)** - Using OpenAPIObj to serve API documentation
+
+## Advanced Concepts
+
+- Object-oriented request handling with MapEntity attribute
+- Custom validation and filtering
+- Structured exception handling
+- Middleware and interceptors
+- Performance optimization techniques
+
+## Prerequisites
+
+- Understanding of PHP 8+ features (attributes, typed properties)
+- Familiarity with basic WebFiori HTTP concepts
+- Knowledge of object-oriented programming patterns
diff --git a/examples/AnnotatedService.php b/examples/AnnotatedService.php
deleted file mode 100644
index 542d83b..0000000
--- a/examples/AnnotatedService.php
+++ /dev/null
@@ -1,22 +0,0 @@
-setRequestMethods([RequestMethod::GET]);
- }
-
- public function isAuthorized(): bool {
- return true;
- }
-
- public function processRequest() {
- $this->sendResponse('Hello from annotated service!');
- }
-}
-
diff --git a/examples/AuthTestService.php b/examples/AuthTestService.php
deleted file mode 100644
index 4103d18..0000000
--- a/examples/AuthTestService.php
+++ /dev/null
@@ -1,19 +0,0 @@
- 'You have super admin access!'];
- }
-}
diff --git a/examples/AuthenticatedController.php b/examples/AuthenticatedController.php
deleted file mode 100644
index 93562a9..0000000
--- a/examples/AuthenticatedController.php
+++ /dev/null
@@ -1,142 +0,0 @@
-sendResponse('This is public information - no authentication required');
- }
-
- #[GetMapping]
- #[RequiresAuth]
- public function getProfile() {
- $user = SecurityContext::getCurrentUser();
- $this->sendResponse('User profile', 200, 'success', [
- 'user' => $user,
- 'roles' => SecurityContext::getRoles(),
- 'authorities' => SecurityContext::getAuthorities()
- ]);
- }
-
- #[PostMapping]
- #[PreAuthorize("hasRole('ADMIN')")]
- public function adminOperation() {
- $this->sendResponse('Admin operation completed successfully');
- }
-
- #[PostMapping]
- #[PreAuthorize("hasAuthority('USER_MANAGE')")]
- public function manageUsers() {
- $this->sendResponse('User management operation completed');
- }
-
- public function isAuthorized(): bool {
- // This is the fallback authorization check
- // In a real application, you might check JWT tokens, session, etc.
- return true;
- }
-
- public function processRequest() {
- // Check method-level authorization first
- if (!$this->checkMethodAuthorization()) {
- $this->sendResponse('Access denied', 403, 'error');
- return;
- }
-
- $action = $_GET['action'] ?? 'public';
-
- switch ($action) {
- case 'public':
- $this->getPublicInfo();
- break;
- case 'profile':
- $this->getProfile();
- break;
- case 'admin':
- $this->adminOperation();
- break;
- case 'manage':
- $this->manageUsers();
- break;
- default:
- $this->sendResponse('Unknown action', 400, 'error');
- }
- }
-
- protected function getCurrentProcessingMethod(): ?string {
- $action = $_GET['action'] ?? 'public';
- return match($action) {
- 'public' => 'getPublicInfo',
- 'profile' => 'getProfile',
- 'admin' => 'adminOperation',
- 'manage' => 'manageUsers',
- default => null
- };
- }
-}
-
-// Demo usage
-// echo "=== Authentication Demo ===\n";
-
-// $controller = new AuthenticatedController();
-
-// // Test 1: Public access (no auth required)
-// echo "\n1. Testing public access:\n";
-// $_GET['action'] = 'public';
-// $controller->processRequest();
-
-// // Test 2: Private access without authentication
-// echo "\n2. Testing private access without auth:\n";
-// $_GET['action'] = 'profile';
-// $controller->processRequest();
-
-// Test 3: Set up authentication
-// echo "\n3. Setting up authentication:\n";
-// SecurityContext::setCurrentUser(['id' => 1, 'name' => 'John Doe', 'email' => 'john@example.com']);
-// SecurityContext::setRoles(['USER']);
-// SecurityContext::setAuthorities(['USER_READ']);
-
-// echo "User authenticated: " . (SecurityContext::isAuthenticated() ? 'Yes' : 'No') . "\n";
-// echo "Roles: " . implode(', ', SecurityContext::getRoles()) . "\n";
-// echo "Authorities: " . implode(', ', SecurityContext::getAuthorities()) . "\n";
-
-// // Test 4: Private access with authentication
-// echo "\n4. Testing private access with auth:\n";
-// $_GET['action'] = 'profile';
-// $controller->processRequest();
-
-// // Test 5: Admin access without admin role
-// echo "\n5. Testing admin access without admin role:\n";
-// $_GET['action'] = 'admin';
-// $controller->processRequest();
-
-// // Test 6: Grant admin role and try again
-// echo "\n6. Granting admin role and testing admin access:\n";
-// SecurityContext::setRoles(['USER', 'ADMIN']);
-// $controller->processRequest();
-
-// // Test 7: Authority-based access
-// echo "\n7. Testing authority-based access:\n";
-// $_GET['action'] = 'manage';
-// $controller->processRequest();
-
-// // Test 8: Grant required authority
-// echo "\n8. Granting USER_MANAGE authority:\n";
-// SecurityContext::setAuthorities(['USER_READ', 'USER_MANAGE']);
-// $controller->processRequest();
-
-// // Cleanup
-// SecurityContext::clear();
-// unset($_GET['action']);
diff --git a/examples/CompleteApiDemo.php b/examples/CompleteApiDemo.php
deleted file mode 100644
index 16f4e4a..0000000
--- a/examples/CompleteApiDemo.php
+++ /dev/null
@@ -1,124 +0,0 @@
-getParamVal('id');
-
- if ($id) {
- // Get specific user
- if ($id <= 0) {
- throw new BadRequestException('Invalid user ID');
- }
-
- if ($id === 404) {
- throw new NotFoundException('User not found');
- }
-
- return [
- 'user' => [
- 'id' => $id,
- 'name' => 'John Doe',
- 'email' => 'john@example.com'
- ]
- ];
- } else {
- // Get all users
- return [
- 'users' => [
- ['id' => 1, 'name' => 'John Doe'],
- ['id' => 2, 'name' => 'Jane Smith']
- ],
- 'total' => 2
- ];
- }
- }
-
- #[PostMapping]
- #[ResponseBody(status: 201)]
- #[RequestParam('name', 'string')]
- #[RequestParam('email', 'email')]
- #[PreAuthorize("hasAuthority('USER_CREATE')")]
- public function createUser(): array {
- $name = $this->getParamVal('name');
- $email = $this->getParamVal('email');
-
- if (empty($name)) {
- throw new BadRequestException('Name is required');
- }
-
- return [
- 'message' => 'User created successfully',
- 'user' => [
- 'id' => rand(1000, 9999),
- 'name' => $name,
- 'email' => $email,
- 'created_at' => date('Y-m-d H:i:s')
- ]
- ];
- }
-
- #[PutMapping]
- #[ResponseBody]
- #[RequestParam('id', 'int')]
- #[RequestParam('name', 'string', true)]
- #[RequestParam('email', 'email', true)]
- #[PreAuthorize("hasAuthority('USER_UPDATE')")]
- public function updateUser(): array {
- $id = $this->getParamVal('id');
- $name = $this->getParamVal('name');
- $email = $this->getParamVal('email');
-
- if ($id === 404) {
- throw new NotFoundException('User not found');
- }
-
- $updates = array_filter([
- 'name' => $name,
- 'email' => $email
- ]);
-
- return [
- 'message' => 'User updated successfully',
- 'user' => [
- 'id' => $id,
- 'updates' => $updates,
- 'updated_at' => date('Y-m-d H:i:s')
- ]
- ];
- }
-
- #[DeleteMapping]
- #[ResponseBody(status: 204)]
- #[RequestParam('id', 'int')]
- #[PreAuthorize("hasRole('ADMIN')")]
- public function deleteUser(): null {
- $id = $this->getParamVal('id');
-
- if ($id === 404) {
- throw new NotFoundException('User not found');
- }
-
- // Simulate deletion
- return null; // Auto-converts to 204 No Content
- }
-}
diff --git a/examples/GetRandomService.php b/examples/GetRandomService.php
deleted file mode 100644
index 7e1cc8d..0000000
--- a/examples/GetRandomService.php
+++ /dev/null
@@ -1,47 +0,0 @@
-setRequestMethods([RequestMethod::GET, RequestMethod::POST]);
- $this->setDescription('Returns a random integer. If no range is specified, the method will return a number between 0 and getrandmax().');
-
- $this->addParameters([
- 'min' => [
- ParamOption::TYPE => ParamType::INT,
- ParamOption::OPTIONAL => true,
- ParamOption::DESCRIPTION => 'Minimum value for the random number.'
- ],
- 'max' => [
- ParamOption::TYPE => ParamType::INT,
- ParamOption::OPTIONAL => true,
- ParamOption::DESCRIPTION => 'Maximum value for the random number.'
- ]
- ]);
- }
-
- public function isAuthorized(): bool {
- return true;
- }
-
- public function processRequest() {
- $max = $this->getParamVal('max');
- $min = $this->getParamVal('min');
-
- if ($max !== null && $min !== null) {
- $random = rand($min, $max);
- } else {
- $random = rand();
- }
-
- $this->sendResponse('Random number generated', 'success', 200, [
- 'number' => $random
- ]);
- }
-}
diff --git a/examples/HelloWithAuthService.php b/examples/HelloWithAuthService.php
deleted file mode 100644
index 2585571..0000000
--- a/examples/HelloWithAuthService.php
+++ /dev/null
@@ -1,51 +0,0 @@
-setRequestMethods([RequestMethod::GET]);
-
- $this->addParameters([
- 'my-name' => [
- ParamOption::TYPE => ParamType::STRING,
- ParamOption::OPTIONAL => true
- ]
- ]);
- }
- public function isAuthorized(): bool {
- //Change default response message to custom one
- ResponseMessage::set('401', 'Not authorized to use this API.');
-
- $authHeader = $this->getAuthHeader();
-
- if ($authHeader === null) {
- return false;
- }
-
- $scheme = $authHeader->getScheme();
- $credentials = $authHeader->getCredentials();
-
- if ($scheme != 'bearer') {
- return false;
- }
-
- return $credentials == 'abc123trX';
- }
-
- public function processRequest() {
- $name = $this->getParamVal('my-name');
-
- if ($name !== null) {
- $this->sendResponse("Hello '$name'.");
- }
- $this->sendResponse('Hello World!');
- }
-}
diff --git a/examples/HelloWorldService.php b/examples/HelloWorldService.php
deleted file mode 100644
index 23dbd9c..0000000
--- a/examples/HelloWorldService.php
+++ /dev/null
@@ -1,36 +0,0 @@
-setRequestMethods([RequestMethod::GET]);
- $this->setDescription('Returns a greeting message.');
-
- $this->addParameters([
- 'my-name' => [
- ParamOption::TYPE => ParamType::STRING,
- ParamOption::OPTIONAL => true,
- ParamOption::DESCRIPTION => 'Your name to include in the greeting.'
- ]
- ]);
- }
- public function isAuthorized(): bool {
- return true;
- }
-
- public function processRequest() {
- $name = $this->getParamVal('my-name');
-
- if ($name !== null) {
- $this->sendResponse("Hello '$name'.");
- } else {
- $this->sendResponse('Hello World!');
- }
- }
-}
diff --git a/examples/ProductController.php b/examples/ProductController.php
deleted file mode 100644
index 760edaf..0000000
--- a/examples/ProductController.php
+++ /dev/null
@@ -1,91 +0,0 @@
-getParamVal('page');
- $limit = $this->getParamVal('limit');
-
- $this->sendResponse('Products retrieved', 'success', 200, [
- 'page' => $page,
- 'limit' => $limit,
- 'products' => []
- ]);
- }
-
- #[PostMapping]
- #[RequestParam('name', 'string', false, null, 'Product name')]
- #[RequestParam('price', 'float', false, null, 'Product price')]
- #[RequestParam('category', 'string', true, 'General', 'Product category')]
- public function createProduct() {
- $name = $this->getParamVal('name');
- $price = $this->getParamVal('price');
- $category = $this->getParamVal('category');
-
- $this->sendResponse('Product created', 'success', 201, [
- 'id' => 123,
- 'name' => $name,
- 'price' => $price,
- 'category' => $category
- ]);
- }
-
- #[PutMapping]
- #[RequestParam('id', 'int', false, null, 'Product ID')]
- #[RequestParam('name', 'string', true)]
- #[RequestParam('price', 'float', true)]
- public function updateProduct() {
- $id = $this->getParamVal('id');
- $name = $this->getParamVal('name');
- $price = $this->getParamVal('price');
-
- $this->sendResponse('Product updated', 'success', 200, [
- 'id' => $id,
- 'name' => $name,
- 'price' => $price
- ]);
- }
-
- #[DeleteMapping]
- #[RequestParam('id', 'int', false, null, 'Product ID to delete')]
- public function deleteProduct() {
- $id = $this->getParamVal('id');
- $this->sendResponse('Product deleted', 'success', 200, ['id' => $id]);
- }
-
- public function isAuthorized(): bool {
- return true;
- }
-
- public function processRequest() {
- $method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
-
- switch ($method) {
- case \WebFiori\Http\RequestMethod::GET:
- $this->getProducts();
- break;
- case \WebFiori\Http\RequestMethod::POST:
- $this->createProduct();
- break;
- case \WebFiori\Http\RequestMethod::PUT:
- $this->updateProduct();
- break;
- case \WebFiori\Http\RequestMethod::DELETE:
- $this->deleteProduct();
- break;
- }
- }
-}
\ No newline at end of file
diff --git a/examples/README.md b/examples/README.md
index 1b22b36..1def9f4 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -1,337 +1,30 @@
# WebFiori HTTP Examples
-This folder contains practical examples demonstrating how to use the WebFiori HTTP library to create RESTful web services. The examples showcase different features including basic services, parameter handling, and authentication.
+This directory contains comprehensive code samples demonstrating the WebFiori HTTP library features, organized from basic to advanced usage patterns.
-## Table of Contents
+## Structure
-- [Prerequisites](#prerequisites)
-- [Setup](#setup)
-- [Available Services](#available-services)
- - [1. Hello World Service](#1-hello-world-service-helloworldservicephp)
- - [2. Random Number Service](#2-random-number-service-getrandomservicephp)
- - [3. Hello with Authentication Service](#3-hello-with-authentication-service-hellowithAuthservicephp)
-- [Main Application](#main-application-indexphp)
-- [Loader Configuration](#loader-configuration-loaderphp)
-- [Key Concepts Demonstrated](#key-concepts-demonstrated)
-- [Testing All Services](#testing-all-services)
-- [Notes](#notes)
+- **[00-basic/](00-basic/)** - Foundation concepts and simple services
+- **[01-core/](01-core/)** - Core features like validation, error handling
+- **[02-security/](02-security/)** - Authentication and authorization patterns
+- **[03-annotations/](03-annotations/)** - Modern annotation-based development
+- **[04-advanced/](04-advanced/)** - Advanced patterns and techniques
+- **05-applications/** - Real-world application examples
-## Prerequisites
+## Quick Start
-- PHP 8.1 or higher
-- Composer installed
-- WebFiori HTTP library dependencies
-
-## Setup
-
-1. **Install dependencies** (run from the project root directory):
+1. Install dependencies:
```bash
composer install
```
-2. **Navigate to the examples directory**:
+2. Navigate to any example folder and run:
```bash
- cd examples
+ php -S localhost:8080
```
-3. **Start the PHP development server**:
- ```bash
- php -S localhost:8989
- ```
-
-## Available Services
-
-### 1. Hello World Service (`HelloWorldService.php`)
-
-A basic service that demonstrates simple parameter handling.
-
-**Service Name**: `hello`
-**HTTP Methods**: GET
-**Parameters**:
-- `my-name` (optional, string): Name to include in greeting
-
-**Code Example**:
-```php
-setRequestMethods([RequestMethod::GET]);
-
- $this->addParameters([
- 'my-name' => [
- ParamOption::TYPE => ParamType::STRING,
- ParamOption::OPTIONAL => true
- ]
- ]);
- }
- public function isAuthorized() {
- }
-
- public function processRequest() {
- $name = $this->getParamVal('my-name');
-
- if ($name !== null) {
- $this->sendResponse("Hello '$name'.");
- }
- $this->sendResponse('Hello World!');
- }
-}
-```
-
-**Test URLs**:
-```bash
-# Basic hello
-curl "http://localhost:8989?service=hello"
-# Response: {"message":"Hello World!","http-code":200}
-
-# Hello with name
-curl "http://localhost:8989?service=hello&my-name=ibrahim"
-# Response: {"message":"Hello 'ibrahim'.","http-code":200}
-```
-
-### 2. Random Number Service (`GetRandomService.php`)
-
-Demonstrates parameter validation and processing with optional integer parameters.
-
-**Service Name**: `get-random-number`
-**HTTP Methods**: GET, POST
-**Parameters**:
-- `min` (optional, integer): Minimum value for random number
-- `max` (optional, integer): Maximum value for random number
-
-**Code Example**:
-```php
-setRequestMethods([
- RequestMethod::GET,
- RequestMethod::POST
- ]);
-
- $this->addParameters([
- 'min' => [
- ParamOption::TYPE => ParamType::INT,
- ParamOption::OPTIONAL => true
- ],
- 'max' => [
- ParamOption::TYPE => ParamType::INT,
- ParamOption::OPTIONAL => true
- ]
- ]);
- }
-
- public function isAuthorized() {
-// $authHeader = $this->getAuthHeader();
-//
-// if ($authHeader === null) {
-// return false;
-// }
-//
-// $scheme = $authHeader->getScheme();
-// $credentials = $authHeader->getCredentials();
-
- //Verify credentials based on auth scheme (e.g. 'Basic', 'Barear'
- }
-
- public function processRequest() {
- $max = $this->getParamVal('max');
- $min = $this->getParamVal('min');
-
- if ($max !== null && $min !== null) {
- $random = rand($min, $max);
- } else {
- $random = rand();
- }
- $this->sendResponse($random);
- }
-}
-```
-
-**Test URLs**:
-```bash
-# Random number without bounds
-curl "http://localhost:8989?service=get-random-number"
-# Response: {"message":"1255598581","http-code":200}
-
-# Random number between 1 and 10
-curl "http://localhost:8989?service=get-random-number&min=1&max=10"
-# Response: {"message":"7","http-code":200}
-
-# Random number between -4 and 0
-curl "http://localhost:8989?service=get-random-number&min=-4&max=0"
-# Response: {"message":"-1","http-code":200}
-
-# Invalid parameter type (demonstrates validation)
-curl "http://localhost:8989?service=get-random-number&min=-4&max=Super"
-# Response: {"message":"The following parameter(s) has invalid values: 'max'.","type":"error","http-code":404,"more-info":{"invalid":["max"]}}
-```
-
-### 3. Hello with Authentication Service (`HelloWithAuthService.php`)
-
-Demonstrates Bearer token authentication implementation.
-
-**Service Name**: `hello-with-auth`
-**HTTP Methods**: GET
-**Authentication**: Bearer token required (`abc123trX`)
-**Parameters**:
-- `my-name` (optional, string): Name to include in greeting
-
-**Code Example**:
-```php
-setRequestMethods([RequestMethod::GET]);
-
- $this->addParameters([
- 'my-name' => [
- ParamOption::TYPE => ParamType::STRING,
- ParamOption::OPTIONAL => true
- ]
- ]);
- }
- public function isAuthorized() {
- //Change default response message to custom one
- ResponseMessage::set('401', 'Not authorized to use this API.');
-
- $authHeader = $this->getAuthHeader();
-
- if ($authHeader === null) {
- return false;
- }
-
- $scheme = $authHeader->getScheme();
- $credentials = $authHeader->getCredentials();
-
- if ($scheme != 'bearer') {
- return false;
- }
-
- return $credentials == 'abc123trX';
- }
-
- public function processRequest() {
- $name = $this->getParamVal('my-name');
-
- if ($name !== null) {
- $this->sendResponse("Hello '$name'.");
- }
- $this->sendResponse('Hello World!');
- }
-}
-```
-
-**Test URLs**:
-```bash
-# Without authorization (will fail)
-curl "http://localhost:8989?service=hello-with-auth&my-name=ibrahim"
-# Response: {"message":"Not authorized to use this API.","type":"error","http-code":401}
-
-# With correct Bearer token
-curl -H "Authorization: Bearer abc123trX" "http://localhost:8989?service=hello-with-auth&my-name=ibrahim"
-# Response: {"message":"Hello 'ibrahim'.","http-code":200}
-```
-
-## Main Application (`index.php`)
-
-The main entry point that registers all services with the WebServicesManager:
-
-```php
-addService(new HelloWorldService());
-$manager->addService(new GetRandomService());
-$manager->addService(new HelloWithAuthService());
-$manager->process();
-```
-
-## Loader Configuration (`loader.php`)
-
-Sets up error reporting and autoloading:
-
-```php
-sendResponse('Retrieved all users', 'success', 200, ['users' => []]);
- }
-
- #[PostMapping]
- public function createUser() {
- $this->sendResponse('User created', 'success', 201, ['id' => 123]);
- }
-
- #[PutMapping]
- public function updateUser() {
- $this->sendResponse('User updated', 'success', 200);
- }
-
- #[DeleteMapping]
- public function deleteUser() {
- $this->sendResponse('User deleted', 'success', 200);
- }
-
- public function isAuthorized(): bool {
- return true;
- }
-
- public function processRequest() {
- $method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
-
- switch ($method) {
- case \WebFiori\Http\RequestMethod::GET:
- $this->getUsers();
- break;
- case \WebFiori\Http\RequestMethod::POST:
- $this->createUser();
- break;
- case \WebFiori\Http\RequestMethod::PUT:
- $this->updateUser();
- break;
- case \WebFiori\Http\RequestMethod::DELETE:
- $this->deleteUser();
- break;
- default:
- $this->sendResponse('Method not allowed', 'error', 405);
- }
- }
-}
diff --git a/examples/index.php b/examples/index.php
deleted file mode 100644
index 537e386..0000000
--- a/examples/index.php
+++ /dev/null
@@ -1,35 +0,0 @@
- 1, 'name' => 'Demo User']);
-SecurityContext::setRoles(['USER', 'ADMIN']);
-SecurityContext::setCurrentUser(['id' => 1, 'name' => 'Demo User']);
-SecurityContext::setRoles(['USER', 'ADMIN']);
-SecurityContext::setAuthorities(['USER_CREATE', 'USER_UPDATE', 'USER_DELETE']);
-
-$manager = new WebServicesManager();
-$manager->addService(new HelloWorldService());
-$manager->addService(new GetRandomService());
-$manager->addService(new HelloWithAuthService());
-$manager->addService(new CompleteApiDemo());
-$manager->addService(new ProductController());
-$manager->addService(new AuthenticatedController());
-$manager->addService(new UserController());
-$manager->addService(new AuthTestService());
-$manager->process();
diff --git a/examples/loader.php b/examples/loader.php
deleted file mode 100644
index a83718c..0000000
--- a/examples/loader.php
+++ /dev/null
@@ -1,7 +0,0 @@
-setDefault('default_value');
+ $filter->addRequestParameter($param);
+
+ $filter->filterGET();
+ $inputs = $filter->getInputs();
+ $this->assertEquals('default_value', $inputs['test']);
+ }
+
+ public function testEmptyStringNotAllowed() {
+ $filter = new APIFilter();
+ $param = new RequestParameter('test', 'string');
+ $param->setIsEmptyStringAllowed(false);
+ $filter->addRequestParameter($param);
+
+ $_GET['test'] = '';
+ $filter->filterGET();
+ $inputs = $filter->getInputs();
+ $this->assertEquals(APIFilter::INVALID, $inputs['test']);
+ unset($_GET['test']);
+ }
+
+ public function testInvalidValueWithDefault() {
+ $filter = new APIFilter();
+ $param = new RequestParameter('age', 'integer', true);
+ $param->setDefault(25);
+ $filter->addRequestParameter($param);
+
+ $_GET['age'] = 'invalid';
+ $filter->filterGET();
+ $inputs = $filter->getInputs();
+ $this->assertEquals(25, $inputs['age']);
+ unset($_GET['age']);
+ }
+
+ public function testArrayParameter() {
+ $filter = new APIFilter();
+ $param = new RequestParameter('items', 'array');
+ $filter->addRequestParameter($param);
+
+ $_GET['items'] = ['a', 'b', 'c'];
+ $filter->filterGET();
+ $inputs = $filter->getInputs();
+ $this->assertIsArray($inputs['items']);
+ $this->assertCount(3, $inputs['items']);
+ unset($_GET['items']);
+ }
+
+ public function testDoubleWithMinMax() {
+ $filter = new APIFilter();
+ $param = new RequestParameter('price', 'double');
+ $param->setMinValue(0);
+ $param->setMaxValue(100);
+ $filter->addRequestParameter($param);
+
+ $_GET['price'] = '50.5';
+ $filter->filterGET();
+ $inputs = $filter->getInputs();
+ $this->assertNotEquals(APIFilter::INVALID, $inputs['price']);
+ unset($_GET['price']);
+ }
+
+ public function testArrayStringParsing() {
+ $filter = new APIFilter();
+ $param = new RequestParameter('values', 'array');
+ $filter->addRequestParameter($param);
+
+ $_GET['values'] = '[true,false,null,123,45.6]';
+ $filter->filterGET();
+ $inputs = $filter->getInputs();
+
+ if (is_array($inputs['values'])) {
+ $this->assertGreaterThan(0, count($inputs['values']));
+ }
+ unset($_GET['values']);
+ }
+
+ public function testBooleanParameter() {
+ $filter = new APIFilter();
+ $param = new RequestParameter('active', 'boolean');
+ $filter->addRequestParameter($param);
+
+ $_GET['active'] = 'true';
+ $filter->filterGET();
+ $inputs = $filter->getInputs();
+ $this->assertTrue($inputs['active'] === true || $inputs['active'] === 1);
+ unset($_GET['active']);
+ }
+
+ public function testOptionalParameterNotProvided() {
+ $filter = new APIFilter();
+ $param = new RequestParameter('optional', 'string', true);
+ $filter->addRequestParameter($param);
+
+ $filter->filterGET();
+ $inputs = $filter->getInputs();
+ $this->assertArrayHasKey('optional', $inputs);
+ }
+
+ public function testClearInputs() {
+ $filter = new APIFilter();
+ $param = new RequestParameter('test', 'string');
+ $filter->addRequestParameter($param);
+
+ $_GET['test'] = 'value';
+ $filter->filterGET();
+ $this->assertNotEmpty($filter->getInputs());
+
+ $filter->clearInputs();
+ $this->assertEmpty($filter->getInputs());
+ unset($_GET['test']);
+ }
}
diff --git a/tests/WebFiori/Tests/Http/AuthenticationAnnotationTest.php b/tests/WebFiori/Tests/Http/AuthenticationAnnotationTest.php
index 9345a3f..ad71e85 100644
--- a/tests/WebFiori/Tests/Http/AuthenticationAnnotationTest.php
+++ b/tests/WebFiori/Tests/Http/AuthenticationAnnotationTest.php
@@ -48,19 +48,19 @@ public function testMethodLevelAuthorization() {
// Test admin method without admin role
$_GET['action'] = 'admin';
- SecurityContext::setRoles(['USER']);
+ SecurityContext::setCurrentUser(new TestUser(1, ['USER'], [], true));
$this->assertFalse($service->checkMethodAuthorization());
// Test admin method with admin role
- SecurityContext::setRoles(['ADMIN']);
+ SecurityContext::setCurrentUser(new TestUser(1, ['ADMIN'], [], true));
$this->assertTrue($service->checkMethodAuthorization());
// Test authority-based method
$_GET['action'] = 'create';
- SecurityContext::setAuthorities(['USER_READ']);
+ SecurityContext::setCurrentUser(new TestUser(1, [], ['USER_READ'], true));
$this->assertFalse($service->checkMethodAuthorization());
- SecurityContext::setAuthorities(['USER_CREATE']);
+ SecurityContext::setCurrentUser(new TestUser(1, [], ['USER_CREATE'], true));
$this->assertTrue($service->checkMethodAuthorization());
}
diff --git a/tests/WebFiori/Tests/Http/HttpMessageTest.php b/tests/WebFiori/Tests/Http/HttpMessageTest.php
index 1ee91cd..8ab32df 100644
--- a/tests/WebFiori/Tests/Http/HttpMessageTest.php
+++ b/tests/WebFiori/Tests/Http/HttpMessageTest.php
@@ -181,4 +181,11 @@ public function testMultipleInstances() {
$this->assertTrue($message2->hasHeader('Accept'));
$this->assertFalse($message2->hasHeader('Content-Type'));
}
+
+ public function testGetBody() {
+ $message = new \WebFiori\Http\Response();
+ $message->write('test body');
+ $this->assertEquals('test body', $message->getBody());
+ }
}
+
diff --git a/tests/WebFiori/Tests/Http/RequestParameterTest.php b/tests/WebFiori/Tests/Http/RequestParameterTest.php
index 4822eab..c7a9000 100644
--- a/tests/WebFiori/Tests/Http/RequestParameterTest.php
+++ b/tests/WebFiori/Tests/Http/RequestParameterTest.php
@@ -673,4 +673,38 @@ public function testRequestMethod00() {
$rp->addMethods(['geT', 'PoSt ']);
$this->assertEquals(['GET', 'POST'], $rp->getMethods());
}
+
+ public function testReservedParameterName() {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('reserved');
+ new RequestParameter('service', 'string');
+ }
+
+ public function testToJSONWithMethods() {
+ $rp = new RequestParameter('user-id', 'integer');
+ $rp->addMethod('POST');
+ $json = $rp->toJSON();
+ $this->assertEquals('body', $json->get('in'));
+ }
+
+ public function testSetCustomFilter() {
+ $rp = new RequestParameter('custom', 'string');
+ $rp->setCustomFilterFunction(function($val) {
+ return strtoupper($val);
+ });
+ $this->assertNotNull($rp->getCustomFilterFunction());
+ }
+
+ public function testSetDescription() {
+ $rp = new RequestParameter('test', 'string');
+ $rp->setDescription('Test parameter');
+ $this->assertEquals('Test parameter', $rp->getDescription());
+ }
+
+ public function testIsEmptyStringAllowed() {
+ $rp = new RequestParameter('test', 'string');
+ $rp->setIsEmptyStringAllowed(true);
+ $this->assertTrue($rp->isEmptyStringAllowed());
+ }
}
+
diff --git a/tests/WebFiori/Tests/Http/RequestTest.php b/tests/WebFiori/Tests/Http/RequestTest.php
index 91abc0c..c8cd1e0 100644
--- a/tests/WebFiori/Tests/Http/RequestTest.php
+++ b/tests/WebFiori/Tests/Http/RequestTest.php
@@ -214,4 +214,44 @@ public function testGetCookie00() {
$this->request = Request::createFromGlobals();
$this->assertEquals('cool_cookie', $this->request->getCookieValue('cool'));
}
+
+ public function testGetPathWithScriptName() {
+ unset($_SERVER['REQUEST_URI']);
+ $_SERVER['SCRIPT_NAME'] = '/api/index.php';
+ $this->request = Request::createFromGlobals();
+ $path = $this->request->getPath();
+ $this->assertNotEmpty($path);
+ }
+
+ public function testGetUri() {
+ $_SERVER['REQUEST_URI'] = '/api/users';
+ $this->request = Request::createFromGlobals();
+ $uri = $this->request->getUri();
+ $this->assertInstanceOf(\WebFiori\Http\RequestUri::class, $uri);
+ }
+
+ public function testGetRequestedURIWithPath() {
+ $_SERVER['REQUEST_URI'] = '/api/test';
+ $_SERVER['HTTP_HOST'] = 'example.com';
+ $this->request = Request::createFromGlobals();
+ $uri = $this->request->getRequestedURI('extra/path');
+ $this->assertStringContainsString('extra/path', $uri);
+ }
+
+ public function testGetPathReturnsSlashWhenNull() {
+ unset($_SERVER['REQUEST_URI']);
+ unset($_SERVER['SCRIPT_NAME']);
+ $this->request = Request::createFromGlobals();
+ $path = $this->request->getPath();
+ $this->assertEquals('/', $path);
+ }
+
+ public function testGetRequestedURIWithTrailingSlash() {
+ $_SERVER['REQUEST_URI'] = '/api/';
+ $_SERVER['HTTP_HOST'] = 'example.com';
+ $this->request = Request::createFromGlobals();
+ $uri = $this->request->getRequestedURI('users');
+ $this->assertStringContainsString('users', $uri);
+ }
}
+
diff --git a/tests/WebFiori/Tests/Http/RequestUriTest.php b/tests/WebFiori/Tests/Http/RequestUriTest.php
index 6be29ab..ea34af7 100644
--- a/tests/WebFiori/Tests/Http/RequestUriTest.php
+++ b/tests/WebFiori/Tests/Http/RequestUriTest.php
@@ -196,4 +196,13 @@ public function testSetUriPossibleVar03() {
$this->assertEquals(['hell','is','not','heven'], $uri->getAllowedParameterValues('second-var'));
$this->assertEquals([], $uri->getAllowedParameterValues('secohhnd-var'));
}
+
+ public function testEqualsWithDifferentMethods() {
+ $uri1 = new RequestUri('https://example.com/test');
+ $uri1->addRequestMethod('GET');
+ $uri2 = new RequestUri('https://example.com/test');
+ $uri2->addRequestMethod('POST');
+ $this->assertFalse($uri1->equals($uri2));
+ }
}
+
diff --git a/tests/WebFiori/Tests/Http/ResponseTest.php b/tests/WebFiori/Tests/Http/ResponseTest.php
index f2f372e..752e0f2 100644
--- a/tests/WebFiori/Tests/Http/ResponseTest.php
+++ b/tests/WebFiori/Tests/Http/ResponseTest.php
@@ -230,4 +230,54 @@ public function testClear() {
$this->assertEquals('', $response->getBody());
$this->assertEquals([], $response->getHeaders());
}
+
+ public function testAppendBody() {
+ $response = new Response();
+ $response->write('First ');
+ $response->write('Second');
+ $this->assertEquals('First Second', $response->getBody());
+ }
+
+ public function testGetHeadersPool() {
+ $response = new Response();
+ $pool = $response->getHeadersPool();
+ $this->assertInstanceOf(\WebFiori\Http\HeadersPool::class, $pool);
+ }
+
+ public function testHasCookie() {
+ $response = new Response();
+ $this->assertFalse($response->hasCookie('test'));
+ }
+
+ public function testGetCookies() {
+ $response = new Response();
+ $cookies = $response->getCookies();
+ $this->assertIsArray($cookies);
+ }
+
+ public function testSetCode() {
+ $response = new Response();
+ $response->setCode(404);
+ $this->assertEquals(404, $response->getCode());
+ }
+
+ public function testIsSent() {
+ $response = new Response();
+ $this->assertFalse($response->isSent());
+ }
+
+ public function testRemoveHeader() {
+ $response = new Response();
+ $response->addHeader('X-Test', 'value');
+ $response->removeHeader('X-Test');
+ $this->assertFalse($response->hasHeader('X-Test'));
+ }
+
+ public function testClearHeaders() {
+ $response = new Response();
+ $response->addHeader('X-Test', 'value');
+ $response->clearHeaders();
+ $this->assertEmpty($response->getHeaders());
+ }
}
+
diff --git a/tests/WebFiori/Tests/Http/SchemaTest.php b/tests/WebFiori/Tests/Http/SchemaTest.php
new file mode 100644
index 0000000..4ef3994
--- /dev/null
+++ b/tests/WebFiori/Tests/Http/SchemaTest.php
@@ -0,0 +1,56 @@
+toJSON();
+ $this->assertEquals('string', $json->get('type'));
+ }
+
+ public function testSchemaInteger() {
+ $schema = new Schema('integer');
+ $json = $schema->toJSON();
+ $this->assertEquals('integer', $json->get('type'));
+ }
+
+ public function testFromRequestParameterEmail() {
+ $param = new RequestParameter('email', ParamType::EMAIL);
+ $schema = Schema::fromRequestParameter($param);
+ $json = $schema->toJSON();
+ $this->assertEquals('email', $json->get('format'));
+ }
+
+ public function testFromRequestParameterUrl() {
+ $param = new RequestParameter('website', ParamType::URL);
+ $schema = Schema::fromRequestParameter($param);
+ $json = $schema->toJSON();
+ $this->assertEquals('uri', $json->get('format'));
+ }
+
+ public function testFromRequestParameterWithMinMax() {
+ $param = new RequestParameter('age', ParamType::INT);
+ $param->setMinValue(0);
+ $param->setMaxValue(120);
+ $schema = Schema::fromRequestParameter($param);
+ $json = $schema->toJSON();
+ $this->assertEquals(0, $json->get('minimum'));
+ $this->assertEquals(120, $json->get('maximum'));
+ }
+
+ public function testFromRequestParameterWithLength() {
+ $param = new RequestParameter('name', ParamType::STRING);
+ $param->setMinLength(2);
+ $param->setMaxLength(50);
+ $schema = Schema::fromRequestParameter($param);
+ $json = $schema->toJSON();
+ $this->assertEquals(2, $json->get('minLength'));
+ $this->assertEquals(50, $json->get('maxLength'));
+ }
+}
diff --git a/tests/WebFiori/Tests/Http/TestServices/AccessDeniedTestService.php b/tests/WebFiori/Tests/Http/TestServices/AccessDeniedTestService.php
new file mode 100644
index 0000000..4447cbf
--- /dev/null
+++ b/tests/WebFiori/Tests/Http/TestServices/AccessDeniedTestService.php
@@ -0,0 +1,18 @@
+addRequestMethod('GET');
+ }
+
+ public function isAuthorized(): bool {
+ return false;
+ }
+
+ public function processRequest() {
+ }
+}
diff --git a/tests/WebFiori/Tests/Http/TestServices/AnnotatedAuthFailService.php b/tests/WebFiori/Tests/Http/TestServices/AnnotatedAuthFailService.php
new file mode 100644
index 0000000..c614b5a
--- /dev/null
+++ b/tests/WebFiori/Tests/Http/TestServices/AnnotatedAuthFailService.php
@@ -0,0 +1,25 @@
+ 'test'];
+ }
+}
diff --git a/tests/WebFiori/Tests/Http/TestServices/AnnotatedMethodService.php b/tests/WebFiori/Tests/Http/TestServices/AnnotatedMethodService.php
new file mode 100644
index 0000000..85665b9
--- /dev/null
+++ b/tests/WebFiori/Tests/Http/TestServices/AnnotatedMethodService.php
@@ -0,0 +1,27 @@
+ $_GET['id'] ?? 0, 'name' => 'Test Item'];
+ }
+}
diff --git a/tests/WebFiori/Tests/Http/TestServices/AutoDiscoveredService.php b/tests/WebFiori/Tests/Http/TestServices/AutoDiscoveredService.php
new file mode 100644
index 0000000..f277f05
--- /dev/null
+++ b/tests/WebFiori/Tests/Http/TestServices/AutoDiscoveredService.php
@@ -0,0 +1,18 @@
+sendResponse('Auto-discovered service works');
+ }
+}
diff --git a/tests/WebFiori/Tests/Http/TestServices/ConflictingAnnotationsService.php b/tests/WebFiori/Tests/Http/TestServices/ConflictingAnnotationsService.php
new file mode 100644
index 0000000..2bd0d17
--- /dev/null
+++ b/tests/WebFiori/Tests/Http/TestServices/ConflictingAnnotationsService.php
@@ -0,0 +1,27 @@
+ 'test'];
+ }
+}
diff --git a/tests/WebFiori/Tests/Http/TestServices/ExceptionTestService.php b/tests/WebFiori/Tests/Http/TestServices/ExceptionTestService.php
index 42ccd87..e6523fb 100644
--- a/tests/WebFiori/Tests/Http/TestServices/ExceptionTestService.php
+++ b/tests/WebFiori/Tests/Http/TestServices/ExceptionTestService.php
@@ -1,79 +1,43 @@
addRequestMethod('GET');
+ $this->addRequestMethod('POST');
+ }
+
+ public function isAuthorized(): bool {
+ return true;
+ }
+
+ public function processRequest() {
+ }
- #[GetMapping]
#[ResponseBody]
- #[RequestParam('id', 'int')]
- public function getUser(): array {
- $id = $this->getParamVal('id');
-
- // For testing, check if id is set via test
- if (!$id && isset($_GET['test_id'])) {
- $id = (int)$_GET['test_id'];
- }
-
- if ($id === 404) {
+ public function getUser() {
+ $testId = $_GET['test_id'] ?? 0;
+ if ($testId == 404) {
throw new NotFoundException('User not found');
}
-
- if ($id === 400) {
+ if ($testId == 400) {
throw new BadRequestException('Invalid user ID');
}
-
- return ['user' => ['id' => $id, 'name' => 'Test User']];
+ return ['id' => $testId];
}
- #[PostMapping]
- #[ResponseBody]
- public function createUser(): array {
+ public function createUser() {
throw new UnauthorizedException('Authentication required');
}
- #[GetMapping]
- #[ResponseBody]
- public function getError(): array {
+ public function getError() {
throw new \Exception('Generic error');
}
-
- public function isAuthorized(): bool {
- return true;
- }
-
- public function processRequest() {
- $action = $_GET['action'] ?? 'get';
- switch ($action) {
- case 'get':
- $this->getUser();
- break;
- case 'create':
- $this->createUser();
- break;
- case 'error':
- $this->getError();
- break;
- }
- }
-
- protected function getCurrentProcessingMethod(): ?string {
- $action = $_GET['action'] ?? 'get';
- return match($action) {
- 'get' => 'getUser',
- 'create' => 'createUser',
- 'error' => 'getError',
- default => null
- };
- }
}
diff --git a/tests/WebFiori/Tests/Http/TestServices/GenericExceptionMethodService.php b/tests/WebFiori/Tests/Http/TestServices/GenericExceptionMethodService.php
new file mode 100644
index 0000000..7acc6ba
--- /dev/null
+++ b/tests/WebFiori/Tests/Http/TestServices/GenericExceptionMethodService.php
@@ -0,0 +1,25 @@
+addRequestMethod('GET');
+ }
+
+ public function isAuthorized(): bool {
+ return true;
+ }
+
+ public function processRequest() {
+ throw new HttpException('Not found', 404);
+ }
+}
diff --git a/tests/WebFiori/Tests/Http/TestServices/JsonBodyService.php b/tests/WebFiori/Tests/Http/TestServices/JsonBodyService.php
new file mode 100644
index 0000000..280df77
--- /dev/null
+++ b/tests/WebFiori/Tests/Http/TestServices/JsonBodyService.php
@@ -0,0 +1,22 @@
+addRequestMethod('POST');
+ $this->addParameter(new RequestParameter('data', 'string'));
+ }
+
+ public function isAuthorized(): bool {
+ return true;
+ }
+
+ public function processRequest() {
+ $inputs = $this->getInputs();
+ $this->sendResponse('JSON received', 200, 'success', $inputs);
+ }
+}
diff --git a/tests/WebFiori/Tests/Http/TestServices/MethodAuthFailService.php b/tests/WebFiori/Tests/Http/TestServices/MethodAuthFailService.php
new file mode 100644
index 0000000..f859750
--- /dev/null
+++ b/tests/WebFiori/Tests/Http/TestServices/MethodAuthFailService.php
@@ -0,0 +1,25 @@
+ 'test'];
+ }
+}
diff --git a/tests/WebFiori/Tests/Http/TestServices/PatchTestService.php b/tests/WebFiori/Tests/Http/TestServices/PatchTestService.php
new file mode 100644
index 0000000..cd0e46e
--- /dev/null
+++ b/tests/WebFiori/Tests/Http/TestServices/PatchTestService.php
@@ -0,0 +1,22 @@
+addRequestMethod('PATCH');
+ $this->addParameter(new RequestParameter('field', 'string'));
+ }
+
+ public function isAuthorized(): bool {
+ return true;
+ }
+
+ public function processRequest() {
+ $inputs = $this->getInputs();
+ $this->sendResponse('PATCH received', 200, 'success', $inputs);
+ }
+}
diff --git a/tests/WebFiori/Tests/Http/TestServices/PutTestService.php b/tests/WebFiori/Tests/Http/TestServices/PutTestService.php
new file mode 100644
index 0000000..16e8f49
--- /dev/null
+++ b/tests/WebFiori/Tests/Http/TestServices/PutTestService.php
@@ -0,0 +1,23 @@
+addRequestMethod('PUT');
+ $this->addParameter(new RequestParameter('name', 'string'));
+ $this->addParameter(new RequestParameter('value', 'integer'));
+ }
+
+ public function isAuthorized(): bool {
+ return true;
+ }
+
+ public function processRequest() {
+ $inputs = $this->getInputs();
+ $this->sendResponse('PUT received', 200, 'success', $inputs);
+ }
+}
diff --git a/tests/WebFiori/Tests/Http/TestServices/ResponseBodyService.php b/tests/WebFiori/Tests/Http/TestServices/ResponseBodyService.php
new file mode 100644
index 0000000..5738917
--- /dev/null
+++ b/tests/WebFiori/Tests/Http/TestServices/ResponseBodyService.php
@@ -0,0 +1,32 @@
+ 123, 'name' => 'New Resource'];
+ }
+
+ #[GetMapping]
+ #[ResponseBody(contentType: 'text/plain')]
+ public function getPlainText() {
+ return 'Plain text response';
+ }
+}
diff --git a/tests/WebFiori/Tests/Http/TestServices/SecureService.php b/tests/WebFiori/Tests/Http/TestServices/SecureService.php
index 5e98b18..d0ec2d0 100644
--- a/tests/WebFiori/Tests/Http/TestServices/SecureService.php
+++ b/tests/WebFiori/Tests/Http/TestServices/SecureService.php
@@ -39,7 +39,7 @@ public function createUser() {
}
public function isAuthorized(): bool {
- return true; // Default fallback
+ return SecurityContext::isAuthenticated();
}
public function processRequest() {
diff --git a/tests/WebFiori/Tests/Http/TestServices/SimpleExceptionService.php b/tests/WebFiori/Tests/Http/TestServices/SimpleExceptionService.php
new file mode 100644
index 0000000..6393665
--- /dev/null
+++ b/tests/WebFiori/Tests/Http/TestServices/SimpleExceptionService.php
@@ -0,0 +1,19 @@
+addRequestMethod('GET');
+ }
+
+ public function isAuthorized(): bool {
+ return true;
+ }
+
+ public function processRequest() {
+ throw new \Exception('Test exception');
+ }
+}
diff --git a/tests/WebFiori/Tests/Http/TestUser.php b/tests/WebFiori/Tests/Http/TestUser.php
index 51190e4..9d13330 100644
--- a/tests/WebFiori/Tests/Http/TestUser.php
+++ b/tests/WebFiori/Tests/Http/TestUser.php
@@ -1,19 +1,21 @@
id = $id;
+ $this->name = $name;
$this->roles = $roles;
$this->authorities = $authorities;
$this->active = $active;
@@ -23,6 +25,10 @@ public function getId(): int|string {
return $this->id;
}
+ public function getName(): string {
+ return $this->name;
+ }
+
public function getRoles(): array {
return $this->roles;
}
diff --git a/tests/WebFiori/Tests/Http/UriTest.php b/tests/WebFiori/Tests/Http/UriTest.php
index 2a2ba9b..f3d800c 100644
--- a/tests/WebFiori/Tests/Http/UriTest.php
+++ b/tests/WebFiori/Tests/Http/UriTest.php
@@ -370,4 +370,11 @@ public function testSplitURI_16() {
$uri = 'https://programmingacademia.com/{?}/{another}/not/super';
$uriObj = new RequestUri($uri);
}
+
+ public function testInvalidUri() {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('Invalid URI');
+ new Uri('not a valid uri!!!');
+ }
}
+
diff --git a/tests/WebFiori/Tests/Http/WebServiceTest.php b/tests/WebFiori/Tests/Http/WebServiceTest.php
index 535f905..2d0b261 100644
--- a/tests/WebFiori/Tests/Http/WebServiceTest.php
+++ b/tests/WebFiori/Tests/Http/WebServiceTest.php
@@ -413,4 +413,18 @@ public function processRequest() {}
$this->assertContains('DELETE', $methods);
$this->assertCount(4, $methods);
}
+
+ public function testConflictingAnnotations() {
+ $service = new \WebFiori\Tests\Http\TestServices\ConflictingAnnotationsService();
+
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('conflicting annotations');
+
+ $service->checkMethodAuthorization();
+ }
}
+
+
+
+
+
diff --git a/tests/WebFiori/Tests/Http/WebServicesManagerTest.php b/tests/WebFiori/Tests/Http/WebServicesManagerTest.php
index 852fccb..8e747e5 100644
--- a/tests/WebFiori/Tests/Http/WebServicesManagerTest.php
+++ b/tests/WebFiori/Tests/Http/WebServicesManagerTest.php
@@ -674,6 +674,192 @@ private function clrearVars() {
unset($_SERVER['CONTENT_TYPE']);
putenv('REQUEST_METHOD');
}
+
+ public function testAutoDiscoverServices() {
+ $manager = new WebServicesManager();
+ $servicesPath = __DIR__ . '/TestServices';
+ $manager->autoDiscoverServices($servicesPath);
+
+ $services = $manager->getServices();
+ $this->assertGreaterThan(0, count($services));
+
+ $found = false;
+ foreach ($services as $service) {
+ if ($service->getName() === 'auto-discovered') {
+ $found = true;
+ break;
+ }
+ }
+ $this->assertTrue($found, 'AutoDiscoveredService should be found');
+ }
+
+ public function testGetResponse() {
+ $manager = new WebServicesManager();
+ $response = $manager->getResponse();
+ $this->assertNotNull($response);
+ }
+
+ public function testSetAndGetBasePath() {
+ $manager = new WebServicesManager();
+ $manager->setBasePath('/api/v1/');
+ $this->assertEquals('/api/v1', $manager->getBasePath());
+
+ $manager->setBasePath('/api/v2');
+ $this->assertEquals('/api/v2', $manager->getBasePath());
+ }
+
+ public function testRemoveServices() {
+ $manager = new WebServicesManager();
+ $manager->addService(new NoAuthService());
+ $this->assertGreaterThan(0, count($manager->getServices()));
+
+ $manager->removeServices();
+ $this->assertEquals(0, count($manager->getServices()));
+ }
+
+ public function testSendWithOutputStream() {
+ $manager = new WebServicesManager();
+ $outputFile = __DIR__ . '/test-output.txt';
+ $manager->setOutputStream(fopen($outputFile, 'w'));
+
+ $manager->send('text/plain', 'Test content', 200);
+
+ $this->assertFileExists($outputFile);
+ $content = file_get_contents($outputFile);
+ $this->assertEquals('Test content', $content);
+
+ unlink($outputFile);
+ }
+
+ public function testAutoDiscoverServicesWithNullPath() {
+ $manager = new WebServicesManager();
+ $result = $manager->autoDiscoverServices();
+ $this->assertInstanceOf(WebServicesManager::class, $result);
+ }
+
+ public function testToJSON() {
+ $manager = new WebServicesManager();
+ $manager->setVersion('1.0.0');
+ $manager->setDescription('Test API');
+ $manager->addService(new NoAuthService());
+
+ $json = $manager->toJSON();
+ $this->assertNotNull($json);
+ $this->assertEquals('1.0.0', $json->get('api-version'));
+ $this->assertEquals('Test API', $json->get('description'));
+ }
+
+ public function testToJSONWithVersion() {
+ $manager = new WebServicesManager();
+ $manager->setVersion('2.0.0');
+ $service = new NoAuthService();
+ $service->setSince('2.0.0');
+ $manager->addService($service);
+
+ $_GET['version'] = '2.0.0';
+ $json = $manager->toJSON();
+ $services = $json->get('services');
+ $this->assertNotEmpty($services);
+ unset($_GET['version']);
+ }
+
+ public function testToOpenAPI() {
+ $manager = new WebServicesManager();
+ $manager->setVersion('1.0.0');
+ $manager->setDescription('Test API');
+ $manager->addService(new NoAuthService());
+
+ $openapi = $manager->toOpenAPI();
+ $this->assertInstanceOf(\WebFiori\Http\OpenAPI\OpenAPIObj::class, $openapi);
+ }
+
+ public function testPutRequest() {
+ $manager = new WebServicesManager();
+ $manager->addService(new \WebFiori\Tests\Http\TestServices\PutTestService());
+
+ $response = $this->putRequest($manager, 'put-test', [
+ 'name' => 'test',
+ 'value' => 123
+ ]);
+
+ $this->assertStringContainsString('PUT received', $response);
+ }
+
+ public function testPatchRequest() {
+ $manager = new WebServicesManager();
+ $manager->addService(new \WebFiori\Tests\Http\TestServices\PatchTestService());
+
+ $response = $this->patchRequest($manager, 'patch-test', [
+ 'field' => 'updated'
+ ]);
+
+ $this->assertStringContainsString('PATCH received', $response);
+ }
+
+ public function testToJSONWithVersionFilter() {
+ $manager = new WebServicesManager();
+ $manager->setVersion('2.0.0');
+
+ $service1 = new NoAuthService();
+ $service1->setSince('1.0.0');
+ $manager->addService($service1);
+
+ $service2 = new \WebFiori\Tests\Http\TestServices\PutTestService();
+ $service2->setSince('2.0.0');
+ $manager->addService($service2);
+
+ $_POST['version'] = '2.0.0';
+ $json = $manager->toJSON();
+ $services = $json->get('services');
+ $this->assertGreaterThan(0, count($services));
+ unset($_POST['version']);
+ }
+
+ public function testAnnotatedMethodService() {
+ $manager = new WebServicesManager();
+ $manager->addService(new \WebFiori\Tests\Http\TestServices\AnnotatedMethodService());
+
+ $response = $this->getRequest($manager, 'annotated-method', ['id' => 123]);
+
+ $this->assertStringContainsString('Test Item', $response);
+ }
+
+ public function testJsonBodyRequest() {
+ $manager = new WebServicesManager();
+ $manager->addService(new \WebFiori\Tests\Http\TestServices\JsonBodyService());
+
+ $response = $this->postRequest($manager, 'json-body', ['data' => 'test']);
+
+ $this->assertStringContainsString('JSON received', $response);
+ }
+
+ public function testMethodAuthorizationFailure() {
+ $manager = new WebServicesManager();
+ $manager->addService(new \WebFiori\Tests\Http\TestServices\MethodAuthFailService());
+
+ $response = $this->getRequest($manager, 'method-auth-fail');
+
+ $this->assertStringContainsString('http-code', $response);
+ }
+
+ public function testHttpExceptionInMethod() {
+ $manager = new WebServicesManager();
+ $manager->addService(new \WebFiori\Tests\Http\TestServices\HttpExceptionMethodService());
+
+ $response = $this->getRequest($manager, 'http-exception-method');
+
+ $this->assertStringContainsString('404', $response);
+ }
+
+ public function testGenericExceptionInMethod() {
+ $manager = new WebServicesManager();
+ $manager->addService(new \WebFiori\Tests\Http\TestServices\GenericExceptionMethodService());
+
+ $response = $this->getRequest($manager, 'generic-exception-method');
+
+ $this->assertStringContainsString('500', $response);
+ }
+
public static function setTestJson($fName, $jsonData) {
$stream = fopen($fName, 'w+');
fwrite($stream, $jsonData);