Skip to content

Commit b9ce95e

Browse files
author
Ibrahim BinAlshikh
committed
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
1 parent 09162ce commit b9ce95e

4 files changed

Lines changed: 221 additions & 0 deletions

File tree

README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ A powerful and flexible PHP library for creating RESTful web APIs with built-in
3333
- [Using Attributes (Recommended)](#using-attributes-recommended)
3434
- [Traditional Class-Based Approach](#traditional-class-based-approach)
3535
- [Parameter Management](#parameter-management)
36+
- [Dynamic Status Codes with ResponseEntity](#dynamic-status-codes-with-responseentity)
3637

3738
- [Testing](#testing)
3839
- [Examples](#examples)
@@ -386,6 +387,63 @@ public function processRequest() {
386387
}
387388
```
388389

390+
### Positional Parameter Injection
391+
392+
When using `#[ResponseBody]`, method parameters are matched **positionally** to `#[RequestParam]` attributes. The PHP variable names do not need to match the request parameter names:
393+
394+
```php
395+
#[GetMapping]
396+
#[ResponseBody]
397+
#[AllowAnonymous]
398+
#[RequestParam('app-id', ParamType::INT)]
399+
#[RequestParam('user-name', ParamType::STRING, true)]
400+
public function getData(int $id, ?string $name): array {
401+
// $id receives the value of 'app-id' (1st attribute → 1st parameter)
402+
// $name receives the value of 'user-name' (2nd attribute → 2nd parameter)
403+
return ['id' => $id, 'name' => $name];
404+
}
405+
```
406+
407+
## Dynamic Status Codes with ResponseEntity
408+
409+
The `ResponseEntity` class allows `#[ResponseBody]` methods to return different HTTP status codes based on runtime logic:
410+
411+
```php
412+
use WebFiori\Http\ResponseEntity;
413+
use WebFiori\Json\Json;
414+
415+
#[PostMapping]
416+
#[ResponseBody]
417+
#[AllowAnonymous]
418+
#[RequestParam('username', ParamType::STRING)]
419+
#[RequestParam('password', ParamType::STRING)]
420+
public function login(string $username, string $password): ResponseEntity {
421+
if ($username === 'admin' && $password === 'secret') {
422+
return ResponseEntity::ok(new Json(['token' => 'abc123']));
423+
}
424+
return ResponseEntity::unauthorized(new Json(['message' => 'Invalid credentials']));
425+
}
426+
```
427+
428+
### Available Factory Methods
429+
430+
| Method | Status Code | Use Case |
431+
|:-------|:------------|:---------|
432+
| `ResponseEntity::ok($body)` | 200 | Successful response |
433+
| `ResponseEntity::created($body)` | 201 | Resource created |
434+
| `ResponseEntity::noContent()` | 204 | Successful deletion |
435+
| `ResponseEntity::badRequest($body)` | 400 | Invalid input |
436+
| `ResponseEntity::unauthorized($body)` | 401 | Authentication failure |
437+
| `ResponseEntity::forbidden($body)` | 403 | Authorization failure |
438+
| `ResponseEntity::notFound($body)` | 404 | Resource not found |
439+
| `ResponseEntity::error($body)` | 500 | Server error |
440+
441+
You can also use the constructor directly for custom status codes:
442+
443+
```php
444+
return new ResponseEntity($body, 418, 'text/plain');
445+
```
446+
389447
## Testing
390448

391449
### Using APITestCase
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# REST Controller with Annotations
2+
3+
Demonstrates the modern annotation-based approach to building REST APIs, including parameter injection, dynamic status codes with `ResponseEntity`, and hyphenated parameter names.
4+
5+
## What This Example Demonstrates
6+
7+
- `#[RestController]` for service naming and description
8+
- `#[GetMapping]`, `#[PostMapping]`, `#[DeleteMapping]` for HTTP method routing
9+
- `#[ResponseBody]` for automatic return value serialization
10+
- `#[RequestParam]` with positional parameter injection
11+
- `ResponseEntity` for dynamic HTTP status codes
12+
- Hyphenated parameter names with arbitrary PHP variable names
13+
14+
## Files
15+
16+
- [`TaskService.php`](TaskService.php) - Complete CRUD service with `ResponseEntity`
17+
- [`index.php`](index.php) - Application entry point
18+
19+
## How to Run
20+
21+
```bash
22+
php -S localhost:8080
23+
```
24+
25+
## Testing
26+
27+
```bash
28+
# List all tasks
29+
curl "http://localhost:8080?service=tasks"
30+
31+
# Get a specific task
32+
curl "http://localhost:8080?service=tasks&task-id=1"
33+
34+
# Create a task
35+
curl -X POST "http://localhost:8080?service=tasks" \
36+
-d "task-name=Buy groceries&task-priority=high"
37+
38+
# Delete a task
39+
curl -X DELETE "http://localhost:8080?service=tasks&task-id=1"
40+
41+
# Try to get a non-existent task (returns 404)
42+
curl "http://localhost:8080?service=tasks&task-id=999"
43+
```
44+
45+
## Code Explanation
46+
47+
### Positional Parameter Injection
48+
49+
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:
50+
51+
```php
52+
#[RequestParam('task-id', ParamType::INT)]
53+
#[RequestParam('task-name', ParamType::STRING, true)]
54+
public function getTask(int $id, ?string $name): ResponseEntity {
55+
// $id ← value of 'task-id' (1st attribute → 1st param)
56+
// $name ← value of 'task-name' (2nd attribute → 2nd param)
57+
}
58+
```
59+
60+
### Dynamic Status Codes
61+
62+
`ResponseEntity` lets you return different HTTP status codes from the same method:
63+
64+
```php
65+
public function getTask(int $id): ResponseEntity {
66+
if ($id === 999) {
67+
return ResponseEntity::notFound(new Json(['message' => 'Not found']));
68+
}
69+
return ResponseEntity::ok(new Json(['id' => $id]));
70+
}
71+
```
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
require_once '../../../vendor/autoload.php';
4+
5+
use WebFiori\Http\Annotations\AllowAnonymous;
6+
use WebFiori\Http\Annotations\DeleteMapping;
7+
use WebFiori\Http\Annotations\GetMapping;
8+
use WebFiori\Http\Annotations\PostMapping;
9+
use WebFiori\Http\Annotations\RequestParam;
10+
use WebFiori\Http\Annotations\ResponseBody;
11+
use WebFiori\Http\Annotations\RestController;
12+
use WebFiori\Http\ParamType;
13+
use WebFiori\Http\ResponseEntity;
14+
use WebFiori\Http\WebService;
15+
use WebFiori\Json\Json;
16+
17+
/**
18+
* A task management service demonstrating:
19+
* - Positional parameter injection with hyphenated names
20+
* - Dynamic HTTP status codes via ResponseEntity
21+
*/
22+
#[RestController('tasks', 'Task management service')]
23+
class TaskService extends WebService {
24+
25+
private array $tasks = [
26+
1 => ['id' => 1, 'name' => 'Write documentation', 'priority' => 'high'],
27+
2 => ['id' => 2, 'name' => 'Fix bugs', 'priority' => 'medium'],
28+
3 => ['id' => 3, 'name' => 'Add tests', 'priority' => 'low'],
29+
];
30+
31+
#[GetMapping]
32+
#[ResponseBody]
33+
#[AllowAnonymous]
34+
#[RequestParam('task-id', ParamType::INT, true)]
35+
public function getTask(?int $id): ResponseEntity {
36+
if ($id === null) {
37+
// Return all tasks
38+
return ResponseEntity::ok(new Json(['tasks' => array_values($this->tasks)]));
39+
}
40+
41+
if (!isset($this->tasks[$id])) {
42+
return ResponseEntity::notFound(new Json(['message' => "Task $id not found"]));
43+
}
44+
45+
return ResponseEntity::ok(new Json($this->tasks[$id]));
46+
}
47+
48+
#[PostMapping]
49+
#[ResponseBody]
50+
#[AllowAnonymous]
51+
#[RequestParam('task-name', ParamType::STRING)]
52+
#[RequestParam('task-priority', ParamType::STRING, true)]
53+
public function createTask(string $name, ?string $priority): ResponseEntity {
54+
$newId = max(array_keys($this->tasks)) + 1;
55+
$task = [
56+
'id' => $newId,
57+
'name' => $name,
58+
'priority' => $priority ?? 'medium',
59+
];
60+
61+
return ResponseEntity::created(new Json($task));
62+
}
63+
64+
#[DeleteMapping]
65+
#[ResponseBody]
66+
#[AllowAnonymous]
67+
#[RequestParam('task-id', ParamType::INT)]
68+
public function deleteTask(int $id): ResponseEntity {
69+
if (!isset($this->tasks[$id])) {
70+
return ResponseEntity::notFound(new Json(['message' => "Task $id not found"]));
71+
}
72+
73+
return ResponseEntity::noContent();
74+
}
75+
76+
public function isAuthorized(): bool {
77+
return true;
78+
}
79+
80+
public function processRequest() {
81+
}
82+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
require_once '../../../vendor/autoload.php';
4+
require_once 'TaskService.php';
5+
6+
use WebFiori\Http\WebServicesManager;
7+
8+
$manager = new WebServicesManager();
9+
$manager->addService(new TaskService());
10+
$manager->process();

0 commit comments

Comments
 (0)