From b9ce95e680557579a21f3fd2d4b12ad39b259afd Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Tue, 5 May 2026 12:40:38 +0300 Subject: [PATCH] docs: add ResponseEntity and positional param injection documentation - Add 'Dynamic Status Codes with ResponseEntity' section to README - Add 'Positional Parameter Injection' section to README - Update table of contents in README - Create annotations example (03-annotations/01-rest-controller) with TaskService demonstrating ResponseEntity and hyphenated params --- README.md | 58 +++++++++++++ .../01-rest-controller/README.md | 71 ++++++++++++++++ .../01-rest-controller/TaskService.php | 82 +++++++++++++++++++ .../01-rest-controller/index.php | 10 +++ 4 files changed, 221 insertions(+) create mode 100644 examples/03-annotations/01-rest-controller/README.md create mode 100644 examples/03-annotations/01-rest-controller/TaskService.php create mode 100644 examples/03-annotations/01-rest-controller/index.php diff --git a/README.md b/README.md index f2e27f1f..a3d94287 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ A powerful and flexible PHP library for creating RESTful web APIs with built-in - [Using Attributes (Recommended)](#using-attributes-recommended) - [Traditional Class-Based Approach](#traditional-class-based-approach) - [Parameter Management](#parameter-management) +- [Dynamic Status Codes with ResponseEntity](#dynamic-status-codes-with-responseentity) - [Testing](#testing) - [Examples](#examples) @@ -386,6 +387,63 @@ public function processRequest() { } ``` +### Positional Parameter Injection + +When using `#[ResponseBody]`, method parameters are matched **positionally** to `#[RequestParam]` attributes. The PHP variable names do not need to match the request parameter names: + +```php +#[GetMapping] +#[ResponseBody] +#[AllowAnonymous] +#[RequestParam('app-id', ParamType::INT)] +#[RequestParam('user-name', ParamType::STRING, true)] +public function getData(int $id, ?string $name): array { + // $id receives the value of 'app-id' (1st attribute → 1st parameter) + // $name receives the value of 'user-name' (2nd attribute → 2nd parameter) + return ['id' => $id, 'name' => $name]; +} +``` + +## Dynamic Status Codes with ResponseEntity + +The `ResponseEntity` class allows `#[ResponseBody]` methods to return different HTTP status codes based on runtime logic: + +```php +use WebFiori\Http\ResponseEntity; +use WebFiori\Json\Json; + +#[PostMapping] +#[ResponseBody] +#[AllowAnonymous] +#[RequestParam('username', ParamType::STRING)] +#[RequestParam('password', ParamType::STRING)] +public function login(string $username, string $password): ResponseEntity { + if ($username === 'admin' && $password === 'secret') { + return ResponseEntity::ok(new Json(['token' => 'abc123'])); + } + return ResponseEntity::unauthorized(new Json(['message' => 'Invalid credentials'])); +} +``` + +### Available Factory Methods + +| Method | Status Code | Use Case | +|:-------|:------------|:---------| +| `ResponseEntity::ok($body)` | 200 | Successful response | +| `ResponseEntity::created($body)` | 201 | Resource created | +| `ResponseEntity::noContent()` | 204 | Successful deletion | +| `ResponseEntity::badRequest($body)` | 400 | Invalid input | +| `ResponseEntity::unauthorized($body)` | 401 | Authentication failure | +| `ResponseEntity::forbidden($body)` | 403 | Authorization failure | +| `ResponseEntity::notFound($body)` | 404 | Resource not found | +| `ResponseEntity::error($body)` | 500 | Server error | + +You can also use the constructor directly for custom status codes: + +```php +return new ResponseEntity($body, 418, 'text/plain'); +``` + ## Testing ### Using APITestCase diff --git a/examples/03-annotations/01-rest-controller/README.md b/examples/03-annotations/01-rest-controller/README.md new file mode 100644 index 00000000..ab20bfb2 --- /dev/null +++ b/examples/03-annotations/01-rest-controller/README.md @@ -0,0 +1,71 @@ +# REST Controller with Annotations + +Demonstrates the modern annotation-based approach to building REST APIs, including parameter injection, dynamic status codes with `ResponseEntity`, and hyphenated parameter names. + +## What This Example Demonstrates + +- `#[RestController]` for service naming and description +- `#[GetMapping]`, `#[PostMapping]`, `#[DeleteMapping]` for HTTP method routing +- `#[ResponseBody]` for automatic return value serialization +- `#[RequestParam]` with positional parameter injection +- `ResponseEntity` for dynamic HTTP status codes +- Hyphenated parameter names with arbitrary PHP variable names + +## Files + +- [`TaskService.php`](TaskService.php) - Complete CRUD service with `ResponseEntity` +- [`index.php`](index.php) - Application entry point + +## How to Run + +```bash +php -S localhost:8080 +``` + +## Testing + +```bash +# List all tasks +curl "http://localhost:8080?service=tasks" + +# Get a specific task +curl "http://localhost:8080?service=tasks&task-id=1" + +# Create a task +curl -X POST "http://localhost:8080?service=tasks" \ + -d "task-name=Buy groceries&task-priority=high" + +# Delete a task +curl -X DELETE "http://localhost:8080?service=tasks&task-id=1" + +# Try to get a non-existent task (returns 404) +curl "http://localhost:8080?service=tasks&task-id=999" +``` + +## Code Explanation + +### Positional Parameter Injection + +Method parameters are matched by position to `#[RequestParam]` attributes, not by name. This allows hyphenated request parameter names (WebFiori convention) with clean PHP variable names: + +```php +#[RequestParam('task-id', ParamType::INT)] +#[RequestParam('task-name', ParamType::STRING, true)] +public function getTask(int $id, ?string $name): ResponseEntity { + // $id ← value of 'task-id' (1st attribute → 1st param) + // $name ← value of 'task-name' (2nd attribute → 2nd param) +} +``` + +### Dynamic Status Codes + +`ResponseEntity` lets you return different HTTP status codes from the same method: + +```php +public function getTask(int $id): ResponseEntity { + if ($id === 999) { + return ResponseEntity::notFound(new Json(['message' => 'Not found'])); + } + return ResponseEntity::ok(new Json(['id' => $id])); +} +``` diff --git a/examples/03-annotations/01-rest-controller/TaskService.php b/examples/03-annotations/01-rest-controller/TaskService.php new file mode 100644 index 00000000..77144aa2 --- /dev/null +++ b/examples/03-annotations/01-rest-controller/TaskService.php @@ -0,0 +1,82 @@ + ['id' => 1, 'name' => 'Write documentation', 'priority' => 'high'], + 2 => ['id' => 2, 'name' => 'Fix bugs', 'priority' => 'medium'], + 3 => ['id' => 3, 'name' => 'Add tests', 'priority' => 'low'], + ]; + + #[GetMapping] + #[ResponseBody] + #[AllowAnonymous] + #[RequestParam('task-id', ParamType::INT, true)] + public function getTask(?int $id): ResponseEntity { + if ($id === null) { + // Return all tasks + return ResponseEntity::ok(new Json(['tasks' => array_values($this->tasks)])); + } + + if (!isset($this->tasks[$id])) { + return ResponseEntity::notFound(new Json(['message' => "Task $id not found"])); + } + + return ResponseEntity::ok(new Json($this->tasks[$id])); + } + + #[PostMapping] + #[ResponseBody] + #[AllowAnonymous] + #[RequestParam('task-name', ParamType::STRING)] + #[RequestParam('task-priority', ParamType::STRING, true)] + public function createTask(string $name, ?string $priority): ResponseEntity { + $newId = max(array_keys($this->tasks)) + 1; + $task = [ + 'id' => $newId, + 'name' => $name, + 'priority' => $priority ?? 'medium', + ]; + + return ResponseEntity::created(new Json($task)); + } + + #[DeleteMapping] + #[ResponseBody] + #[AllowAnonymous] + #[RequestParam('task-id', ParamType::INT)] + public function deleteTask(int $id): ResponseEntity { + if (!isset($this->tasks[$id])) { + return ResponseEntity::notFound(new Json(['message' => "Task $id not found"])); + } + + return ResponseEntity::noContent(); + } + + public function isAuthorized(): bool { + return true; + } + + public function processRequest() { + } +} diff --git a/examples/03-annotations/01-rest-controller/index.php b/examples/03-annotations/01-rest-controller/index.php new file mode 100644 index 00000000..787addd1 --- /dev/null +++ b/examples/03-annotations/01-rest-controller/index.php @@ -0,0 +1,10 @@ +addService(new TaskService()); +$manager->process();