REST API guided project

Filtering And Validation

Step 6 / 8 25-40 min JSON API SQLite to MySQL path

Filtering And Validation

The service coordinates allowed operations; the validator checks input before the repository writes. These are the exact implementations used in the completed SQLite API.

1

Action

Passes query parameters or parsed body into AnimalService.

2

Service

Chooses a read/write workflow and checks related records.

3

Validator

Checks create fields or the provided fields of a partial update.

4

Repository

Runs parameterized SQL and returns stored records.

5

Action

Formats data or catches a domain exception.

Supported Filters

Use species, status, shelter_id, min_age_months, max_age_months, or search in the collection query. The repository restricts SQL fragments to known filters and binds values; never insert visitor-provided column names into SQL.

Animal Validator

Create src/Modules/ShelterApi/AnimalValidator.php with the following contents:

<?php
declare(strict_types=1);

namespace App\Modules\ShelterApi;

/*
 * Validation belongs before repository writes. Keeping accepted species,
 * statuses, and field messages here lets store() and update() share rules.
 */
final class AnimalValidator
{
    private const SPECIES = ['cat', 'dog', 'bunny', 'bird'];
    private const STATUSES = ['available', 'reserved', 'adopted'];

    public function validateCreate(array $input): array
    {
        $data = [
            'name' => trim((string) ($input['name'] ?? '')),
            'species' => (string) ($input['species'] ?? ''),
            'shelter_id' => (int) ($input['shelter_id'] ?? 0),
            'age_months' => (int) ($input['age_months'] ?? 0),
            'status' => (string) ($input['status'] ?? 'available'),
            'description' => trim((string) ($input['description'] ?? '')),
        ];
        $errors = [];

        if ($data['name'] === '') {
            $errors['name'][] = 'Name is required.';
        }

        if (!in_array($data['species'], self::SPECIES, true)) {
            $errors['species'][] = 'Species must be cat, dog, bunny, or bird.';
        }

        if ($data['shelter_id'] < 1) {
            $errors['shelter_id'][] = 'Shelter is required.';
        }

        if ($data['age_months'] < 0) {
            $errors['age_months'][] = 'Age cannot be negative.';
        }

        if (!in_array($data['status'], self::STATUSES, true)) {
            $errors['status'][] = 'Status must be available, reserved, or adopted.';
        }

        if ($errors !== []) {
            throw new ValidationException($errors);
        }

        return $data;
    }
}

Animal Service

Create src/Modules/ShelterApi/AnimalService.php with the following contents:

<?php
declare(strict_types=1);

namespace App\Modules\ShelterApi;

/*
 * Application workflow layer:
 * controllers call this service. It validates writes, handles not-found rules,
 * and delegates persistence to AnimalRepository.
 */
final class AnimalService
{
    public function __construct(
        private readonly AnimalRepository $animals = new AnimalRepository(),
        private readonly AnimalValidator $validator = new AnimalValidator(),
    ) {
    }

    public function list(array $filters): array
    {
        return $this->animals->list($filters);
    }

    public function find(int $id): array
    {
        $animal = $this->animals->find($id);
        if ($animal === null) {
            throw new NotFoundException('Animal not found.');
        }

        return $animal;
    }

    public function create(array $input): array
    {
        $data = $this->validator->validateCreate($input);
        $id = $this->animals->create($data);
        return $this->find($id);
    }

    public function update(int $id, array $input): array
    {
        $current = $this->find($id);
        $data = $this->validator->validateCreate(array_merge($current, $input));
        $this->animals->update($id, $data);
        return $this->find($id);
    }

    public function archive(int $id): void
    {
        $this->find($id);
        $this->animals->archive($id);
    }

    public function species(): array
    {
        return $this->animals->species();
    }

    public function shelters(): array
    {
        return $this->animals->shelters();
    }
}

PATCH And Validation Errors

POST requires the create fields. PATCH is partial: this service merges submitted fields with the current record, then validates the complete result. Omitted values are preserved. An empty PATCH currently leaves values unchanged; add an explicit rejection if your API contract requires a change.

The validator checks accepted species/status values and required input. Actions turn ValidationException into 422 using ApiJson::error(). Missing or archived records produce NotFoundException and 404. Before deploying, also validate referenced shelter/species rows, field types/lengths, and writable-field allowlists rather than relying only on database constraints.

POST /api/shelter/animals
Content-Type: application/json

{"species":"dragon"}

Expect 422 and field errors. Then create a valid animal, PATCH only its status, and verify the name/species are unchanged. DELETE archives the record; subsequent GET should return 404 while the database row remains.

Use the same validator/service from every endpoint. Do not duplicate field rules in routes and actions.

Continue with Errors And Versioning.

Project files

Download completed API

This download does not include the CorianderPHP framework. Start from a CorianderPHP project, then copy these app-owned files into it.

Download completed API