Forms

Whilst it is possible to access a RequestInterface from with a Controller, it is sometimes necessary to format or validate this data before using it.

Forms that implement Centum\Interfaces\Http\FormInterface have special access to the data and files in a Request:

namespace App\Web\Forms;

use Centum\Interfaces\Http\FormInterface;
use InvalidArgumentException;

class LoginForm implements FormInterface
{
    public function __construct(
        protected readonly string $username,
        protected readonly string $password
    ) {
        if (strlen($username) < 6) {
            throw new InvalidArgumentException("Username is too short.");
        }

        if (strlen($password) < 6) {
            throw new InvalidArgumentException("Password is too short.");
        }
    }

    public function getUsername(): string
    {
        return $this->username;
    }

    public function getPassword(): string
    {
        return $this->password;
    }
}

You can then inject the Form in a Controller:

namespace App\Web\Controllers;

use App\Web\Forms\LoginForm;
use Centum\Http\Response;
use Centum\Interfaces\Http\ResponseInterface;
use Centum\Interfaces\Router\ControllerInterface;

class LoginController implements ControllerInterface
{
    public function form(): ResponseInterface
    {
        return new Response("<form><!-- Login form --></form>");
    }

    public function submit(LoginForm $loginForm): ResponseInterface
    {
        return new Response("Hello {$loginForm->getUsername()}");
    }
}

If a field cannot be found in the Request data, a Centum\Container\Exception\FormFieldNotFoundException will be thrown.

Different Types of Data

bool

You can use the bool type in the constructor to confirm whether a field exists in the Request data. This can be useful with checkboxes as they only send data if they are selected.

For example, a login form might have a checkbox to remember the user:

<form>
    <input type="text" name="username">
    <input type="password" name="password">

    <div>
        <input type="checkbox" name="rememberMe">
        Remember me
    </div>

    <button type="submit">Submit</button>
</form>
namespace App\Web\Forms;

use Centum\Interfaces\Http\FormInterface;

class LoginForm implements FormInterface
{
    public function __construct(
        protected readonly string $username,
        protected readonly string $password,
        protected readonly bool $rememberMe
    ) {
        // ...
    }

    // ...
}

array

If a field has multiple values, you can use the array type in the constructor:

<form>
    <div>
        Who are your friends?
        <input type="text" name="friends[]">
        <input type="text" name="friends[]">
        <input type="text" name="friends[]">
    </div>

    <button type="submit">Submit</button>
</form>
namespace App\Web\Forms;

use Centum\Interfaces\Http\FormInterface;

class FriendsForm implements FormInterface
{
    /**
     * @param array<string> $friends
     */
    public function __construct(
        protected readonly array $friends
    ) {
        // ...
    }

    // ...
}

Optional Fields

In the previous examples, all of the fields were required by the Form. It is possible to make these optional by allowing null or setting a default value in the constructor:

namespace App\Web\Forms;

use Centum\Interfaces\Http\FormInterface;

class ExampleForm implements FormInterface
{
    public function __construct(
        protected readonly string $requiredField,
        protected readonly ?string $optionalField1 = null,
        protected readonly string $optionalField2 = "default value"
    ) {
        // ...
    }

    // ...
}

Injecting Things from the Container

You can, of course, inject things from the Container, as you normally would:

namespace App\Web\Forms;

use Centum\Interfaces\Clock\ClockInterface;
use Centum\Interfaces\Http\FormInterface;
use DateTimeImmutable;
use InvalidArgumentException;

class CreateUserForm implements FormInterface
{
    protected readonly DateTimeImmutable $dateCreated;

    public function __construct(
        protected readonly string $username,
        protected readonly string $password,
        ClockInterface $clock,
        UsernameChecker $usernameChecker
    ) {
        if (strlen($username) < 6) {
            throw new InvalidArgumentException("Username is too short.");
        }

        if ($usernameChecker->usernameExists($username)) {
            throw new InvalidArgumentException("Username already exists.");
        }

        if (strlen($password) < 6) {
            throw new InvalidArgumentException("Password is too short.");
        }

        $this->dateCreated = $clock->now();
    }


    public function getUsername(): string
    {
        return $this->username;
    }

    public function getPassword(): string
    {
        return $this->password;
    }

    public function getDateCreated(): DateTimeImmutable
    {
        return $this->dateCreated;
    }
}

(UsernameChecker not shown but checks if a username already exists in the database).

File Uploads

File uploads can also be accessed in a Form:

<form method="POST" enctype="multipart/form-data">
    <input type="file" name="images" multiple />

    <button type="submit">Upload</button>
</form>

In the Form, you can access the FileGroupInterface instances:

namespace App\Web\Forms;

use Centum\Interfaces\Http\FileGroupInterface;
use Centum\Interfaces\Http\FormInterface;
use InvalidArgumentException;

class UploadForm implements FormInterface
{
    public function __construct(
        protected readonly FileGroupInterface $images
    ) {
        if (count($images->all()) === 0) {
            throw new InvalidArgumentException(
                "No images uploaded."
            );
        }
    }

    public function getImages(): FileGroupInterface
    {
        return $this->images;
    }
}