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):

  1. Referer — if internal (same host or relative /...)
  2. SAFE_BACK_URL — request attribute from StorePreviousUrlMiddleware
  3. 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:

ConstantCodeTypical use
OK200Successful response
CREATED201Resource created
NO_CONTENT204Success without body
FOUND302Redirect (default)
NOT_FOUND404Resource not found
UNPROCESSABLE_ENTITY422FormRequest validation
PAGE_EXPIRED419CSRF (CsrfException)
INTERNAL_SERVER_ERROR500Unhandled 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:

  • ValidationHandleValidationExceptionMiddleware: JSON 422 or back()
  • CSRFCsrfException (code 419), handled by the error handler
  • Production / API errors — Whoops + jsonError() per RequestFormat

Do not use echo or header() from a controller — only ResponseInterface. Otherwise the emitter and middleware (e.g. DebugBar) cannot process the response correctly.