Controllers

Basics

A controller contains the methods that are invoked when a request comes through. Your controllers must extend Controller. Let's say you needed an endpoint to get a user. Simple:

use Aphiria\Api\Controllers\Controller;
use App\Users\IUserService;
use App\Users\User;

final class UserController extends Controller
{
    public function __construct(private IUserService $users) {}

    public function getUser(int $id): User
    {
        return $this->users->getUserById($id);
    }
}

Aphiria will grab the ID from the URI (preference is given to route variables, and then to query string variables). It will also detect that a User object was returned by the method, and create a 200 response whose body is the serialized user object. It uses content negotiation to determine the media type to serialize to (eg JSON).

You can also be a bit more explicit and return a response yourself. For example, the following controller method is functionally identical to the previous example:

final class UserController extends Controller
{
    // ...

    public function getUser(int $id): IResponse
    {
        $user = $this->users->getUserById($id);

        return $this->ok($user);
    }
}

The ok() helper method uses a NegotiatedResponseFactory to build a response using the current request and content negotiation. You can pass in a POPO as the response body, and the factory will use content negotiation to determine how to serialize it.

Similarly, Aphiria can automatically deserialize request bodies.

The following helper methods come bundled with Controller:

Method Status Code
badRequest() 400
conflict() 409
created() 201
forbidden() 403
found() 302
internalServerError() 500
movedPermanently() 301
noContent() 204
notFound() 404
ok() 200
unauthorized() 401

If your controller method has a void return type, a 204 "No Content" response will be created automatically.

If you need access to the current request, use $this->request within your controller method.

Setting headers is simple, too:

use Aphiria\Net\Http\Headers;

final class UserController extends Controller
{
    // ...

    public function getUser(int $id): IResponse
    {
        $user = $this->users->getUserById($id);
        $headers = new Headers();
        $headers->add('Cache-Control', 'no-cache');

        return $this->ok($user, $headers);
    }
}

Parameter Resolution

Your controller methods will frequently need to do things like deserialize the request body or read route/query string values. Aphiria simplifies this process enormously by allowing your method signatures to be expressive.

Request Bodies

Object type hints are always assumed to be the request body, and can be automatically deserialized to any POPO:

final class UserController extends Controller
{
    // ...

    public function createUser(UserDto $userDto): IResponse
    {
        $user = $this->users->createUser($userDto->email, $userDto->password);

        return $this->created("users/{$user->id}", $user);
    }
}

This works for any media type (eg JSON) that you've registered to your content negotiator.

URI Parameters

Aphiria also supports resolving scalar parameters in your controller methods. It will scan route variables, and then, if no matches are found, the query string for scalar parameters. For example, this method will grab includeDeletedUsers from the query string and cast it to a bool:

final class UserController extends Controller
{
    // ...

    // Assume path and query string is "users?includeDeletedUsers=1"
    public function getAllUsers(bool $includeDeletedUsers): array
    {
        return $this->users->getAllUsers($includeDeletedUsers);
    }
}

Nullable parameters and parameters with default values are also supported. If a query string parameter is optional, it must be either nullable or have a default value.

Arrays in Request Bodies

Request bodies might contain an array of values. Because PHP doesn't support generics or typed arrays, you cannot use type-hints alone to deserialize arrays of values. However, it's still easy to do within your controller methods:

final class UserController extends Controller
{
    // ...

    public function createManyUsers(): IResponse
    {
        $users = $this->readRequestBodyAs(User::class . '[]');
        $this->users->createManyUsers($users);

        return $this->created();
    }
}

Validating Request Bodies

It's possible to combine the power of Symfony's serialization component with Aphiria's validation library to automatically validate request bodies on every request. By default, when an invalid request body is detected, a problem details response is returned as a 400. If you'd like to change the response body to something different, you may do so by changing the exception response factory for an InvalidRequestBodyException.

If a request body cannot be automatically deserialized, as in the case of arrays of objects in request bodies, you must manually perform validation.

use Aphiria\Api\Validation\IRequestBodyValidator;

final class UserController extends Controller
{
    public function __construct(private IRequestBodyValidator $validator) {}

    public function createManyUsers(): IResponse
    {
        $users = $this->readRequestBodyAs(User::class . '[]');
        $this->validator->validate($this->request, $users);

        // Continue processing the users...
    }
}

Parsing Request Data

Your controllers might need to do more advanced reading of request data, such as reading cookies, reading multipart bodies, or determining the content type of the request. To simplify this kind of work, an instance of RequestParser is set in your controller:

final class JsonPrettifierController extends Controller
{
    public function prettifyJson(): IResponse
    {
        if (!$this->requestParser->isJson($this->request)) {
            return $this->badRequest();
        }

        $bodyAsString = $this->request->getBody()->readAsString();
        $prettyJson = json_encode($bodyAsString, JSON_PRETTY_PRINT);
        $headers = new Headers();
        $headers->add('Content-Type', 'application/json');
        $response = new Response(200, $headers, new StringBody($prettyJson));

        return $response;
    }
}

Formatting Response Data

If you need to write data back to the response, eg cookies or creating a redirect, an instance of ResponseFormatter is automatically available in the controller:

final class LoginController extends Controller
{
    // ...

    public function logIn(LoginDto $login): IResponse
    {
        $authResults = null;

        // Assume this logic resides in your application
        if (!$this->authenticator->tryLogIn($login->username, $login->password, $authResults)) {
            return $this->unauthorized();
        }

        // Write a cookie containing the auth token back to the response
        $response = new Response(200);
        $authTokenCookie = new Cookie('authtoken', $authResults->getAuthToken(), 3600);
        $this->responseFormatter->setCookie($response, $authTokenCookie);

        return $response;
    }
}

Controller Dependencies

The API library provides support for auto-wiring your controllers. In other words, it can scan your controllers' and middleware's constructors for dependencies and instantiate them. You can read more about the container here.