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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
71 changes: 71 additions & 0 deletions examples/03-annotations/01-rest-controller/README.md
Original file line number Diff line number Diff line change
@@ -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]));
}
```
82 changes: 82 additions & 0 deletions examples/03-annotations/01-rest-controller/TaskService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

require_once '../../../vendor/autoload.php';

use WebFiori\Http\Annotations\AllowAnonymous;
use WebFiori\Http\Annotations\DeleteMapping;
use WebFiori\Http\Annotations\GetMapping;
use WebFiori\Http\Annotations\PostMapping;
use WebFiori\Http\Annotations\RequestParam;
use WebFiori\Http\Annotations\ResponseBody;
use WebFiori\Http\Annotations\RestController;
use WebFiori\Http\ParamType;
use WebFiori\Http\ResponseEntity;
use WebFiori\Http\WebService;
use WebFiori\Json\Json;

/**
* A task management service demonstrating:
* - Positional parameter injection with hyphenated names
* - Dynamic HTTP status codes via ResponseEntity
*/
#[RestController('tasks', 'Task management service')]
class TaskService extends WebService {

private array $tasks = [
1 => ['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() {
}
}
10 changes: 10 additions & 0 deletions examples/03-annotations/01-rest-controller/index.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

require_once '../../../vendor/autoload.php';
require_once 'TaskService.php';

use WebFiori\Http\WebServicesManager;

$manager = new WebServicesManager();
$manager->addService(new TaskService());
$manager->process();
Loading