Responses
Every controller action returns Psr\Http\Message\ResponseInterface (PSR-7).
After Router::dispatch(), the core invokes SapiEmitter (Laminas) —
headers and body are sent to the PHP SAPI (FPM, Apache, built-in server).
For accessing the current request anywhere in the application, the RequestProxy service can be used.
See also: controllers, request lifecycle → response, Views (Twig).
Two factories
| Interface | Class | Purpose |
|---|---|---|
ResponseFactoryInterface |
Concept\Core\Http\ResponseFactory |
JSON, redirect, empty response, back() |
ViewResponseFactoryInterface |
Concept\Core\Services\View\ViewResponseFactory |
HTML via Twig |
Both are registered in HttpServiceProvider. In a controller — via constructor (autowiring):
public function __construct(
private readonly ResponseFactoryInterface $response,
private readonly ViewResponseFactoryInterface $viewResponse,
) {}
HTML (Twig)
return $this->viewResponse->create('@cabinet/index', [
'user' => $user,
'projects' => $projects,
]);
@cabinet/index— logical template name (component prefix)- Second argument — local variables for this action
- Third argument — HTTP status code (default
200)
ViewResponseFactory merges data from the request attribute
Concept\Core\Http\Requests\RequestAttribute::VIEW_PAYLOAD (flash, errors, old, CSRF
from ShareViewDataMiddleware), then renders Twig
and sets Content-Type: text/html.
For more on templates — Views.
JSON
Arbitrary JSON — json()
return $this->response->json([
'token' => bin2hex(random_bytes(32)),
]);
Body — your array as-is; default status 200.
Consistent success — jsonSuccess()
return $this->response->jsonSuccess(['id' => $user->getId()]);
// Response (example)
// { "status": "success", "code": 200, "data": { "id": 42 } }
Consistent error — jsonError()
return $this->response->jsonError(
'Resource not found',
HttpStatusCode::NOT_FOUND
);
// With an errors array (e.g. validation)
return $this->response->jsonError(
'Validation failed',
HttpStatusCode::UNPROCESSABLE_ENTITY,
['email' => ['The email field is required.']]
);
// { "status": "error", "code": 422, "message": "...", "errors": { ... } }
The errors key is added only when the array is not empty.
API routes
For JSON requests, attach to a route group (as needed):
ParseJsonBodyMiddleware (body → getParsedBody()),
ForceJsonResponseMiddleware (Accept/Content-Type JSON).
More details — core middleware.
RequestFormat::expectsJson() determines whether the client expects JSON
(Accept: application/json or X-Requested-With: XMLHttpRequest).
The same logic handles validation errors in middleware.
Redirects
redirect($url)
return $this->response->redirect('/cabinet/profile', HttpStatusCode::FOUND);
Hard-coded path or full URL. Default status — 302 Found.
redirectByName()
return $this->response->redirectByName('admin.users');
return $this->response->redirectByName('admin.user.edit', ['id' => $user->getId()]);
URL is built via UrlGeneratorInterface::uri() — relative path for a
named route. Convenient after POST (PRG).
back()
Safe “go back” (not an open redirect):
- Referer — if internal (same host or relative
/...) SAFE_BACK_URL— request attribute fromStorePreviousUrlMiddleware- Fallback — method argument (default
/)
return $this->response->back();
return $this->response->back(HttpStatusCode::FOUND, '/cabinet');
After failed validation, HandleValidationExceptionMiddleware calls
back() for HTML forms (flash + old input).
Empty and custom responses
use Concept\Core\Http\Protocol\HttpStatusCode;
$response = $this->response->createResponse(HttpStatusCode::NO_CONTENT);
// or CREATED, NOT_FOUND, etc. — without a body
createResponse() — from PSR-17; for a custom body, write to
$response->getBody() and add headers via withHeader().
In normal code, the factory methods above are sufficient.
Status codes
Constants in Concept\Core\Http\Protocol\HttpStatusCode:
| Constant | Code | Typical use |
|---|---|---|
OK | 200 | Successful response |
CREATED | 201 | Resource created |
NO_CONTENT | 204 | Success without body |
FOUND | 302 | Redirect (default) |
NOT_FOUND | 404 | Resource not found |
UNPROCESSABLE_ENTITY | 422 | FormRequest validation |
PAGE_EXPIRED | 419 | CSRF (CsrfException) |
INTERNAL_SERVER_ERROR | 500 | Unhandled error |
getReasonPhrase($code) — text for the PSR-7 reason phrase.
Common patterns
POST → redirect (PRG)
public function store(StoreUserRequest $request): ResponseInterface
{
$this->userModel->newQuery()->create(
$this->normalizeUserPayload($request->validated())
);
$this->flashBag->addSuccess('User successfully created');
return $this->response->redirectByName('admin.users');
}
Not found → flash + redirect
if (!($user instanceof UserModel)) {
$this->flashBag->addError('User not found');
return $this->response->redirectByName('admin.users');
}
return $this->viewResponse->create('@auth-admin/users/show', ['user' => $user]);
JSON endpoint
public function generateTokenApi(): ResponseInterface
{
return $this->response->json([
'token' => bin2hex(random_bytes(32)),
]);
}
Responses without a controller
Some responses are produced in middleware or the error handler:
- Validation —
HandleValidationExceptionMiddleware: JSON 422 orback() - CSRF —
CsrfException(code 419), handled by the error handler - Production / API errors — Whoops +
jsonError()perRequestFormat
Do not use echo or header() from a controller — only
ResponseInterface. Otherwise the emitter and middleware (e.g. DebugBar) cannot
process the response correctly.