Building REST Controllers with the Winter Boot Framework
Winter Boot’s #[RestController] attribute transforms an ordinary PHP class into a fully managed HTTP handler. The framework discovers annotated classes automatically through namespace scanning, wires their dependencies, routes incoming requests to the correct method, and serialises the return value into an HTTP response — all without any XML, YAML, or routing-file configuration.
Registering a Controller
Section titled “Registering a Controller”Apply #[RestController] at the class level to register the class as a REST endpoint handler. The class must be concrete (non-abstract), and its namespace must fall within one of the component-scan packages declared in your application configuration.
<?php
namespace dev\winterboot\samples\controller;
use dev\winterframework\stereotype\RestController;
#[RestController]class UserController{ // handler methods go here}Injecting Dependencies
Section titled “Injecting Dependencies”Inject services, repositories, and other beans into your controller with #[Autowired]. Winter Boot resolves and injects the dependency at startup, so there is no manual container look-up at runtime.
<?php
namespace dev\winterboot\samples\controller;
use dev\winterboot\samples\service\UserService;use dev\winterframework\stereotype\Autowired;use dev\winterframework\stereotype\RestController;
#[RestController]class UserController{ #[Autowired] protected UserService $userService;
// $this->userService is fully initialised before any request arrives}Reading the Request
Section titled “Reading the Request”Add an HttpRequest parameter to any handler method and Winter Boot hands you the current request. Use it to read anything the parameter annotations (#[PathVariable], #[RequestParam], #[RequestBody]) do not cover — headers, cookies, the raw body, or details about the caller.
<?php
namespace dev\winterboot\samples\controller;
use dev\winterframework\stereotype\RestController;use dev\winterframework\stereotype\web\GetMapping;use dev\winterframework\web\http\HttpRequest;
#[RestController]class UserController{ // GET /whoami — echo back what the request carried #[GetMapping(path: '/whoami')] public function whoAmI(HttpRequest $req): array { return [ 'method' => $req->getMethod(), // 'GET' 'uri' => $req->getUri(), // '/whoami' 'page' => $req->getQueryParam('page'), 'auth' => $req->getFirstHeader('Authorization'), 'sessionId' => $req->getCookie('SID'), 'contentType' => $req->getContentType(), 'body' => $req->getRawBody(), ]; }}| What you call | What it gives you |
|---|---|
getMethod() |
HTTP method in upper case (GET, POST, …) |
getUri() |
Request path without the query string |
getQueryParam($name) / getQueryParams() |
One (or all) URL query values |
getPostParam($name) / getPostParams() |
One (or all) form fields |
getFirstHeader($name) / getHeader($name) |
First value (or all values) of a request header |
getCookie($name) / getCookies() |
One (or all) request cookies |
getRawBody() |
Request body as plain text |
getContentType() |
Body content type, e.g. application/json |
getFile($name) / getFiles() |
Uploaded files |
Caller and server details
Section titled “Caller and server details”These getters describe who called and where the request landed — the client address for localhost checks or logging, the server address for multi-homed hosts, and timing info. They return null when the runtime did not provide a value.
$ip = $req->getRemoteAddr(); // client IP, e.g. '127.0.0.1'$port = $req->getRemotePort(); // client port$serverIp = $req->getServerAddr(); // your server's IP$protocol = $req->getServerProtocol(); // e.g. 'HTTP/1.1'$query = $req->getQueryString(); // raw 'page=2&sort=name'$when = $req->getRequestTime(); // unix timestampReturn Types
Section titled “Return Types”Controller methods can return three types of value. Winter Boot inspects the return value and sets the appropriate Content-Type header automatically.
| Return type | Behaviour |
|---|---|
array |
Serialised to JSON; Content-Type: application/json |
ResponseEntity |
Full control over status code, headers, and body |
string |
Sent as-is; Content-Type: text/plain |
ResponseEntity Builder API
Section titled “ResponseEntity Builder API”ResponseEntity (namespace dev\winterframework\web\http\ResponseEntity) provides static factory methods to start a response, followed by fluent builder calls to attach a body or headers.
ResponseEntity factory methods
| Method | Description |
|---|---|
ResponseEntity::ok() |
200 OK |
ResponseEntity::created(string $location) |
201 Created, sets Location header |
ResponseEntity::accepted() |
202 Accepted |
ResponseEntity::noContent() |
204 No Content |
ResponseEntity::badRequest() |
400 Bad Request |
ResponseEntity::unauthorized() |
401 Unauthorized |
ResponseEntity::notFound() |
404 Not Found |
ResponseEntity::unprocessableEntity() |
422 Unprocessable Entity |
ResponseEntity::status(HttpStatus $status) |
Any status from HttpStatus |
ResponseEntity builder methods
| Method | Description |
|---|---|
->withJson(mixed $body) |
Set body + Content-Type: application/json |
->withXml(mixed $body) |
Set body + Content-Type: application/xml |
->withHeader(string $name, string $value) |
Append a response header |
->withStatusCode(int $code) |
Override status by numeric code |
->withStatus(HttpStatus $status) |
Override status with an HttpStatus value |
->withContentType(string $type) |
Set Content-Type without touching the body |
->withContentLength(int $bytes) |
Set Content-Length (for file downloads) |
->withCookie(name, value, expires, path, domain, secure, httponly) |
Send a cookie (see below) |
->setBody(mixed $body) |
Set body without changing Content-Type |
use dev\winterframework\web\http\ResponseEntity;use dev\winterframework\web\http\HttpStatus;
// 200 OK with a JSON bodyreturn ResponseEntity::ok()->withJson($user);
// 404 Not Found with no bodyreturn ResponseEntity::notFound();
// 201 Created pointing to the new resourcereturn ResponseEntity::created('/users/42')->withJson($newUser);
// Custom status codereturn ResponseEntity::status(HttpStatus::$CONFLICT)->withJson(['message' => 'Already exists']);Setting cookies
Section titled “Setting cookies”withCookie() sends a cookie with the response. Only name is required — everything else has a safe default:
| Argument | Meaning | Default |
|---|---|---|
name |
Cookie name | (required) |
value |
Cookie value | '' |
expires |
Expiry as a unix timestamp (time() + 3600 = one hour) |
0 (browser-session cookie, deleted on close) |
path |
Which site paths receive the cookie | '' |
domain |
Which domain receives it (empty = current domain) | '' |
secure |
Send over HTTPS only | false |
httponly |
Hide from JavaScript | false |
// Remember a theme for 30 days, visible across the whole sitereturn ResponseEntity::ok() ->withCookie('theme', 'dark', time() + 30 * 24 * 3600, '/') ->withJson(['status' => 'saved']);
// Log out: overwrite with an already-expired cookie so the browser drops itreturn ResponseEntity::ok() ->withCookie('SID', '', time() - 3600, '/') ->withJson(['status' => 'logged-out']);Call it once per cookie — each call adds another Set-Cookie header. Read the cookies a request carried with HttpRequest::getCookie($name) (see Reading the Request).
Complete CRUD Example
Section titled “Complete CRUD Example”The controller below brings together every common pattern: #[Autowired] injection, all five HTTP method annotations, #[PathVariable], #[RequestBody], #[RequestParam], and both array and ResponseEntity return types.
<?php
namespace dev\winterboot\samples\doctrine\controller;
use dev\winterboot\samples\doctrine\model\User;use dev\winterboot\samples\doctrine\service\UserService;use dev\winterframework\stereotype\Autowired;use dev\winterframework\stereotype\RestController;use dev\winterframework\stereotype\web\DeleteMapping;use dev\winterframework\stereotype\web\GetMapping;use dev\winterframework\stereotype\web\PostMapping;use dev\winterframework\stereotype\web\PutMapping;use dev\winterframework\stereotype\web\PathVariable;use dev\winterframework\stereotype\web\RequestBody;use dev\winterframework\stereotype\web\RequestParam;
#[RestController]class UserController{ #[Autowired] protected UserService $userService;
// GET /users — return all users as JSON #[GetMapping(path: '/users')] public function getAllUsers(): array { return [ 'success' => true, 'data' => $this->userService->findAll(), ]; }
// GET /users/{id} — return a single user #[GetMapping(path: '/users/{id}')] public function getUserById(#[PathVariable] int $id): array { $user = $this->userService->findById($id);
if ($user) { return ['success' => true, 'data' => $user]; }
return ['success' => false, 'message' => 'User not found']; }
// POST /users — create a new user from a JSON body #[PostMapping(path: '/users')] public function createUser(#[RequestBody] User $user): array { try { $existing = $this->userService->findByEmail($user->getEmail()); if ($existing) { return [ 'success' => false, 'message' => 'User with email ' . $user->getEmail() . ' already exists', ]; }
$created = $this->userService->createUser($user); return [ 'success' => true, 'data' => $created, 'message' => 'User created successfully', ]; } catch (\Exception $e) { return ['success' => false, 'message' => 'Failed to create user: ' . $e->getMessage()]; } }
// PUT /users/{id} — replace an existing user #[PutMapping(path: '/users/{id}')] public function updateUser(#[PathVariable] int $id, #[RequestBody] User $user): array { try { $existing = $this->userService->findById($id); if (!$existing) { return ['success' => false, 'message' => 'User not found']; }
$user->setId($id); $updated = $this->userService->updateUser($user);
return [ 'success' => true, 'data' => $updated, 'message' => 'User updated successfully', ]; } catch (\Exception $e) { return ['success' => false, 'message' => 'Failed to update user: ' . $e->getMessage()]; } }
// DELETE /users/{id} #[DeleteMapping(path: '/users/{id}')] public function deleteUser(#[PathVariable] int $id): array { try { $deleted = $this->userService->deleteUser($id); if ($deleted) { return ['success' => true, 'message' => 'User deleted successfully']; } return ['success' => false, 'message' => 'User not found']; } catch (\Exception $e) { return ['success' => false, 'message' => 'Failed to delete user: ' . $e->getMessage()]; } }
// GET /users/search?email=... #[GetMapping(path: '/users/search')] public function searchByEmail(#[RequestParam] string $email): array { $user = $this->userService->findByEmail($email);
if ($user) { return ['success' => true, 'data' => $user]; }
return ['success' => false, 'message' => 'User not found']; }}Testing with curl
Section titled “Testing with curl”curl http://localhost/userscurl http://localhost/users/1curl -X POST http://localhost/users \ -H "Content-Type: application/json" \ -d '{"name":"John Doe","email":"john@example.com"}'curl -X PUT http://localhost/users/1 \ -H "Content-Type: application/json" \ -d '{"name":"Jane Doe","email":"jane@example.com"}'curl -X DELETE http://localhost/users/1curl "http://localhost/users/search?email=john@example.com"Error Handling
Section titled “Error Handling”When an unhandled error or exception occurs, Winter Boot delegates to an ErrorController implementation. The ErrorController interface lives in dev\winterframework\core\web\error\ErrorController and exposes a single method:
interface ErrorController{ public function handleError( HttpRequest $request, ResponseEntity $response, HttpStatus $status, ?Throwable $t = null ): void;}Register your implementation under the bean name errorController using either of the two approaches below.
<?php
use dev\winterframework\core\web\error\ErrorController;use dev\winterframework\stereotype\Component;use dev\winterframework\web\http\HttpRequest;use dev\winterframework\web\http\HttpStatus;use dev\winterframework\web\http\ResponseEntity;
#[Component('errorController')]class MyErrorController implements ErrorController{ public function handleError( HttpRequest $request, ResponseEntity $response, HttpStatus $status, ?Throwable $t = null ): void { $response->withStatusCode($status->getValue()) ->withJson([ 'error' => $status->getReasonPhrase(), 'message' => $t?->getMessage(), ]); }}<?php
use dev\winterframework\core\web\error\ErrorController;use dev\winterframework\stereotype\Bean;use dev\winterframework\stereotype\Configuration;
#[Configuration]class AppConfig{ #[Bean('errorController')] public function getErrorController(): ErrorController { return new MyErrorController(); }}