From d06f00ed204018fe369c0849801e49926db15e4f Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Mon, 1 Jun 2026 13:53:07 +0300 Subject: [PATCH] feat(#121): create RequestProcessor class - Add RequestProcessor for standalone service processing without a manager - Accepts WebService + optional Request + optional output stream - Handles full pipeline: content type, method check, params, auth, invocation - Internally uses WebServicesManager (single-service auto-select) - Add example in examples/04-advanced/04-request-processor/ - 6 new tests covering GET, POST, auth denial, method not allowed, content type - Step 4 of 5 in ADR-0005 (RequestProcessor refactor) --- WebFiori/Http/RequestProcessor.php | 59 +++++++ .../04-request-processor/README.md | 63 +++++++ .../04-request-processor/index.php | 40 +++++ .../Tests/Http/RequestProcessorTest.php | 162 ++++++++++++++++++ 4 files changed, 324 insertions(+) create mode 100644 WebFiori/Http/RequestProcessor.php create mode 100644 examples/04-advanced/04-request-processor/README.md create mode 100644 examples/04-advanced/04-request-processor/index.php create mode 100644 tests/WebFiori/Tests/Http/RequestProcessorTest.php diff --git a/WebFiori/Http/RequestProcessor.php b/WebFiori/Http/RequestProcessor.php new file mode 100644 index 0000000..6911d25 --- /dev/null +++ b/WebFiori/Http/RequestProcessor.php @@ -0,0 +1,59 @@ +process(new MyService(), Request::createFromGlobals()); + * ``` + * + * @author Ibrahim + */ +class RequestProcessor { + /** + * Process a request against a specific web service. + * + * The processor runs the full pipeline: + * 1. Content type validation + * 2. HTTP method matching + * 3. Parameter filtering and validation + * 4. Authorization check + * 5. Method invocation + * 6. Response serialization + * + * @param WebService $service The service to process. + * @param Request|null $request The incoming HTTP request. If null, creates from globals. + * @param resource|null $outputStream Optional output stream for testing. + */ + public function process(WebService $service, ?Request $request = null, $outputStream = null) : void { + if ($request === null) { + $request = Request::createFromGlobals(); + } + + $manager = new WebServicesManager($request); + $manager->addService($service); + + if ($outputStream !== null) { + $manager->setOutputStream($outputStream); + } + + $manager->process(); + } +} diff --git a/examples/04-advanced/04-request-processor/README.md b/examples/04-advanced/04-request-processor/README.md new file mode 100644 index 0000000..7801389 --- /dev/null +++ b/examples/04-advanced/04-request-processor/README.md @@ -0,0 +1,63 @@ +# RequestProcessor — Standalone Service Processing + +Demonstrates processing a web service directly without a `WebServicesManager`. + +## What This Example Demonstrates + +- Using `RequestProcessor` to process a single service +- No service registry or manager setup required +- Automatic request creation from globals +- Full pipeline: validation, auth, invocation, serialization + +## Files + +- [`index.php`](index.php) - Processes a service directly with RequestProcessor + +## How to Run + +```bash +php -S localhost:8080 +``` + +## Testing + +```bash +# GET request +curl "http://localhost:8080?name=Ibrahim" + +# GET without param (uses default) +curl "http://localhost:8080" + +# POST request +curl -X POST http://localhost:8080 \ + -d "to=Alice&body=Hi there" +``` + +## Code Explanation + +### Before (WebServicesManager) + +```php +$manager = new WebServicesManager(); +$manager->addService(new GreetService()); +$manager->process(); +``` + +### After (RequestProcessor) + +```php +$processor = new RequestProcessor(); +$processor->process(new GreetService()); +``` + +The `RequestProcessor` is ideal when: +- You have a router that already resolved which service to call +- You want to process a single service without registry overhead +- You're building framework integrations that handle routing externally + +### With explicit Request (for testing) + +```php +$processor = new RequestProcessor(); +$processor->process(new GreetService(), $request, $outputStream); +``` diff --git a/examples/04-advanced/04-request-processor/index.php b/examples/04-advanced/04-request-processor/index.php new file mode 100644 index 0000000..f692c69 --- /dev/null +++ b/examples/04-advanced/04-request-processor/index.php @@ -0,0 +1,40 @@ + 'Hello, ' . ($name ?? 'World') . '!']; + } + + #[PostMapping] + #[ResponseBody] + #[AllowAnonymous] + #[RequestParam('to', ParamType::STRING)] + #[RequestParam('body', ParamType::STRING)] + public function sendGreeting(string $to, string $body): array { + return ['sent_to' => $to, 'body' => $body, 'timestamp' => time()]; + } + + public function isAuthorized(): bool { return true; } + public function processRequest() {} +} + +// Process directly — no WebServicesManager needed +$processor = new RequestProcessor(); +$processor->process(new GreetService()); diff --git a/tests/WebFiori/Tests/Http/RequestProcessorTest.php b/tests/WebFiori/Tests/Http/RequestProcessorTest.php new file mode 100644 index 0000000..40a0eca --- /dev/null +++ b/tests/WebFiori/Tests/Http/RequestProcessorTest.php @@ -0,0 +1,162 @@ + 'hello']; + $_SERVER['CONTENT_TYPE'] = ''; + + $outFile = $this->getOutputFile(); + $stream = fopen($outFile, 'w'); + $request = Request::createFromGlobals(); + + $processor->process($service, $request, $stream); + + $output = file_get_contents($outFile); + @unlink($outFile); + + $this->assertNotEmpty($output); + } + + /** + * Test processing a POST request with parameter validation. + */ + public function testProcessPostWithParams() { + $processor = new RequestProcessor(); + $service = new AllMethodsService(); + + putenv('REQUEST_METHOD=POST'); + $_POST = ['name' => 'John']; + $_SERVER['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'; + + $outFile = $this->getOutputFile(); + $stream = fopen($outFile, 'w'); + $request = Request::createFromGlobals(); + + $processor->process($service, $request, $stream); + + $output = file_get_contents($outFile); + @unlink($outFile); + + $this->assertNotEmpty($output); + } + + /** + * Test that unauthorized service returns 401. + */ + public function testUnauthorizedReturnsError() { + $processor = new RequestProcessor(); + $service = new StringAuthDenialService(); + + putenv('REQUEST_METHOD=GET'); + $_GET = []; + $_SERVER['CONTENT_TYPE'] = ''; + + $outFile = $this->getOutputFile(); + $stream = fopen($outFile, 'w'); + $request = Request::createFromGlobals(); + + $processor->process($service, $request, $stream); + + $output = file_get_contents($outFile); + @unlink($outFile); + + $response = json_decode($output, true); + $this->assertIsArray($response); + $this->assertEquals('error', $response['type']); + $this->assertEquals(401, $response['http-code']); + $this->assertEquals('You must be a premium member to access this resource.', $response['message']); + } + + /** + * Test that wrong HTTP method returns 405. + */ + public function testMethodNotAllowed() { + $processor = new RequestProcessor(); + $service = new AnnotatedMethodService(); + + putenv('REQUEST_METHOD=DELETE'); + $_GET = []; + $_SERVER['CONTENT_TYPE'] = ''; + + $outFile = $this->getOutputFile(); + $stream = fopen($outFile, 'w'); + $request = Request::createFromGlobals(); + + $processor->process($service, $request, $stream); + + $output = file_get_contents($outFile); + @unlink($outFile); + + $response = json_decode($output, true); + $this->assertIsArray($response); + $this->assertEquals('error', $response['type']); + } + + /** + * Test that unsupported content type returns 415. + */ + public function testContentTypeNotSupported() { + $processor = new RequestProcessor(); + $service = new AllMethodsService(); + + putenv('REQUEST_METHOD=POST'); + $_POST = []; + $_SERVER['CONTENT_TYPE'] = 'text/xml'; + + $outFile = $this->getOutputFile(); + $stream = fopen($outFile, 'w'); + $request = Request::createFromGlobals(); + + $processor->process($service, $request, $stream); + + $output = file_get_contents($outFile); + @unlink($outFile); + + $response = json_decode($output, true); + $this->assertIsArray($response); + $this->assertEquals('error', $response['type']); + } + + /** + * Test processing with null request (creates from globals). + */ + public function testProcessWithNullRequest() { + $processor = new RequestProcessor(); + $service = new AnnotatedMethodService(); + + putenv('REQUEST_METHOD=GET'); + $_GET = ['param1' => 'test']; + $_SERVER['CONTENT_TYPE'] = ''; + + $outFile = $this->getOutputFile(); + $stream = fopen($outFile, 'w'); + + $processor->process($service, null, $stream); + + $output = file_get_contents($outFile); + @unlink($outFile); + + $this->assertNotEmpty($output); + } +}