code:
<?php

declare(strict_types=1);

namespace App\Controllers;

use App\Models\PairModel;
use App\Models\PairPlayerPivotModel;
use App\Models\PlayerModel;
use App\Validations\PairPlayerValidation;
use App\Validations\PairValidation;

class PairPlayerController extends BaseController
{
    /**
     * Muestra la información de todas las "parejas de jugadores".
     */
    public function index(): void
    {
        // Define los query params de la petición.
        $queryFields = [
            'page' => 1,
            'orderBy' => 'created_at',
            'sortBy' => 'desc',
            'registration_category_id' => null,
            'is_eliminated' => null,
            'is_active' => null,
        ];

        $queryParams = [];

        // Obtiene solo los query params necesarios.
        foreach ($queryFields as $param => $default) {
            $queryParams[$param] = $this->app()->request()->query->{$param} ?? $default;

            if (is_string($queryParams[$param]) && empty($queryParams[$param])) {
                $queryParams[$param] = $default;
            }

            if (is_null($queryParams[$param])) {
                unset($queryParams[$param]);
            }
        }

        $queryNames = array_keys($queryFields);

        // Obtiene y establece las reglas de validación.
        $this->gump()->validation_rules(PairValidation::getRules($queryNames));

        // Obtiene y establece los filtros de validación.
        $this->gump()->filter_rules(PairValidation::getFilters($queryNames));

        // Comprueba los query params de la petición.
        $queryParams = $this->gump()->run($queryParams);

        // Comprueba si existen errores de validación.
        if ($this->gump()->errors()) {
            $this->respondValidationErrors(
                $this->gump()->get_errors_array(),
                'The pairs players search information is incorrect');
        }

        // Consulta la información de todas las "parejas" con paginación.
        $pairModel = new PairModel;

        // Establece los filtros permitidos.
        foreach (['registration_category_id', 'is_eliminated', 'is_active'] as $param) {
            if (isset($queryParams[$param])) {
                $pairModel->eq($param, $queryParams[$param]);
            }
        }

        // Obtiene la información sobre la paginación.
        $pairModel->paginate($queryParams['page']);
        $pagination = $pairModel->pagination;

        // Aplica los parámetros de ordenamiento.
        $pairModel->orderBy(sprintf('%s %s', $queryParams['orderBy'], $queryParams['sortBy']));

        // Consulta las "parejas" de búsqueda.
        $pairs = array_map(static function (PairModel $pair): array {
            // Consulta la "categoría de inscripción" de la "pareja".
            $pair->setCustomData('registration_category', $pair->registrationCategory);
            unset($pair->registration_category_id);

            $players = array_map(static fn (PairPlayerPivotModel $pairPlayerRel): array => [
                'player' => $pairPlayerRel->player,
                'relationship' => $pairPlayerRel,
            ], $pair->pairPlayerPivot);

            return ['pair' => $pair, 'players' => $players];
        }, $pairModel->findAll());

        $this->respondPagination($pairs, $pagination, 'Information about all the pairs players with pagination');
    }

    /**
     * Crea una "pareja" con la información de los "jugadores".
     */
    public function create(): void
    {
        // Define los campos necesarios de la petición.
        $fields = ['registration_category_id', 'players'];

        $data = [];

        // Obtiene solo los campos necesarios.
        foreach ($fields as $field) {
            $data[$field] = $this->app()->request()->data->{$field} ?? null;
        }

        // Define los campos necesarios de los "jugadores".
        $playersFields = ['fullname', 'city', 'weight', 'height', 'age', 'experience'];

        // Obtiene las reglas de validación.
        $rules = array_merge(
            PairPlayerValidation::getRules($fields),
            PairPlayerValidation::getPlayersRules($playersFields),
        );

        // Define todas las reglas de validación como obligatorias.
        foreach (array_keys($rules) as $rule) {
            array_unshift($rules[$rule], 'required');
        }

        // Establece las reglas de validación.
        $this->gump()->validation_rules($rules);

        // Establece los filtros de validación.
        $this->gump()->filter_rules(PairPlayerValidation::getPlayersFilters($playersFields));

        // Valida el cuerpo de la petición.
        $data = $this->gump()->run($data);

        // Comprueba si existen errores de validación.
        if ($this->gump()->errors()) {
            $this->respondValidationErrors(
                $this->gump()->get_errors_array(),
                'The pair with players information is incorrect');
        }

        $dataPlayers = $data['players'];

        unset($data['players']);

        // Registra la información de la "pareja".
        $pair = new PairModel;
        $pair->copyFrom($data);
        $pair->insert();

        $player = new PlayerModel;
        $pairPlayerPivot = new PairPlayerPivotModel;

        foreach ($dataPlayers as $dataPlayer) {
            // Registra la información de los "jugadores".
            $player->copyFrom($dataPlayer);
            $player->insert();

            // Registra la relación del "jugador" con la "pareja".
            $pairPlayerPivot->copyFrom(['player_id' => $player->id, 'pair_id' => $pair->id]);
            $pairPlayerPivot->insert();

            $player->reset();
            $pairPlayerPivot->reset();
        }

        $id = $pair->id;

        // Consulta la información de la "pareja" registrada.
        $pair->reset();
        $pair->find($id);

        // Consulta la "categoría de inscripción" de la "pareja".
        $pair->setCustomData('registration_category', $pair->registrationCategory);
        unset($pair->registration_category_id);

        // Consulta los "jugadores" registrados de la "pareja".
        $players = array_map(static fn (PairPlayerPivotModel $pairPlayerRel): array => [
            'player' => $pairPlayerRel->player,
            'relationship' => $pairPlayerRel,
        ], $pair->pairPlayerPivot);

        $this->respondCreated(['pair' => $pair, 'players' => $players], 'The pair with players was created successfully');
    }

    /**
     * Muestra la información de los "jugadores de una pareja".
     */
    public function show(string $id): void
    {
        // Obtiene las reglas de validación
        // y las establece como obligatorias.
        $rules = PairValidation::getRules(['id']);
        array_unshift($rules['id'], 'required');

        // Establece las reglas de validación.
        $this->gump()->validation_rules($rules);

        // Comprueba los parámetros de la petición.
        $this->gump()->run(['id' => $id]);

        // Comprueba si existen errores de validación.
        if ($this->gump()->errors()) {
            $this->respondValidationErrors(
                $this->gump()->get_errors_array(),
                'The pair identifier is incorrect');
        }

        // Consulta la información de la "pareja".
        $pair = new PairModel;
        $pair->select('id')->eq('id', $id)->find();

        // Comprueba si existe la "pareja".
        if (! $pair->isHydrated()) {
            $this->respondNotFound('The pair information was not found');
        }

        // Consulta los "jugadores" de la "pareja".
        $players = array_map(static fn (PairPlayerPivotModel $pairPlayerRel): array => [
            'player' => $pairPlayerRel->player,
            'relationship' => $pairPlayerRel,
        ], $pair->pairPlayerPivot);

        $this->respond($players, 'Information about the pair players');
    }
}


code:
<?php

declare(strict_types=1);

namespace App\Controllers;

use App\Models\PlayerModel;
use App\Validations\PlayerValidation;
use PDOException;

class PlayerController extends BaseController
{
    /**
     * Muestra la información de todos los "jugadores".
     */
    public function index(): void
    {
        // Define los query params de la petición.
        $queryFields = [
            'page' => 1,
            'search' => '',
            'filterBy' => 'fullname',
            'orderBy' => 'created_at',
            'sortBy' => 'desc',
            'is_active' => null,
        ];

        $queryParams = [];

        // Obtiene solo los query params necesarios.
        foreach ($queryFields as $param => $default) {
            $queryParams[$param] = $this->app()->request()->query->{$param} ?? $default;

            if (is_string($queryParams[$param]) && empty($queryParams[$param])) {
                $queryParams[$param] = $default;
            }

            if (is_null($queryParams[$param])) {
                unset($queryParams[$param]);
            }
        }

        $queryNames = array_keys($queryFields);

        // Obtiene y establece las reglas de validación.
        $this->gump()->validation_rules(PlayerValidation::getRules($queryNames));

        // Obtiene y establece los filtros de validación.
        $this->gump()->filter_rules(PlayerValidation::getFilters($queryNames));

        // Comprueba los query params de la petición.
        $queryParams = $this->gump()->run($queryParams);

        // Comprueba si existen errores de validación.
        if ($this->gump()->errors()) {
            $this->respondValidationErrors(
                $this->gump()->get_errors_array(),
                'The players search information is incorrect');
        }

        // Consulta la información de todos los "jugadores" con paginación.
        $players = new PlayerModel;
        $players->like($queryParams['filterBy'], sprintf('%%%s%%', $queryParams['search']));

        // Filtra los "jugadores" por estatus de actividad.
        if (isset($queryParams['is_active'])) {
            $players->eq('is_active', $queryParams['is_active']);
        }

        // Obtiene la información sobre la paginación.
        $players->paginate($queryParams['page']);
        $pagination = $players->pagination;

        // Aplica los parámetros de ordenamiento.
        $players->orderBy(sprintf('%s %s', $queryParams['orderBy'], $queryParams['sortBy']));

        $this->respondPagination($players->findAll(), $pagination, 'Information about all the players with pagination');
    }

    /**
     * Registra la información de un "jugador".
     */
    public function create(): void
    {
        // Define los campos necesarios de la petición.
        $fields = ['fullname', 'city', 'weight', 'height', 'age', 'experience'];

        $data = [];

        // Obtiene solo los campos necesarios.
        foreach ($fields as $field) {
            $data[$field] = $this->app()->request()->data->{$field} ?? null;
        }

        // Obtiene las reglas de validación.
        $rules = PlayerValidation::getRules($fields);

        // Define todas las reglas de validación como obligatorias.
        foreach (array_keys($rules) as $rule) {
            array_unshift($rules[$rule], 'required');
        }

        // Establece las reglas de validación.
        $this->gump()->validation_rules($rules);

        // Establece los filtros de validación.
        $this->gump()->filter_rules(PlayerValidation::getFilters($fields));

        // Valida el cuerpo de la petición.
        $data = $this->gump()->run($data);

        // Comprueba si existen errores de validación.
        if ($this->gump()->errors()) {
            $this->respondValidationErrors(
                $this->gump()->get_errors_array(),
                'The player information is incorrect');
        }

        // Registra la información del "jugador".
        $player = new PlayerModel;
        $player->copyFrom($data);
        $player->insert();

        $id = $player->id;

        // Consulta la información del "jugador" registrado.
        $player->reset();
        $player->find($id);

        $this->respondCreated($player, 'The player was created successfully');
    }

    /**
     * Muestra la información de un "jugador".
     */
    public function show(string $id): void
    {
        // Obtiene las reglas de validación
        // y las establece como obligatorias.
        $rules = PlayerValidation::getRules(['id']);
        array_unshift($rules['id'], 'required');

        // Establece las reglas de validación.
        $this->gump()->validation_rules($rules);

        // Comprueba los parámetros de la petición.
        $this->gump()->run(['id' => $id]);

        // Comprueba si existen errores de validación.
        if ($this->gump()->errors()) {
            $this->respondValidationErrors(
                $this->gump()->get_errors_array(),
                'The player identifier is incorrect');
        }

        // Consulta la información del "jugador".
        $player = new PlayerModel;
        $player->find($id);

        // Comprueba si existe el "jugador".
        if (! $player->isHydrated()) {
            $this->respondNotFound('The player information was not found');
        }

        $this->respond($player, 'Information about the player');
    }

    /**
     * Muestra la información de la "pareja" de un "jugador".
     */
    public function pair(string $id): void
    {
        // Obtiene las reglas de validación
        // y las establece como obligatorias.
        $rules = PlayerValidation::getRules(['id']);
        array_unshift($rules['id'], 'required');

        // Establece las reglas de validación.
        $this->gump()->validation_rules($rules);

        // Comprueba los parámetros de la petición.
        $this->gump()->run(['id' => $id]);

        // Comprueba si existen errores de validación.
        if ($this->gump()->errors()) {
            $this->respondValidationErrors(
                $this->gump()->get_errors_array(),
                'The player identifier is incorrect');
        }

        // Consulta la información del "jugador".
        $player = new PlayerModel;
        $player->select('id')->eq('id', $id)->find();

        // Comprueba si existe el "jugador".
        if (! $player->isHydrated()) {
            $this->respondNotFound('The player information was not found');
        }

        // Consulta la relación de la "pareja" con el "jugador".
        $pairPlayerRel = $player->pairPlayerPivot;

        // Consulta la "pareja" del "jugador".
        $pair = $pairPlayerRel->pair;

        // Consulta la "categoría de inscripción" de la "pareja".
        $pair->setCustomData('registration_category', $pair->registrationCategory);
        unset($pair->registration_category_id);

        $this->respond(['pair' => $pair, 'relationship' => $pairPlayerRel], 'Information about the player pair');
    }

    /**
     * Muestra la información del "grupo" de un "jugador".
     */
    public function group(string $id): void
    {
        // Obtiene las reglas de validación
        // y las establece como obligatorias.
        $rules = PlayerValidation::getRules(['id']);
        array_unshift($rules['id'], 'required');

        // Establece las reglas de validación.
        $this->gump()->validation_rules($rules);

        // Comprueba los parámetros de la petición.
        $this->gump()->run(['id' => $id]);

        // Comprueba si existen errores de validación.
        if ($this->gump()->errors()) {
            $this->respondValidationErrors(
                $this->gump()->get_errors_array(),
                'The player identifier is incorrect');
        }

        // Consulta la información del "jugador".
        $player = new PlayerModel;
        $player->select('id')->eq('id', $id)->find();

        // Comprueba si existe el "jugador".
        if (! $player->isHydrated()) {
            $this->respondNotFound('The player information was not found');
        }

        // Consulta la relación de la "pareja" con el "jugador".
        $pairPlayerRel = $player->pairPlayerPivot;

        // Consulta la relación de la "pareja" con el "grupo".
        $groupPairRel = $pairPlayerRel->pair->groupPairPivot;

        // Consulta el "grupo" de la "pareja".
        $group = $groupPairRel->_group;

        // Consulta la "categoría de inscripción" del "grupo".
        $group->setCustomData('registration_category', $group->registrationCategory);
        unset($group->registration_category_id);

        $this->respond([
            'group' => $group,
            'relationship_pair_player' => $pairPlayerRel,
            'relationship_group_pair' => $groupPairRel,
        ], 'Information about the player group');
    }

    /**
     * Modifica la información de un "jugador".
     */
    public function update(string $id): void
    {
        // Define los campos que se pueden modificar.
        $fields = ['fullname', 'city', 'weight', 'height', 'age', 'experience', 'is_active'];

        $data = ['id' => $id];

        // Obtiene solo los campos necesarios.
        foreach ($fields as $field) {
            $data[$field] = $this->app()->request()->data->{$field} ?? null;

            if (is_null($data[$field])) {
                unset($data[$field]);
            }
        }

        $fieldNames = array_keys($data);

        // Obtiene las reglas de validación
        // y establece el identificador como obligatorio.
        $rules = PlayerValidation::getRules(['id', ...$fieldNames]);
        array_unshift($rules['id'], 'required');

        // Establece las reglas de validación.
        $this->gump()->validation_rules($rules);

        // Establece los filtros de validación.
        $this->gump()->filter_rules(PlayerValidation::getFilters($fieldNames));

        // Valida el cuerpo de la petición.
        $data = $this->gump()->run($data);

        unset($data['id']);

        // Comprueba si existen errores de validación.
        if ($this->gump()->errors()) {
            $this->respondValidationErrors(
                $this->gump()->get_errors_array(),
                'The player information is incorrect');
        }

        // Consulta la información del "jugador".
        $player = new PlayerModel;
        $player->select('id')->eq('id', $id)->find();

        // Comprueba si existe el "jugador".
        if (! $player->isHydrated()) {
            $this->respondNotFound('The player information was not found');
        }

        // Modifica la información del "jugador".
        if (! empty($data)) {
            $player->copyFrom($data);
            $player->update();
            $player->reset();
        }

        // Consulta la información actualizada del "jugador".
        $player->find($id);

        $this->respondUpdated($player, 'The player was updated successfully');
    }

    /**
     * Elimina la información de un "jugador".
     */
    public function delete(string $id): void
    {
        // Obtiene las reglas de validación
        // y las establece como obligatorias.
        $rules = PlayerValidation::getRules(['id']);
        array_unshift($rules['id'], 'required');

        // Establece las reglas de validación.
        $this->gump()->validation_rules($rules);

        // Comprueba los parámetros de la petición.
        $this->gump()->run(['id' => $id]);

        // Comprueba si existen errores de validación.
        if ($this->gump()->errors()) {
            $this->respondValidationErrors(
                $this->gump()->get_errors_array(),
                'The player identifier is incorrect');
        }

        // Consulta la información del "jugador".
        $player = new PlayerModel;
        $player->find($id);

        // Comprueba si existe el "jugador".
        if (! $player->isHydrated()) {
            $this->respondNotFound('The player information was not found');
        }

        // Elimina la información del "jugador".
        try {
            $player->delete();
        } catch (PDOException $e) {
            $this->respondConflict('The player contains related information');
        }

        $this->respondDeleted($player, 'The player was deleted successfully');
    }
}

license: mit


code:
<?php

declare(strict_types=1);

namespace App\Controllers;

use App\Models\GroupModel;
use App\Models\PairModel;
use App\Models\PairPlayerPivotModel;
use App\Models\RegistrationCategoryModel;
use App\Validations\RegistrationCategoryValidation;

class RegistrationCategoryController extends BaseController
{
    /**
     * Muestra la información de todas las
     * "categorías de inscripción de las parejas de jugadores".
     */
    public function index(): void
    {
        $categories = (new RegistrationCategoryModel)->orderBy('description ASC')->findAll();
        $this->respond($categories, 'Information about all the registrations categories');
    }

    /**
     * Muestra la información de una
     * "categoría de inscripción de las parejas de jugadores".
     */
    public function show(string $id): void
    {
        // Obtiene las reglas de validación
        // y las establece como obligatorias.
        $rules = RegistrationCategoryValidation::getRules(['id']);
        array_unshift($rules['id'], 'required');

        // Establece las reglas de validación.
        $this->gump()->validation_rules($rules);

        // Comprueba los parámetros de la petición.
        $this->gump()->run(['id' => $id]);

        // Comprueba si existen errores de validación.
        if ($this->gump()->errors()) {
            $this->respondValidationErrors(
                $this->gump()->get_errors_array(),
                'The registration category identifier is incorrect');
        }

        // Consulta la información de la "categoría de inscripción".
        $registrationCategory = new RegistrationCategoryModel;
        $registrationCategory->find($id);

        // Comprueba si existe la "categoría de inscripción".
        if (! $registrationCategory->isHydrated()) {
            $this->respondNotFound('The registration category information was not found');
        }

        $this->respond($registrationCategory, 'Information about the registration category');
    }

    /**
     * Muestra la información de los "grupos"
     * de una "categoría de inscripción de las parejas de jugadores".
     */
    public function groups(string $id): void
    {
        // Obtiene las reglas de validación
        // y las establece como obligatorias.
        $rules = RegistrationCategoryValidation::getRules(['id']);
        array_unshift($rules['id'], 'required');

        // Establece las reglas de validación.
        $this->gump()->validation_rules($rules);

        // Comprueba los parámetros de la petición.
        $this->gump()->run(['id' => $id]);

        // Comprueba si existen errores de validación.
        if ($this->gump()->errors()) {
            $this->respondValidationErrors(
                $this->gump()->get_errors_array(),
                'The registration category identifier is incorrect');
        }

        // Consulta la información de la "categoría de inscripción".
        $registrationCategory = new RegistrationCategoryModel;
        $registrationCategory->select('id')->eq('id', $id)->find();

        // Comprueba si existe la "categoría de inscripción".
        if (! $registrationCategory->isHydrated()) {
            $this->respondNotFound('The registration category information was not found');
        }

        // Consulta la "categoría de inscripción" de los "grupos".
        $groups = array_map(static function (GroupModel $group): GroupModel {
            $group->setCustomData('registration_category', $group->registrationCategory);
            unset($group->registration_category_id);

            return $group;
        }, $registrationCategory->groups);

        $this->respond($groups, 'Information about the registration category groups');
    }

    /**
     * Ranking de las "parejas" y "jugadores" de la "categoría de inscripción".
     */
    public function ranking(string $id): void
    {
        // Obtiene las reglas de validación
        // y las establece como obligatorias.
        $rules = RegistrationCategoryValidation::getRules(['id']);
        array_unshift($rules['id'], 'required');

        // Establece las reglas de validación.
        $this->gump()->validation_rules($rules);

        // Comprueba los parámetros de la petición.
        $this->gump()->run(['id' => $id]);

        // Comprueba si existen errores de validación.
        if ($this->gump()->errors()) {
            $this->respondValidationErrors(
                $this->gump()->get_errors_array(),
                'The registration category identifier is incorrect');
        }

        // Consulta la información de la "categoría de inscripción".
        $registrationCategory = new RegistrationCategoryModel;
        $registrationCategory->select('id')->eq('id', $id)->find();

        // Comprueba si existe la "categoría de inscripción".
        if (! $registrationCategory->isHydrated()) {
            $this->respondNotFound('The registration category information was not found');
        }

        // Consulta el ranking de las "parejas" del "grupo".
        $ranking = (new PairModel)->select('pairs.*', 'SUM(CASE WHEN matches_pairs.is_winner = 1 THEN 1 ELSE 0 END) AS total_winners', 'SUM(matches_pairs.score) AS total_score')
            ->join('matches_pairs', 'pairs.id = matches_pairs.pair_id', 'INNER') // Partidos de las parejas.
            ->eq('registration_category_id', $id)
            ->groupBy('pairs.id')
            ->orderBy('total_winners DESC', 'total_score DESC')
            ->findAll();

        // Obtiene la información de las "parejas".
        $pairs = array_map(static function (PairModel $pair): array {
            // Consulta la "categoría de inscripción" de la "pareja".
            $pair->setCustomData('registration_category', $pair->registrationCategory);

            // Consulta los "jugadores" de la "pareja".
            $pair->setCustomData('players', array_map(static fn (PairPlayerPivotModel $pairPlayerRel): array => [
                'player' => $pairPlayerRel->player,
                'relationship' => $pairPlayerRel,
            ], $pair->pairPlayerPivot));

            // Obtiene los datos de "clasificación" de la "pareja".
            $rating = ['total_winners' => $pair->total_winners, 'total_score' => $pair->total_score];

            unset($pair->registration_category_id, $pair->total_winners, $pair->total_score);

            return ['pair' => $pair, 'rating' => $rating];
        }, $ranking);

        $this->respond($pairs, 'Information about the registration category ranking');
    }
}


code:
<?php

declare(strict_types=1);

namespace App\Controllers;

use App\Models\RoleModel;
use App\Validations\RoleValidation;

class RoleController extends BaseController
{
    /**
     * Muestra la información de todos los
     * "roles de los usuarios de acceso".
     */
    public function index(): void
    {
        $roles = (new RoleModel)->orderBy('description ASC')->findAll();
        $this->respond($roles, 'Information about all the access users roles');
    }

    /**
     * Muestra la información de un
     * "rol de usuario de acceso".
     */
    public function show(string $id): void
    {
        // Obtiene las reglas de validación
        // y las establece como obligatorias.
        $rules = RoleValidation::getRules(['id']);
        array_unshift($rules['id'], 'required');

        // Establece las reglas de validación.
        $this->gump()->validation_rules($rules);

        // Comprueba los parámetros de la petición.
        $this->gump()->run(['id' => $id]);

        // Comprueba si existen errores de validación.
        if ($this->gump()->errors()) {
            $this->respondValidationErrors(
                $this->gump()->get_errors_array(),
                'The access user role identifier is incorrect');
        }

        // Consulta la información del "rol de usuario de acceso".
        $role = new RoleModel;
        $role->find($id);

        // Comprueba si existe el "rol de usuario de acceso".
        if (! $role->isHydrated()) {
            $this->respondNotFound('The access user role information was not found');
        }

        $this->respond($role, 'Information about the access user role');
    }
}


code:
<?php

declare(strict_types=1);

namespace App\Controllers;

use App\Helpers\Date;
use App\Models\GroupMatchPivotModel;
use App\Models\GroupModel;
use App\Models\GroupPairPivotModel;
use App\Models\MatchCategoryModel;
use App\Models\MatchModel;
use App\Models\MatchPairPivotModel;
use App\Models\MatchStatusModel;
use App\Models\PairModel;
use App\Models\RegistrationCategoryModel;

class RoundController extends BaseController
{
    /**
     * Obtiene las reglas del torneo por "categoría de inscripción".
     */
    private function getRules(): array
    {
        return [
            'open' => ['max_pairs' => 4, 'max_groups' => 27],
            'seniors' => ['max_pairs' => 4, 'max_groups' => 27],
        ];
    }

    /**
     * Genera "grupos" para cada tipo de "categoría de inscripción"
     * y asigna las "parejas" de cada "categoría" a un "grupo" de manera aleatoria.
     *
     * Después de crearlos se generan todos los "partidos" posibles del mismo "grupo".
     */
    public function init(): void
    {
        $rules = $this->getRules();

        $registrationCategory = new RegistrationCategoryModel;
        $categoriesNames = array_keys($rules);

        // Obtiene la información de todas las "categorías de inscripción".
        $categories = $registrationCategory->select('id', 'name')
            ->in('name', $categoriesNames)
            ->findAll();

        // Obtiene la información de la "categoría de los partidos".
        $matchCategory = (new MatchCategoryModel)->select('id')
            ->eq('name', 'qualifier')
            ->find();

        // Obtiene la información del "estatus de juego de los partidos".
        $matchStatus = (new MatchStatusModel)->select('id')
            ->eq('name', 'scheduled')
            ->find();

        // Modelos.
        $pair = new PairModel;
        $group = new GroupModel;
        $groupPairPivot = new GroupPairPivotModel;
        $match = new MatchModel;
        $groupMatchPivot = new GroupMatchPivotModel;
        $matchPairPivot = new MatchPairPivotModel;

        $column = 'registration_category_id';

        // Obtiene el año actual.
        $year = Date::getCurrentYear();

        // Formato del nombre de los grupos.
        // Nombre: A_2025, B_2025, C_2025, ...
        // Descripción: A, B, C, ...
        foreach ($categories as $category) {
            $group->reset();

            // Obtiene el número de "grupos" de la "categoría".
            $group->select('COUNT(*) AS _count')
                ->eq($column, $category->id)
                ->eq('is_active', 1)
                ->find();

            // Ignora las "categorías" que contengan "grupos".
            if ($group->_count > 0) {
                continue;
            }

            $group->reset();

            // Obtiene los parámetros de configuración de la "categoría".
            $params = $rules[$category->name];

            // Obtiene el número de "parejas" de la "categoría".
            $pair->select('COUNT(*) AS _count')
                ->eq($column, $category->id)
                ->eq('is_active', 1)
                ->find();

            // Calcula el número total de "grupos" de la "categoría".
            $numGroups = $pair->_count / $params['max_pairs'];

            $pair->reset();

            // Comprueba si la "categoría" completa los "grupos" de las "parejas de jugadores".
            if ($numGroups < 1 || is_float($numGroups)) {
                $this->respondDependecyError('The registration category of pairs players does not complete the groups');
            }

            // Comprueba el número máximo de "grupos" permitidos.
            if ($numGroups > $params['max_groups']) {
                $this->respondDependecyError('The registration category of pairs players exceeds the number of allowed groups');
            }

            // Genera los nombres de los "grupos".
            $groupsNames = range('A', chr(64 + $numGroups));

            // Agrega la letra "Ñ" a los nombres de los "grupos".
            if ($numGroups > 14) {
                array_splice($groupsNames, 14, 0, 'Ñ');
                array_pop($groupsNames);
            }

            // Obtiene la información de las "parejas" de la "categoría".
            $pairs = $pair->select('id')
                ->eq($column, $category->id)
                ->eq('is_active', 1)
                ->findAll();

            $pair->reset();

            // Ordena las "parejas" de manera aleatoria.
            shuffle($pairs);

            // Segmenta las "parejas" con el número máximo de "parejas" por "grupo".
            foreach (array_chunk($pairs, $params['max_pairs']) as $key => $segment) {
                // Registra la información del "grupo".
                $group->copyFrom([
                    $column => $category->id,
                    'name' => sprintf('%s_%u', $groupsNames[$key], $year),
                    'description' => $groupsNames[$key],
                ]);

                $group->insert();

                // Registra la relación de la "pareja" con el "grupo".
                foreach ($segment as $dataPair) {
                    $groupPairPivot->copyFrom(['group_id' => $group->id, 'pair_id' => $dataPair->id]);
                    $groupPairPivot->insert();
                    $groupPairPivot->reset();
                }

                // Consulta la información de las "parejas" del "grupo".
                $groupsPairs = $groupPairPivot->select('pair_id')
                    ->eq('group_id', $group->id)
                    ->findAll();

                $groupPairPivot->reset();

                // Registra la relación de los "partidos" con las "parejas" del "grupo".
                for ($i = 0; $i < $params['max_pairs']; $i++) {
                    for ($j = $i + 1; $j < $params['max_pairs']; $j++) {
                        // Registra la información del "partido".
                        $match->copyFrom([
                            $column => $category->id,
                            'match_category_id' => $matchCategory->id,
                            'match_status_id' => $matchStatus->id,
                        ]);

                        $match->insert();

                        // Registra la relación del "partido" con el "grupo".
                        $groupMatchPivot->copyFrom(['group_id' => $group->id, 'match_id' => $match->id]);
                        $groupMatchPivot->insert();
                        $groupMatchPivot->reset();

                        // Registra la relación del "partido" con la primera "pareja".
                        $matchPairPivot->copyFrom(['match_id' => $match->id, 'pair_id' => $groupsPairs[$i]->pair_id]);
                        $matchPairPivot->insert();
                        $matchPairPivot->reset();

                        // Registra la relación del "partido" con la otra "pareja".
                        $matchPairPivot->copyFrom(['match_id' => $match->id, 'pair_id' => $groupsPairs[$j]->pair_id]);
                        $matchPairPivot->insert();
                        $matchPairPivot->reset();

                        $match->reset();
                    }
                }
            }
        }

        $this->respondNoContent('The round init action was executed successfully');
    }

    /**
     * Descalifica los "grupos" donde todas sus "parejas" están descalificadas.
     */
    public function purgeGroups(): void
    {
        // Consulta el número de "parejas" eliminadas en cada "grupo".
        $rating = (new GroupModel)->select('groups.id, groups.registration_category_id, SUM(CASE WHEN pairs.is_eliminated = 1 THEN 1 ELSE 0 END) AS total_eliminated')
            ->join('groups_pairs', 'groups.id = groups_pairs.group_id', 'INNER')
            ->join('pairs', 'groups_pairs.pair_id = pairs.id', 'INNER')
            ->eq('is_active', 1)
            ->eq('is_eliminated', 0)
            ->groupBy('groups.id')
            ->findAll();

        $rules = $this->getRules();

        foreach ($rating as $group) {
            $settings = $rules[$group->registrationCategory->name];

            // Comprueba si el "grupo" está descalificado.
            if ($group->total_eliminated >= $settings['max_pairs']) {
                unset($group->registration_category_id, $group->total_eliminated);

                // Actualiza el estatus de eliminación del "grupo".
                $group->is_eliminated = 1;
                $group->update();
            }
        }

        $this->respondNoContent('The purge groups action was executed successfully');
    }
}


code:
<?php

declare(strict_types=1);

namespace App\Controllers;

use App\Models\UserModel;
use App\Validations\UserValidation;

class UserController extends BaseController
{
    /**
     * Obtiene las columnas visibles del "usuario".
     */
    private function getColumns(): array
    {
        return ['id', 'email', 'username', 'role_id', 'fullname', 'is_active', 'created_at', 'updated_at'];
    }

    /**
     * Muestra la información de todos los "usuarios".
     */
    public function index(): void
    {
        // Define los query params de la petición.
        $queryFields = [
            'page' => 1,
            'search' => '',
            'filterBy' => 'fullname',
            'orderBy' => 'created_at',
            'sortBy' => 'desc',
            'role_id' => null,
            'is_active' => null,
        ];

        $queryParams = [];

        // Obtiene solo los query params necesarios.
        foreach ($queryFields as $param => $default) {
            $queryParams[$param] = $this->app()->request()->query->{$param} ?? $default;

            if (is_string($queryParams[$param]) && empty($queryParams[$param])) {
                $queryParams[$param] = $default;
            }

            if (is_null($queryParams[$param])) {
                unset($queryParams[$param]);
            }
        }

        $queryNames = array_keys($queryFields);

        // Obtiene y establece las reglas de validación.
        $this->gump()->validation_rules(UserValidation::getRules($queryNames));

        // Obtiene y establece los filtros de validación.
        $this->gump()->filter_rules(UserValidation::getFilters($queryNames));

        // Comprueba los query params de la petición.
        $queryParams = $this->gump()->run($queryParams);

        // Comprueba si existen errores de validación.
        if ($this->gump()->errors()) {
            $this->respondValidationErrors(
                $this->gump()->get_errors_array(),
                'The users search information is incorrect');
        }

        // Consulta la información de todos los "usuarios" con paginación.
        $users = new UserModel;
        $users->select(...$this->getColumns())
            ->like($queryParams['filterBy'], sprintf('%%%s%%', $queryParams['search']));

        // Filtra los "usuarios" por estatus de actividad.
        if (isset($queryParams['is_active'])) {
            $users->eq('is_active', $queryParams['is_active']);
        }

        // Obtiene la información sobre la paginación.
        $users->paginate($queryParams['page']);
        $pagination = $users->pagination;

        // Aplica los parámetros de ordenamiento.
        $users->orderBy(sprintf('%s %s', $queryParams['orderBy'], $queryParams['sortBy']));

        $this->respondPagination($users->findAll(), $pagination, 'Information about all the users with pagination');
    }

    /**
     * Registra la información de un "usuario".
     */
    public function create(): void
    {
        // Define los campos necesarios de la petición.
        $fields = ['email', 'username', 'role_id', 'fullname', 'password', 'password_confirm'];

        $data = [];

        // Obtiene solo los campos necesarios.
        foreach ($fields as $field) {
            $data[$field] = $this->app()->request()->data->{$field} ?? null;
        }

        // Obtiene las reglas de validación.
        $rules = UserValidation::getRules($fields);

        // Define todas las reglas de validación como obligatorias.
        foreach (array_keys($rules) as $rule) {
            array_unshift($rules[$rule], 'required');
        }

        // Establece las reglas de validación.
        $this->gump()->validation_rules($rules);

        // Establece los filtros de validación.
        $this->gump()->filter_rules(UserValidation::getFilters($fields));

        // Valida el cuerpo de la petición.
        $data = $this->gump()->run($data);

        unset($data['password_confirm']);

        // Comprueba si existen errores de validación.
        if ($this->gump()->errors()) {
            $this->respondValidationErrors(
                $this->gump()->get_errors_array(),
                'The user information is incorrect');
        }

        $user = new UserModel;

        // Comprueba que los valores de las columnas sean únicos.
        foreach (['username', 'email'] as $column) {
            $user->select('id')->eq($column, $data[$column])->find();

            if ($user->isHydrated()) {
                $this->respondResourceExists(
                    [$column => 'The '.$column.' field must be unique'],
                    'The user information is incorrect');
            }

            $user->reset();
        }

        // Registra la información del "usuario".
        $user->copyFrom($data);
        $user->insert();

        // Obtiene el ID del nuevo "usuario".
        $id = $user->id;

        // Obtiene la información del nuevo "usuario".
        $user->reset();
        $user->select(...$this->getColumns())->eq('id', $id)->find();

        // Consulta el "rol" del "usuario".
        $user->setCustomData('role', $user->role);
        unset($user->role_id);

        $this->respondCreated($user, 'The user was created successfully');
    }

    /**
     * Muestra la información de un "usuario".
     */
    public function show(string $id): void
    {
        // Obtiene las reglas de validación
        // y las establece como obligatorias.
        $rules = UserValidation::getRules(['id']);
        array_unshift($rules['id'], 'required');

        // Establece las reglas de validación.
        $this->gump()->validation_rules($rules);

        // Comprueba los parámetros de la petición.
        $this->gump()->run(['id' => $id]);

        // Comprueba si existen errores de validación.
        if ($this->gump()->errors()) {
            $this->respondValidationErrors(
                $this->gump()->get_errors_array(),
                'The user identifier is incorrect');
        }

        // Consulta la información del "usuario".
        $user = new UserModel;
        $user->select(...$this->getColumns())->eq('id', $id)->find();

        // Comprueba si existe el "usuario".
        if (! $user->isHydrated()) {
            $this->respondNotFound('The user information was not found');
        }

        // Consulta el "rol" del "usuario".
        $user->setCustomData('role', $user->role);
        unset($user->role_id);

        $this->respond($user, 'Information about the user');
    }

    /**
     * Modifica la información de un "usuario".
     */
    public function update(string $id): void
    {
        // Define los campos que se pueden modificar.
        $fields = ['email', 'username', 'role_id', 'fullname', 'password', 'password_confirm'];

        $data = ['id' => $id];

        // Obtiene solo los campos necesarios.
        foreach ($fields as $field) {
            $data[$field] = $this->app()->request()->data->{$field} ?? null;

            if (is_null($data[$field])) {
                unset($data[$field]);
            }
        }

        $fieldNames = array_keys($data);

        // Obtiene las reglas de validación
        // y establece el identificador como obligatorio.
        $rules = UserValidation::getRules(['id', ...$fieldNames]);
        array_unshift($rules['id'], 'required');

        // Establece las reglas de validación.
        $this->gump()->validation_rules($rules);

        // Establece los filtros de validación.
        $this->gump()->filter_rules(UserValidation::getFilters($fieldNames));

        // Valida el cuerpo de la petición.
        $data = $this->gump()->run($data);

        foreach (['id', 'password_confirm'] as $field) {
            unset($data[$field]);
        }

        // Comprueba si existen errores de validación.
        if ($this->gump()->errors()) {
            $this->respondValidationErrors(
                $this->gump()->get_errors_array(),
                'The user information is incorrect');
        }

        $user = new UserModel;

        // Comprueba que los valores de las columnas sean únicos.
        foreach (['username', 'email'] as $column) {
            $user->select('id')->eq($column, $data[$column])->find();

            if ($user->isHydrated()) {
                $this->respondResourceExists(
                    [$column => 'The '.$column.' field must be unique'],
                    'The user information is incorrect');
            }

            $user->reset();
        }

        // Consulta la información del "usuario".
        $user->select('id')->eq('id', $id)->find();

        // Comprueba si existe el "usuario".
        if (! $user->isHydrated()) {
            $this->respondNotFound('The user information was not found');
        }

        // Modifica la información del "usuario".
        if (! empty($data)) {
            $user->copyFrom($data);
            $user->update();
            $user->reset();
        }

        // Consulta la información actualizada del "usuario".
        $user->find($id);

        // Consulta el "rol" del "usuario".
        $user->setCustomData('role', $user->role);
        unset($user->role_id);

        $this->respondUpdated($user, 'The user was updated successfully');
    }

    /**
     * Elimina la información de un "usuario".
     */
    public function delete(string $id): void
    {
        // Obtiene las reglas de validación
        // y las establece como obligatorias.
        $rules = UserValidation::getRules(['id']);
        array_unshift($rules['id'], 'required');

        // Establece las reglas de validación.
        $this->gump()->validation_rules($rules);

        // Comprueba los parámetros de la petición.
        $this->gump()->run(['id' => $id]);

        // Comprueba si existen errores de validación.
        if ($this->gump()->errors()) {
            $this->respondValidationErrors(
                $this->gump()->get_errors_array(),
                'The user identifier is incorrect');
        }

        // Consulta la información del "usuario".
        $user = new UserModel;
        $user->select(...$this->getColumns())
            ->ne('id', $this->userAuth()->id)
            ->eq('id', $id)
            ->find();

        // Comprueba si existe el "usuario".
        if (! $user->isHydrated()) {
            $this->respondNotFound('The user information was not found');
        }

        // Consulta el "rol" del "usuario".
        $user->setCustomData('role', $user->role);
        unset($user->role_id);

        // Elimina la información del "jugador".
        $user->delete();

        $this->respondDeleted($user, 'The user was deleted successfully');
    }
}


code:
<?php

declare(strict_types=1);

namespace App\Helpers;

/**
 * Colección de herramientas
 * para la API key de autenticación.
 */
class Auth
{
    private const LENGTH = 32;

    private const ALGORITHM = 'sha512';

    private const ENV_VARNAME = 'APP_SECRET';

    private const EXPIRATION = '+8 minute';

    private const VARNAME = 'userAuth';

    /**
     * Genera una key de autenticación.
     */
    public static function generateKey(): string
    {
        return bin2hex(random_bytes(self::LENGTH));
    }

    /**
     * Obtiene la clave secreta compartida.
     */
    private static function getSecret(): string
    {
        return Env::get(self::ENV_VARNAME);
    }

    /**
     * Genera el hash de una key de autenticación.
     */
    public static function generateHash(string $key): string
    {
        return hash_hmac(self::ALGORITHM, $key, self::getSecret());
    }

    /**
     * Obtiene el entorno de ejecución de la aplicación.
     */
    private static function getEnvironment(): string
    {
        return Env::get('APP_ENVIRONMENT');
    }

    /**
     * Obtiene la fecha de expiración de la API Key.
     */
    public static function getExpiration(): string
    {
        $expiration = self::EXPIRATION;

        if (self::getEnvironment() === 'development') {
            $expiration = '+1 day';
        }

        return Date::strtodatetime($expiration);
    }

    /**
     * Comprueba si el hash de una key es autentica.
     */
    public static function verity(string $hash, string $original): bool
    {
        return hash_equals($hash, $original);
    }

    /**
     * Obtiene el nombre de la variable de autenticación.
     */
    public static function getVarname(): string
    {
        return self::VARNAME;
    }
}


code:
<?php

declare(strict_types=1);

namespace App\Helpers;

use RuntimeException;

/**
 * Colección de herramientas
 * para las opciones de configuración.
 */
class Config
{
    private const PATH = __DIR__.'/../../config/';

    /**
     * Obtiene las opciones de configuración de un archivo
     * desde la carpeta "config/" de la raíz del proyecto.
     */
    public static function getFromFilename(string $filename): array
    {
        $config = require self::PATH.$filename.'.php';

        if (! is_array($config)) {
            throw new RuntimeException(sprintf('The config options file "%s" is not an array.', $filename));
        }

        return $config;
    }
}


code:
<?php

declare(strict_types=1);

namespace App\Helpers;

use Infocyph\UID\UUID;
use PDO;

/**
 * Colección de herramientas
 * para la base de datos.
 */
class Database
{
    private const CONFIG_FILENAME = 'database';

    private static PDO $connection;

    /**
     * Obtiene el DSN de origen de la conexión de la base de datos.
     */
    private static function getDsn(array $options): string
    {
        $dsn = sprintf('%s:host=%s;port=%u;dbname=%s',
            $options['driver'],
            $options['host'],
            $options['port'],
            $options['database']);

        if ($options['driver'] === 'mysql') {
            $dsn = sprintf('%s;charset=%s', $dsn, $options['charset']);
        }

        return $dsn;
    }

    /**
     * Establece la conexión con la base de datos.
     */
    private static function setConnection(): void
    {
        $options = Config::getFromFilename(self::CONFIG_FILENAME);

        self::$connection = new PDO(self::getDsn($options), $options['username'], $options['password'], $options['options']);
    }

    /**
     * Obtiene una sola conexión de la base de datos.
     */
    public static function getConnection(): PDO
    {
        if (empty(self::$connection)) {
            self::setConnection();
        }

        return self::$connection;
    }

    /**
     * Obtiene un UUID aleatorio.
     */
    public static function getUuid(): string
    {
        return UUID::v4();
    }
}


code:
<?php

declare(strict_types=1);

namespace App\Helpers;

/**
 * Colección de herramientas
 * para fechas y horas.
 */
class Date
{
    private const DATETIME_FORMAT = 'Y-m-d H:i:s';

    /**
     * Obtiene el datetime del tiempo actual.
     */
    public static function getCurrentDateTime(): string
    {
        return date(self::DATETIME_FORMAT);
    }

    /**
     * Obtiene el datetime desde un formato de fecha y hora.
     */
    public static function strtodatetime(string $format): string
    {
        return date(self::DATETIME_FORMAT, strtotime($format));
    }

    /**
     * Obtiene el año actual.
     */
    public static function getCurrentYear(): int
    {
        return (int) date('Y');
    }
}


code:
<?php

declare(strict_types=1);

namespace App\Helpers;

use PhpDevCommunity\DotEnv;

/**
 * Colección de herramientas
 * para las variables de entorno.
 */
class Env
{
    private const PATH = __DIR__.'/../../.env';

    /**
     * Carga variables de entorno desde el archivo ".env".
     */
    public static function loadDotEnv(): void
    {
        (new DotEnv(self::PATH))->load();
    }

    /**
     * Establece el valor de una variable de entorno.
     */
    public static function set(string $varname, mixed $value = null): void
    {
        $_ENV[$varname] = $value;
    }

    /**
     * Obtiene el valor de una variable de entorno.
     */
    public static function get(string $varname, mixed $default = null): mixed
    {
        return $_ENV[$varname] ?? $_SERVER[$varname] ?? $default;
    }
}


code:
<?php

declare(strict_types=1);

namespace App\Helpers;

/**
 * Colección de herramientas
 * para contraseñas.
 */
class Password
{
    private const ALGORITHM = PASSWORD_DEFAULT;

    private const OPTIONS = ['cost' => 12];

    /**
     * Encripta una contraseña.
     */
    public static function encrypt(string $password): string
    {
        return password_hash($password, self::ALGORITHM, self::OPTIONS);
    }

    /**
     * Comprueba si una contraseña es autentica.
     */
    public static function verify(string $password, string $hash): bool
    {
        return password_verify($password, $hash);
    }
}


code:
<?php

declare(strict_types=1);

namespace App\Middlewares;

use App\Helpers\Auth;
use App\Helpers\Date;
use App\Models\ApiKeyModel;
use App\Validations\AuthValidation;

/**
 * Middleware que comprueba la "API key" de
 * autenticación del "usuario de acceso".
 */
class AuthMiddleware extends BaseMiddleware
{
    private const HEADER = 'X-API-KEY';

    public function before(array $params): void
    {
        // Obtiene la key del encabezado.
        $key = $this->app()->request()->header(self::HEADER);

        // Obtiene las reglas de validación
        // y las establece como obligatorias.
        $rules = AuthValidation::getRules(['api_key']);
        array_unshift($rules['api_key'], 'required');

        // Establece las reglas de validación.
        $this->gump()->validation_rules($rules);

        // Valida la key del encabezado.
        $this->gump()->run(['api_key' => $key]);

        // Comprueba si existen errores de validación.
        if ($this->gump()->errors()) {
            $this->respondValidationErrors(
                $this->gump()->get_errors_array(),
                'The API key information is incorrect');
        }

        $hash = Auth::generateHash($key);

        // Consula la información de la API key del usuario.
        $apiKey = new ApiKeyModel;
        $apiKey->select('id', 'user_id', 'hash', 'expires_at')
            ->eq('hash', $hash)
            ->eq('is_revoked', 0)
            ->find();

        // Comprueba si el hash de la key es autentica.
        if (! $apiKey->isHydrated() || ! Auth::verity($hash, $apiKey->hash)) {
            $this->respondUnauthorized('The API key is invalid');
        }

        // Comprueba si la key está expirada.
        if (strtotime(Date::getCurrentDateTime()) > strtotime($apiKey->expires_at)) {
            $apiKey->delete();
            $this->respondUnauthorized('The API key is expired');
        }

        $userAuth = $apiKey->user;

        // Comprueba si el "usuario de acceso" autenticado está activo.
        if (empty($userAuth->is_active)) {
            $this->respondForbidden('The authenticated user is not active');
        }

        // Consulta el "rol" del "usuario" autenticado.
        $userAuth->setCustomData('role', $userAuth->role);
        unset($userAuth->role_id);

        /**
         * Se pasa la variable "$this->app()->get('userAuth')"
         * a todos los Middlewares y Controladores
         * con la información del "usuario de acceso" autenticado.
         */
        $this->app()->set(Auth::getVarname(), $userAuth);
    }

    public function after(array $params): void {}
}


code:
<?php

declare(strict_types=1);

namespace App\Middlewares;

use App\Traits\HelperTrait;
use App\Traits\ResponseTrait;
use flight\Engine;

/**
 * Clase base para todos los Middlewares.
 */
abstract class BaseMiddleware
{
    use HelperTrait;
    use ResponseTrait;

    private Engine $app;

    /**
     * Método abstracto que se ejecuta antes de procesar una ruta.
     */
    abstract public function before(array $params): void;

    /**
     * Método abstracto que se ejecuta después de procesar una ruta.
     */
    abstract public function after(array $params): void;

    /**
     * Constructor de la clase.
     */
    public function __construct(Engine $app)
    {
        $this->app = $app;
    }

    /**
     * Obtiene la instancia de la aplicación.
     */
    protected function app(): Engine
    {
        return $this->app;
    }
}


code:
<?php

declare(strict_types=1);

namespace App\Middlewares;

/**
 * Middleware que comprueba el "rol" del "usuario de acceso" autenticado.
 */
class CheckRoleMiddleware extends BaseMiddleware
{
    private const ROLES = ['admin', 'web', 'reader', 'judge'];

    public function before(array $params): void
    {
        foreach (self::ROLES as $role) {
            if ($this->userAuth()->role->name === $role) {
                return;
            }
        }

        $this->respondForbidden('The authenticated user does not have a valid role');
    }

    public function after(array $params): void {}
}


code:
<?php

declare(strict_types=1);

namespace App\Middlewares;

use JustSteveKing\StatusCode\Http;

/**
 * Middleware que habilita CORS.
 */
class CorsMiddleware extends BaseMiddleware
{
    public function before(array $params): void
    {
        $request = $this->app()->request();
        $response = $this->app()->response();

        if ($request->method === 'OPTIONS') {
            // Define los métodos HTTP permitidos.
            if ($request->getVar('HTTP_ACCESS_CONTROL_REQUEST_METHOD')) {
                $response->header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD');
            }

            // Envía los encabezados en la respuesta.
            $response->status(Http::OK());
            $response->send();
            exit();
        }
    }

    public function after(array $params): void {}
}


code:
<?php

declare(strict_types=1);

namespace App\Middlewares;

/**
 * Middleware que comprueba que el "rol"
 * del "usuario de acceso" autenticado
 * sea administrador.
 */
class IsAdminRoleMiddleware extends BaseMiddleware
{
    public function before(array $params): void
    {
        if ($this->userAuth()->role->name !== 'admin') {
            $this->respondForbidden('The authenticated user is not administrator');
        }
    }

    public function after(array $params): void {}
}


code:
<?php

declare(strict_types=1);

namespace App\Middlewares;

/**
 * Middleware que comprueba que el "usuario de acceso" autenticado
 * tenga permisos de escritura (crear, editar, eliminar).
 */
class WritePermissionMiddleware extends BaseMiddleware
{
    private const ROLES = ['admin', 'judge'];

    public function before(array $params): void
    {
        foreach (self::ROLES as $role) {
            if ($this->userAuth()->role->name === $role) {
                return;
            }
        }

        $this->respondForbidden('The authenticated user does not have write permissions');
    }

    public function after(array $params): void {}
}


code:
<?php

declare(strict_types=1);

namespace App\Models;

use App\Helpers\Auth;

/**
 * Modelo que representa la tabla
 * de las "API keys de los usuarios de acceso".
 */
class ApiKeyModel extends BaseModel
{
    protected array $relations = [
        'user' => [
            self::BELONGS_TO,
            UserModel::class,
            'user_id',
            ['select' => ['id', 'email', 'username', 'role_id', 'fullname', 'is_active', 'created_at', 'updated_at']],
        ],
    ];

    public function getTableName(): string
    {
        return 'api_keys';
    }

    protected function beforeInsert(BaseModel $self): void
    {
        parent::beforeInsert($self);

        $self->is_revoked = 0;
        $self->expires_at = Auth::getExpiration();
    }
}


code:
<?php

declare(strict_types=1);

namespace App\Models;

use App\Helpers\Database;
use App\Helpers\Date;
use flight\ActiveRecord;

/**
 * Clase base para todos los Modelos.
 */
abstract class BaseModel extends ActiveRecord
{
    protected const LIMIT = 16;

    /**
     * Método abstracto para obtener el nombre de la tabla.
     */
    abstract public function getTableName(): string;

    public function __construct()
    {
        parent::__construct(Database::getConnection(), $this->getTableName());
    }

    /**
     * Establece campos antes de insertarlos.
     */
    protected function beforeInsert(self $self): void
    {
        $self->id = Database::getUuid();
        $self->created_at = Date::getCurrentDateTime();
        $self->updated_at = Date::getCurrentDateTime();
    }

    /**
     * Establece campos antes de modificarlos.
     */
    protected function beforeUpdate(self $self): void
    {
        $self->updated_at = Date::getCurrentDateTime();
    }

    /**
     * Establece la paginación de los resultados.
     */
    public function paginate(int $page, ?int $limit = null): self
    {
        $limit ??= self::LIMIT;

        // Obtiene el número total de registros.
        $copy = clone $this;
        $copy = $copy->select('COUNT(*) AS _count')->find();

        // Calcula el número total de páginas.
        $total = ceil($copy->_count / $limit);

        // Calcula la posición del registro donde inicia la paginación.
        $offset = ($page - 1) * $limit;

        // Establece la paginación.
        $this->limit($limit)->offset($offset);

        // Información de la paginación.
        $this->setCustomData('pagination', [
            'page' => $page,
            'limit' => $limit,
            'total' => $total,
            'count' => $copy->_count,
            'offset' => $offset,
        ]);

        return $this;
    }
}


code:
<?php

declare(strict_types=1);

namespace App\Models;

/**
 * Modelo que representa la tabla pivote
 * de los "partidos" de los "grupos".
 */
class GroupMatchPivotModel extends BaseModel
{
    protected array $relations = [
        '_group' => [self::BELONGS_TO, GroupModel::class, 'group_id'],
        'match' => [self::BELONGS_TO, MatchModel::class, 'match_id'],
    ];

    public function getTableName(): string
    {
        return 'groups_matches';
    }
}


code:
<?php

declare(strict_types=1);

namespace App\Models;

/**
 * Modelo que representa la tabla de los "grupos".
 */
class GroupModel extends BaseModel
{
    protected array $relations = [
        'registrationCategory' => [
            self::BELONGS_TO,
            RegistrationCategoryModel::class,
            'registration_category_id',
            ['select' => ['id', 'name', 'description']],
        ],
        'groupPairPivot' => [
            self::HAS_MANY,
            GroupPairPivotModel::class,
            'group_id',
            ['orderBy' => 'created_at DESC'],
        ],
        'groupMatchPivot' => [
            self::HAS_MANY,
            GroupMatchPivotModel::class,
            'group_id',
            ['orderBy' => 'created_at DESC'],
        ],
    ];

    public function getTableName(): string
    {
        return 'groups';
    }

    protected function beforeInsert(BaseModel $self): void
    {
        parent::beforeInsert($self);

        $self->is_eliminated = 0;
        $self->is_active = 1;
    }
}


code:
<?php

declare(strict_types=1);

namespace App\Models;

/**
 * Modelo que representa la tabla pivote
 * de las "parejas de los grupos".
 */
class GroupPairPivotModel extends BaseModel
{
    protected array $relations = [
        '_group' => [self::BELONGS_TO, GroupModel::class, 'group_id'],
        'pair' => [self::BELONGS_TO, PairModel::class, 'pair_id'],
    ];

    public function getTableName(): string
    {
        return 'groups_pairs';
    }
}


code:
<?php

declare(strict_types=1);

namespace App\Models;

/**
 * Modelo que representa la tabla
 * de las "categorías de los partidos".
 */
class MatchCategoryModel extends BaseModel
{
    protected array $relations = [
        'matches' => [
            self::HAS_MANY,
            MatchModel::class,
            'match_category_id',
            ['orderBy' => 'created_at DESC'],
        ],
    ];

    public function getTableName(): string
    {
        return 'match_categories';
    }
}


code:
<?php

declare(strict_types=1);

namespace App\Models;

/**
 * Modelo que representa la tabla de "partidos".
 */
class MatchModel extends BaseModel
{
    protected array $relations = [
        'registrationCategory' => [
            self::BELONGS_TO,
            RegistrationCategoryModel::class,
            'registration_category_id',
            ['select' => ['id', 'name', 'description']],
        ],
        'groupMatchPivot' => [
            self::HAS_ONE,
            GroupMatchPivotModel::class,
            'match_id',
        ],
        'matchCategory' => [
            self::BELONGS_TO,
            MatchCategoryModel::class,
            'match_category_id',
            ['select' => ['id', 'name', 'description']],
        ],
        'matchStatus' => [
            self::BELONGS_TO,
            MatchStatusModel::class,
            'match_status_id',
            ['select' => ['id', 'name', 'description']],
        ],
        'matchPairPivot' => [
            self::HAS_MANY,
            MatchPairPivotModel::class,
            'match_id',
            ['orderBy' => 'created_at DESC'],
        ],
    ];

    public function getTableName(): string
    {
        return 'matches';
    }

    protected function beforeInsert(BaseModel $self): void
    {
        parent::beforeInsert($self);

        $self->is_active = 1;
    }
}


code:
<?php

declare(strict_types=1);

namespace App\Models;

/**
 * Modelo que representa la tabla pivote
 * de los "partidos de las parejas".
 */
class MatchPairPivotModel extends BaseModel
{
    protected array $relations = [
        'match' => [self::BELONGS_TO, MatchModel::class, 'match_id'],
        'pair' => [self::BELONGS_TO, PairModel::class, 'pair_id'],
    ];

    public function getTableName(): string
    {
        return 'matches_pairs';
    }

    protected function beforeInsert(BaseModel $self): void
    {
        parent::beforeInsert($self);

        $self->score = 0;
        $self->is_winner = 0;
    }
}


code:
<?php

declare(strict_types=1);

namespace App\Models;

/**
 * Modelo que representa la tabla
 * de las "categorías de los partidos".
 */
class MatchStatusModel extends BaseModel
{
    protected array $relations = [
        'matches' => [
            self::HAS_MANY,
            MatchModel::class,
            'match_status_id',
            ['orderBy' => 'created_at DESC'],
        ],
    ];

    public function getTableName(): string
    {
        return 'match_status';
    }
}


code:
<?php

declare(strict_types=1);

namespace App\Models;

/**
 * Modelo que representa la tabla
 * de las "parejas de jugadores".
 */
class PairModel extends BaseModel
{
    protected array $relations = [
        'registrationCategory' => [
            self::BELONGS_TO,
            RegistrationCategoryModel::class,
            'registration_category_id',
            ['select' => ['id', 'name', 'description']],
        ],
        'pairPlayerPivot' => [
            self::HAS_MANY,
            PairPlayerPivotModel::class,
            'pair_id',
            ['orderBy' => 'created_at DESC'],
        ],
        'matchPairPivot' => [
            self::HAS_MANY,
            MatchPairPivotModel::class, 'pair_id',
            ['orderBy' => 'created_at DESC'],
        ],
        'groupPairPivot' => [self::HAS_ONE, GroupPairPivotModel::class, 'pair_id'],
    ];

    public function getTableName(): string
    {
        return 'pairs';
    }

    protected function beforeInsert(BaseModel $self): void
    {
        parent::beforeInsert($self);

        $self->is_eliminated = 0;
        $self->is_active = 1;
    }
}


code:
<?php

declare(strict_types=1);

namespace App\Models;

/**
 * Modelo que representa la tabla pivote
 * de los "jugadores de las parejas".
 */
class PairPlayerPivotModel extends BaseModel
{
    protected array $relations = [
        'pair' => [self::BELONGS_TO, PairModel::class, 'pair_id'],
        'player' => [self::BELONGS_TO, PlayerModel::class, 'player_id'],
    ];

    public function getTableName(): string
    {
        return 'pairs_players';
    }
}


code:
<?php

declare(strict_types=1);

namespace App\Models;

/**
 * Modelo que representa la tabla de "jugadores".
 */
class PlayerModel extends BaseModel
{
    protected array $relations = [
        'pairPlayerPivot' => [self::HAS_ONE, PairPlayerPivotModel::class, 'player_id'],
    ];

    public function getTableName(): string
    {
        return 'players';
    }

    protected function beforeInsert(BaseModel $self): void
    {
        parent::beforeInsert($self);

        $self->experience ??= 0;
        $self->is_active = 1;
    }
}


code:
<?php

declare(strict_types=1);

namespace App\Models;

/**
 * Modelo que representa la tabla
 * de las "categorías de inscripción de las parejas de jugadores".
 */
class RegistrationCategoryModel extends BaseModel
{
    protected array $relations = [
        'pairs' => [
            self::HAS_MANY, PairModel::class,
            'registration_category_id',
            ['eq' => ['is_active', 1], 'orderBy' => 'created_at DESC'],
        ],
        'groups' => [
            self::HAS_MANY, GroupModel::class,
            'registration_category_id',
            ['eq' => ['is_active', 1], 'orderBy' => 'name ASC'],
        ],
        'matches' => [
            self::HAS_MANY,
            MatchModel::class,
            'registration_category_id',
            ['eq' => ['is_active', 1], 'orderBy' => 'created_at DESC'],
        ],
    ];

    public function getTableName(): string
    {
        return 'registration_categories';
    }
}


code:
<?php

declare(strict_types=1);

namespace App\Models;

/**
 * Modelo que representa la tabla
 * de los "roles de los usuarios de acceso".
 */
class RoleModel extends BaseModel
{
    protected array $relations = [
        'users' => [
            self::HAS_MANY,
            UserModel::class,
            'role_id',
            ['select' => ['id', 'email', 'username', 'role_id', 'fullname', 'is_active', 'created_at', 'updated_at'], 'orderBy' => 'created_at DESC'],
        ],
    ];

    public function getTableName(): string
    {
        return 'roles';
    }
}


code:
<?php

declare(strict_types=1);

namespace App\Models;

use App\Helpers\Password;

/**
 * Modelo que representa la tabla
 * de los "usuarios de acceso".
 */
class UserModel extends BaseModel
{
    protected array $relations = [
        'role' => [
            self::BELONGS_TO, RoleModel::class,
            'role_id',
            ['select' => ['id', 'name', 'description']],
        ],
        'apiKey' => [
            self::HAS_ONE, ApiKeyModel::class,
            'user_id',
            ['select' => ['id', 'user_id', 'is_revoked', 'expires_at', 'created_at', 'updated_at']],
        ],
    ];

    public function getTableName(): string
    {
        return 'users';
    }

    protected function beforeInsert(BaseModel $self): void
    {
        parent::beforeInsert($self);

        $self->password = Password::encrypt($self->password);
        $self->is_active = 1;
    }
}


code:
<?php

declare(strict_types=1);

namespace App\Traits;

use App\Helpers\Auth;
use App\Models\UserModel;
use flight\Engine;
use GUMP;

/**
 * Extensión que agrega atajos cortos
 * para herramientas y ayudas en común.
 */
trait HelperTrait
{
    /**
     * Método abstractor para obtener una instancia
     * existente de la aplicación.
     */
    abstract protected function app(): Engine;

    /**
     * Obtiene la instancia de las validaciones.
     */
    protected function gump(): GUMP
    {
        return $this->app()->gump();
    }

    /**
     * Obtiene la información del usuario de acceso autenticado.
     */
    protected function userAuth(): ?UserModel
    {
        return $this->app()->get(Auth::getVarname());
    }
}


code:
<?php

declare(strict_types=1);

namespace App\Traits;

use flight\Engine;
use JustSteveKing\StatusCode\Http;

/**
 * Extensión para realizar respuestas HTTP simples.
 */
trait ResponseTrait
{
    private array $body = [];

    /**
     * Método abstractor para obtener una instancia
     * existente de la aplicación.
     */
    abstract protected function app(): Engine;

    /**
     * Genera una respuesta genérica.
     */
    protected function respond(mixed $data, string $description, ?int $status = null): void
    {
        $status ??= Http::OK();

        $response = [
            'data' => $data,
            'status' => $status,
            ...$this->body,
            'description' => $description,
        ];

        $this->body = [];

        $this->app()->jsonHalt($response, $status);
    }

    /**
     * Genera una respuesta genérica ante fallos.
     */
    protected function respondFail(string $description, ?int $status, string $error): void
    {
        $status ??= Http::BAD_REQUEST();

        $this->body['error'] = $error;

        $this->respond(null, $description, $status);
    }

    /**
     * Genera una respuesta de errores de servidor.
     */
    protected function respondServerError(array $trace, string $description, string $error = 'Internal Server Error'): void
    {
        $this->body['stack_traces'] = $trace;

        $this->respondFail($description, Http::INTERNAL_SERVER_ERROR(), $error);
    }

    /**
     * Genera una respuesta cuando un servicio no está disponible.
     */
    protected function respondServiceUnavailable(string $description, string $error = 'Service Unavailable'): void
    {
        $this->respondFail($description, Http::SERVICE_UNAVAILABLE(), $error);
    }

    /**
     * Genera una respuesta de errores de validación.
     */
    protected function respondValidationErrors(array $validations, string $description, string $error = 'Validation error'): void
    {
        $this->body['validations'] = $validations;

        $this->respondFail($description, Http::BAD_REQUEST(), $error);
    }

    /**
     * Genera una respuesta al crear un nuevo recurso.
     */
    protected function respondCreated(mixed $data, string $description): void
    {
        $this->respond($data, $description, Http::CREATED());
    }

    /**
     * Genera una respuesta de conflicto.
     */
    protected function respondConflict(string $description, string $error = 'Conflict'): void
    {
        $this->respondFail($description, Http::CONFLICT(), $error);
    }

    /**
     * Genera una respuesta cuando se intenta
     * crear un nuevo recurso que ya existe.
     */
    protected function respondResourceExists(array $validations, string $description, string $error = 'Resource exists'): void
    {
        $this->body['validations'] = $validations;

        $this->respondConflict($description, $error);
    }

    /**
     * Genera una respuesta al modificar un recurso.
     */
    protected function respondUpdated(mixed $data, string $description): void
    {
        $this->respond($data, $description, Http::OK());
    }

    /**
     * Genera una respuesta al eliminar un recurso.
     */
    protected function respondDeleted(mixed $data, string $description): void
    {
        $this->respond($data, $description, Http::OK());
    }

    /**
     * Genera una respuesta al no encontrar un recurso.
     */
    protected function respondNotFound(string $description, string $error = 'Not Found'): void
    {
        $this->respondFail($description, Http::NOT_FOUND(), $error);
    }

    /**
     * Genera una respuesta cuando el cliente no tiene
     * las credenciales de acceso o son incorrectas.
     */
    protected function respondUnauthorized(string $description, string $error = 'Unauthorized'): void
    {
        $this->respondFail($description, Http::UNAUTHORIZED(), $error);
    }

    /**
     * Genera una respuesta cuando el cliente no tiene
     * los permisos adecuados para acceder a un recurso.
     */
    protected function respondForbidden(string $description, string $error = 'Forbidden'): void
    {
        $this->respondFail($description, Http::FORBIDDEN(), $error);
    }

    /**
     * Genera una respuesta exitosa sin enviar información al cliente.
     */
    protected function respondNoContent(string $description): void
    {
        // $this->respond(null, $description, Http::NO_CONTENT());
        $this->respond(null, $description);
    }

    /**
     * Genera una respuesta para paginación.
     */
    protected function respondPagination(mixed $data, mixed $pagination, string $description): void
    {
        $this->body['pagination'] = $pagination;

        $this->respond($data, $description, Http::OK());
    }

    public function respondDependecyError(string $description, string $error = 'Failed Dependency'): void
    {
        $this->respondFail($description, Http::FAILED_DEPENDENCY(), $error);
    }
}


code:
<?php

declare(strict_types=1);

namespace App\Validations;

/**
 * Define las reglas de validación de la API Key.
 */
class AuthValidation extends BaseValidation
{
    public static function getAllRules(): array
    {
        return [
            'api_key' => ['regex' => '/^\S+$/', 'between_len' => [1, 255]],
        ];
    }

    public static function getAllFilters(): array
    {
        return [];
    }
}


code:
<?php

declare(strict_types=1);

namespace App\Validations;

/**
 * Clase base para todas las reglas de validación.
 */
abstract class BaseValidation
{
    /**
     * Método abstracto para obtener todas las reglas de validación.
     */
    abstract public static function getAllRules(): array;

    /**
     * Método abstracto para obtener todos los filtros de validación.
     */
    abstract public static function getAllFilters(): array;

    /**
     * Obtiene las reglas de validación.
     */
    public static function getRules(array $fields): array
    {
        $result = [];
        $rules = static::getAllRules();

        foreach ($fields as $field) {
            if (isset($rules[$field])) {
                $result[$field] = $rules[$field];
            }
        }

        return $result;
    }

    /**
     * Obtiene los filtros de validación.
     */
    public static function getFilters(array $fields): array
    {
        $result = [];
        $filters = static::getAllFilters();

        foreach ($fields as $field) {
            if (isset($filters[$field])) {
                $result[$field] = $filters[$field];
            }
        }

        return $result;
    }
}


code:
<?php

declare(strict_types=1);

namespace App\Validations;

/**
 * Define las reglas de validación de las "grupos de parejas".
 */
class GroupValidation extends BaseValidation
{
    public static function getAllRules(): array
    {
        return [
            'id' => ['guidv4'],
            'registration_category_id' => ['guidv4', 'contains_list' => PairValidation::getRegistrationCategories()],
            'name' => ['alpha_dash', 'between_len' => [1, 32]],
            'description' => ['alpha_space', 'between_len' => [1, 64]],
            'is_eliminated' => ['boolean'],
            'is_active' => ['boolean'],
            'page' => ['integer', 'min_numeric' => 1],
            'search' => ['max_len' => 64],
            'filterBy' => ['contains' => ['name', 'description']],
            'orderBy' => ['contains' => ['name', 'description', 'created_at', 'updated_at']],
            'sortBy' => ['contains' => ['asc', 'desc']],
        ];
    }

    public static function getAllFilters(): array
    {
        return [
            'is_eliminated' => 'boolean|whole_number',
            'is_active' => 'boolean|whole_number',
            'page' => 'whole_number',
            'search' => 'trim',
            'sortBy' => 'upper_case',
        ];
    }
}


code:
<?php

declare(strict_types=1);

namespace App\Validations;

/**
 * Define las reglas de validación
 * de las "categorías de las rondas de los partidos".
 */
class MatchCategoryValidation extends BaseValidation
{
    public static function getAllRules(): array
    {
        return [
            'id' => ['guidv4'],
            'name' => ['alpha_dash', 'between_len' => [1, 32]],
            'description' => ['alpha_space', 'between_len' => [1, 64]],
        ];
    }

    public static function getAllFilters(): array
    {
        return [];
    }
}


code:
<?php

declare(strict_types=1);

namespace App\Validations;

/**
 * Define las reglas de validación de las "parejas" de los "partidos".
 */
class MatchPairValidation extends BaseValidation
{
    public static function getAllRules(): array
    {
        return [
            'id' => ['guidv4'],
            'pair_id' => ['guidv4'],
            'match_id' => ['guidv4'],
            'score' => ['integer', 'min_numeric' => 0, 'max_numeric' => 255],
            'is_winner' => ['boolean'],
        ];
    }

    public static function getAllFilters(): array
    {
        return [
            'is_winner' => 'boolean|whole_number',
        ];
    }
}


code:
<?php

declare(strict_types=1);

namespace App\Validations;

/**
 * Define las reglas de validación
 * de los "estatus de juego de los partidos".
 */
class MatchStatusValidation extends BaseValidation
{
    public static function getAllRules(): array
    {
        return [
            'id' => ['guidv4'],
            'name' => ['alpha_dash', 'between_len' => [1, 32]],
            'description' => ['alpha_space', 'between_len' => [1, 64]],
        ];
    }

    public static function getAllFilters(): array
    {
        return [];
    }
}


code:
<?php

declare(strict_types=1);

namespace App\Validations;

use App\Models\MatchCategoryModel;
use App\Models\MatchStatusModel;

/**
 * Define las reglas de validación de los "partidos".
 */
class MatchValidation extends BaseValidation
{
    private const NUM_PAIRS = 2;

    private static array $matchCategoriesIds = [];

    private static array $matchStatusIds = [];

    public static function getAllRules(): array
    {
        return [
            'id' => ['guidv4'],
            'registration_category_id' => ['guidv4', 'contains_list' => PairValidation::getRegistrationCategories()],
            'match_category_id' => ['guidv4', 'contains_list' => self::getMatchCategories()],
            'match_status_id' => ['guidv4', 'contains_list' => self::getMatchStatus()],
            'is_active' => ['boolean'],
            'page' => ['integer', 'min_numeric' => 1],
            'orderBy' => ['contains' => ['created_at', 'updated_at']],
            'sortBy' => ['contains' => ['asc', 'desc']],
            'pairs' => ['valid_array_size_equal' => self::getNumPairs(), 'guidv4'],
        ];
    }

    public static function getAllFilters(): array
    {
        return [
            'is_active' => 'boolean|whole_number',
            'page' => 'whole_number',
            'sortBy' => 'upper_case',
        ];
    }

    /**
     * Obtiene el número de "contrincantes" en un "partido".
     */
    public static function getNumPairs(): int
    {
        return self::NUM_PAIRS;
    }

    /**
     * Obtiene los IDs de las "categorías de los partidos".
     */
    public static function getMatchCategories(): array
    {
        if (empty(self::$matchCategoriesIds)) {
            $categories = (new MatchCategoryModel)->select('id')->findAll();

            self::$matchCategoriesIds = array_column($categories, 'id');
        }

        return self::$matchCategoriesIds;
    }

    /**
     * Obtiene los IDs de los "estatus de juego de los partidos".
     */
    public static function getMatchStatus(): array
    {
        if (empty(self::$matchStatusIds)) {
            $status = (new MatchStatusModel)->select('id')->findAll();

            self::$matchStatusIds = array_column($status, 'id');
        }

        return self::$matchStatusIds;
    }
}


code:
<?php

declare(strict_types=1);

namespace App\Validations;

/**
 * Define las reglas de validación de las "parejas con sus jugadores".
 */
class PairPlayerValidation extends BaseValidation
{
    public static function getAllRules(): array
    {
        return [
            'registration_category_id' => ['guidv4', 'contains_list' => PairValidation::getRegistrationCategories()],
            'players' => ['valid_array_size_equal' => PairValidation::getNumPlayers()],
        ];
    }

    public static function getAllFilters(): array
    {
        return [];
    }

    /**
     * Obtiene las reglas de validación de los "jugadores".
     */
    public static function getPlayersRules(array $fields): array
    {
        $result = [];

        foreach (PlayerValidation::getRules($fields) as $field => $value) {
            $result['players.*.'.$field] = $value;
        }

        return $result;
    }

    /**
     * Obtiene los filtros de validación de los "jugadores".
     */
    public static function getPlayersFilters(array $fields): array
    {
        $result = [];

        foreach (PlayerValidation::getFilters($fields) as $field => $value) {
            $result['players.*.'.$field] = $value;
        }

        return $result;
    }
}


code:
<?php

declare(strict_types=1);

namespace App\Validations;

use App\Models\RegistrationCategoryModel;

/**
 * Define las reglas de validación de las "parejas de jugadores".
 */
class PairValidation extends BaseValidation
{
    private const NUM_PLAYERS = 2;

    private static array $registrationCategoriesIds = [];

    public static function getAllRules(): array
    {
        return [
            'id' => ['guidv4'],
            'registration_category_id' => ['guidv4', 'contains_list' => self::getRegistrationCategories()],
            'is_eliminated' => ['boolean'],
            'is_active' => ['boolean'],
            'page' => ['integer', 'min_numeric' => 1],
            'orderBy' => ['contains' => ['created_at', 'updated_at']],
            'sortBy' => ['contains' => ['asc', 'desc']],
            'players' => ['valid_array_size_equal' => self::getNumPlayers(), 'guidv4'],
        ];
    }

    public static function getAllFilters(): array
    {
        return [
            'is_eliminated' => 'boolean|whole_number',
            'is_active' => 'boolean|whole_number',
            'page' => 'whole_number',
            'sortBy' => 'upper_case',
        ];
    }

    /**
     * Obtiene el número de "jugadores" permitidos en una "pareja".
     */
    public static function getNumPlayers(): int
    {
        return self::NUM_PLAYERS;
    }

    /**
     * Obtiene los IDs de las "categorías de inscripción de las parejas de jugadores".
     */
    public static function getRegistrationCategories(): array
    {
        if (empty(self::$registrationCategoriesIds)) {
            $categories = (new RegistrationCategoryModel)->select('id')->findAll();

            self::$registrationCategoriesIds = array_column($categories, 'id');
        }

        return self::$registrationCategoriesIds;
    }
}


code:
<?php

declare(strict_types=1);

namespace App\Validations;

/**
 * Define las reglas de validación de los "jugadores".
 */
class PlayerValidation extends BaseValidation
{
    public static function getAllRules(): array
    {
        return [
            'id' => ['guidv4'],
            'fullname' => ['alpha_space', 'between_len' => [1, 128]],
            'city' => ['alpha_space', 'between_len' => [1, 128]],
            'weight' => ['numeric', 'regex' => '/^\d+(\.\d{1,2})?$/', 'min_numeric' => 20, 'max_numeric' => 600],
            'height' => ['numeric', 'regex' => '/^\d+(\.\d{1,2})?$/', 'min_numeric' => 0.5, 'max_numeric' => 2.5],
            'age' => ['integer', 'min_numeric' => 14, 'max_numeric' => 122],
            'experience' => ['integer', 'min_numeric' => 0, 'max_numeric' => 50],
            'is_active' => ['boolean'],
            'page' => ['integer', 'min_numeric' => 1],
            'search' => ['max_len' => 128],
            'filterBy' => ['contains' => ['fullname']],
            'orderBy' => ['contains' => ['fullname', 'weight', 'height', 'age', 'experience', 'created_at', 'updated_at']],
            'sortBy' => ['contains' => ['asc', 'desc']],
        ];
    }

    public static function getAllFilters(): array
    {
        return [
            'fullname' => 'trim',
            'city' => 'trim',
            'is_active' => 'boolean|whole_number',
            'page' => 'whole_number',
            'search' => 'trim',
            'sortBy' => 'upper_case',
        ];
    }
}


code:
<?php

declare(strict_types=1);

namespace App\Validations;

/**
 * Define las reglas de validación
 * de las "categorías de inscripción de las parejas de jugadores".
 */
class RegistrationCategoryValidation extends BaseValidation
{
    public static function getAllRules(): array
    {
        return [
            'id' => ['guidv4'],
            'name' => ['alpha_dash', 'between_len' => [1, 32]],
            'description' => ['alpha_space', 'between_len' => [1, 64]],
        ];
    }

    public static function getAllFilters(): array
    {
        return [];
    }
}


code:
<?php

declare(strict_types=1);

namespace App\Validations;

/**
 * Define las reglas de validación
 * de los "roles de usuario de acceso".
 */
class RoleValidation extends BaseValidation
{
    public static function getAllRules(): array
    {
        return [
            'id' => ['guidv4'],
            'name' => ['alpha_dash', 'between_len' => [1, 32]],
            'description' => ['alpha_space', 'between_len' => [1, 64]],
        ];
    }

    public static function getAllFilters(): array
    {
        return [];
    }
}


code:
<?php

namespace App\Validations;

use App\Models\RoleModel;

/**
 * Define las reglas de validación de los "usuarios de acceso".
 */
class UserValidation extends BaseValidation
{
    private static array $rolesIds = [];

    public static function getAllRules(): array
    {
        return [
            'id' => ['guidv4'],
            'email' => ['valid_email', 'between_len' => [1, 128]],
            'username' => ['alpha_numeric_dash', 'between_len' => [1, 64]],
            'role_id' => ['guidv4', 'contains_list' => self::getRoles()],
            'fullname' => ['alpha_space', 'between_len' => [1, 128]],
            'password' => ['regex' => '/^\S+$/', 'between_len' => [8, 64]],
            'is_active' => ['boolean'],
            'password_confirm' => ['equalsfield' => 'password'],
            'page' => ['integer', 'min_numeric' => 1],
            'search' => ['max_len' => 128],
            'filterBy' => ['contains' => ['fullname', 'email', 'username']],
            'orderBy' => ['contains' => ['fullname', 'email', 'username', 'created_at', 'updated_at']],
            'sortBy' => ['contains' => ['asc', 'desc']],
        ];
    }

    public static function getAllFilters(): array
    {
        return [
            'fullname' => 'trim',
            'email' => 'lower_case',
            'is_active' => 'boolean|whole_number',
            'page' => 'whole_number',
            'search' => 'trim',
            'sortBy' => 'upper_case',
        ];
    }

    /**
     * Obtiene los IDs de los "roles de los usuarios de acceso".
     */
    private static function getRoles()
    {
        if (empty(self::$rolesIds)) {
            $roles = (new RoleModel)->select('id')->findAll();

            self::$rolesIds = array_column($roles, 'id');
        }

        return self::$rolesIds;
    }
}


description: API RESTful del evento Torneo de las Fresas Irapuato 2025

type: project

App\: app/


_readme: This file locks the dependencies of your project to a known state

_readme: Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies

flightphp/active-record

type: git

type: zip

type: library

flight\: src/

Lloyd Zhou

role: Original Developer

role: Maintainer

description: Micro Active Record library in PHP, support chain calls, events, and relations.

keywords:
Active Record
Simple
database
lite
micro
orm
pdo
relation

flightphp/core

latte/latte: Latte template engine

phpstan/phpstan: PHP Static Analyzer

tracy/tracy: Tracy debugger

description: Flight is a fast, simple, extensible framework for PHP. Flight enables you to quickly and easily build RESTful web applications. This is the maintained fork of mikecao/flight

infocyph/uid

Infocyph\UID\: src/

description: UUID (RFC 4122 + Unofficial/Draft), ULID, Snowflake ID, Sonyflake ID, TBSL (library exclusive) generator!

keywords: id key rfc4122 snowflake sonyflake tbsl ulid unique uuid

juststeveking/http-status-code

JustSteveKing\StatusCode\: src/

Steve McDougall

role: Developer

description: A simple class to return correct status codes for http responses

type: github

lulco/phoenix

nette/neon: Allows use *.neon file as config file and class Phoenix\Config\Parser\NeonConfigParser to parse it

symfony/yaml: Allows use *.yml / *.yaml file as config file and class Phoenix\Config\Parser\YamlConfigParser to parse it

Phoenix\: src/

description: Database Migrations for PHP

keywords:
Multiple folders
create
database
dump
migrate
migrations
php
query builder
rollback
status

nategood/httpful

Httpful: src/

description: A Readable, Chainable, REST friendly, PHP HTTP Client

keywords: api curl http requests rest restful

phpdevcommunity/php-dotenv

PhpDevCommunity\: src

Test\PhpDevCommunity\: tests

description: PHP-DotEnv is a lightweight PHP library designed to simplify the management of environment variables in your PHP applications.

psr/container

Psr\Container\: src/

description: Common Container Interface (PHP FIG PSR-11)

keywords: PSR-11 container container-interface container-interop psr

symfony/console

description: Eases the creation of beautiful and testable command line interfaces

keywords: cli command-line console terminal

type: custom

type: tidelift

symfony/deprecation-contracts

description: A generic function and convention to trigger deprecation notices

symfony/finder

description: Finds files and directories via an intuitive fluent interface

symfony/polyfill-ctype

ext-ctype: For best performance

description: Symfony polyfill for ctype functions

keywords: compatibility ctype polyfill portable

symfony/polyfill-intl-grapheme

ext-intl: For best performance

description: Symfony polyfill for intl's grapheme_* functions

keywords: compatibility grapheme intl polyfill portable shim

symfony/polyfill-intl-normalizer

description: Symfony polyfill for intl's Normalizer class and related functions

keywords: compatibility intl normalizer polyfill portable shim

symfony/polyfill-mbstring

ext-mbstring: For best performance

description: Symfony polyfill for the Mbstring extension

keywords: compatibility mbstring polyfill portable shim

symfony/service-contracts

description: Generic abstractions related to writing services

keywords: abstractions contracts decoupling interfaces interoperability standards

symfony/string

description: Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way

wixel/gump

description: A fast, extensible & stand-alone PHP input validation class that allows you to validate any data.

laravel/pint

description: An opinionated code formatter for PHP.


code:
<?php

use App\Helpers\Env;

/**
 * Opciones de configuración de la aplicación.
 */

// Establece la zona horario para todas las funciones de fecha y hora.
date_default_timezone_set(Env::get('APP_TIMEZONE', 'America/Mexico_City'));

return [
    'environment' => Env::get('APP_ENVIRONMENT', 'production'),
    'base_url' => Env::get('APP_BASE_URL', '/'),
    'secret' => Env::get('APP_SECRET', ''),
    'data_model_url' => Env::get('APP_DATA_MODEL_URL', 'http://localhost:3000'),
];


code:
<?php

require __DIR__.'/../vendor/autoload.php';

/**
 * Configuración y arranque de la aplicación.
 */

/**
 * Carga variables de entorno desde el archivo ".env".
 */
\App\Helpers\Env::loadDotEnv();

/**
 * Carga opciones de configuración de la aplicación.
 */
foreach (\App\Helpers\Config::getFromFilename('app') as $key => $value) {
    \App\Helpers\Env::set('APP_'.strtoupper($key), $value);
}

/**
 * Crea una instancia del framework.
 */
$app = \Flight::app();

/**
 * Establece opciones de configuración del framework.
 */
foreach (\App\Helpers\Config::getFromFilename('framework') as $key => $value) {
    $app->set($key, $value);
}

/**
 * Carga opciones de configuración de los servicios del framework.
 */
foreach (\App\Helpers\Config::getFromFilename('services') as $option) {
    $app->register($option['name'], $option['class'], $option['params'] ?? null);
}

/**
 * Manejo de errores.
 */
$app->map('notFound', [new \App\Controllers\ApiController($app), 'notFound']); // 404.
$app->map('error', [new \App\Controllers\ApiController($app), 'errors']); // 500.

/**
 * Habilita CORS.
 */
$app->before('start', [new \App\Middlewares\CorsMiddleware($app), 'before']);

/**
 * Carga las rutas y middlewares de la aplicación.
 *
 * Se pasa la variable "$app" al archivo.
 */
require __DIR__.'/routes.php';

/**
 * Inicia la ejecución del framework.
 */
$app->start();


code:
<?php

declare(strict_types=1);

use App\Helpers\Env;

/**
 * Opciones de configuración de la base de datos.
 */
return (static function (): array {
    $options = [
        'driver' => Env::get('DB_DRIVER', 'mysql'),
        'host' => Env::get('DB_HOST', '127.0.0.1'),
        'port' => Env::get('DB_PORT', 3306),
        'database' => Env::get('DB_DATABASE', 'torneo_fresas'),
        'username' => Env::get('DB_USERNAME', 'TDLF2025'),
        'password' => Env::get('DB_PASSWORD', 'secret'),
        'charset' => Env::get('DB_CHARSET', 'utf8mb4'),
        'collation' => Env::get('DB_COLLATION', 'utf8mb4_general_ci'),
    ];

    $options['options'] = match ($options['driver']) {
        'mysql' => [
            PDO::MYSQL_ATTR_INIT_COMMAND => sprintf('SET NAMES %s COLLATE %s', $options['charset'], $options['collation']),
            // PDO::ATTR_AUTOCOMMIT => true,
        ],
        default => [],
    };

    return $options;
})();


code:
<?php

use App\Helpers\Env;

/**
 * Opciones de configuración del framework.
 */
return [
    'flight.base_url' => Env::get('APP_BASE_URL'),
    'flight.case_sensitive' => true,
    'flight.log_errors' => true,
    'flight.handle_errors' => true,
    'flight.content_length' => true,
];