Controllers

Basics

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

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

final class UserController extends Controller
{
    // ...

    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): IHttpResponseMessage
    {
        $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:

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\HttpHeaders;

final class UserController extends Controller
{
    // ...

    public function getUser(int $id): IHttpResponseMessage
    {
        $user = $this->users->getUserById($id);
        $headers = new HttpHeaders();
        $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): IHttpResponseMessage
    {
        $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(): IHttpResponseMessage
    {
        $users = $this->readRequestBodyAs(User::class . '[]');
        $this->users->createManyUsers($users);

        return $this->created();
    }
}

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(): IHttpResponseMessage
    {
        if (!$this->requestParser->isJson($this->request)) {
            return $this->badRequest();
        }

        $bodyAsString = $this->request->getBody()->readAsString();
        $prettyJson = json_encode($bodyAsString, JSON_PRETTY_PRINT);
        $headers = new HttpHeaders();
        $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 available in the controller:

final class LoginController extends Controller
{
    // ...

    public function logIn(LoginDto $login): IHttpResponseMessage
    {
        $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(), time() + 3600);
        $this->responseFormatter->setCookie($response, $authTokenCookie);

        return $response;
    }
}

Closure Controllers

Sometimes, a controller class is overkill for a route that does very little. In this case, you can use a Closure when defining your routes:

 $routes->get('ping')
    ->toClosure(fn () => $this->ok());

Closures support the same parameter resolution features as controller methods. Here's the cool part - Aphiria will bind an instance of Controller to your closure, which means you can use all the methods, request parsers, and response formatters available inside of Controller via $this.

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. Dependency resolvers simply need to implement IDependencyResolver. To make it easy for users of Aphiria's DI container, you can inject ContainerDependencyResolver into App and Router.

use Aphiria\Api\App;
use Aphiria\Api\ContainerDependencyResolver;
use Aphiria\Api\Router;
use Aphiria\DependencyInjection\Container;

$container = new Container();
$dependencyResolver = new ContainerDependencyResolver($container);

// Assume the route matcher was already set up
$router = new Router($routeMatcher, $dependencyResolver);
$app = new App($dependencyResolver, $router);