Контроллер аутентификации​

PHP:
<?php
// app/Controllers/AuthController.php
namespace App\Controllers;

use App\Core\Controller;
use App\Models\User;

class AuthController extends Controller
{
    public function loginForm()
    {
        if ($this->getCurrentUser()) {
            $this->redirect('/tasks');
        }
       
        $this->render('auth/login');
    }

    public function login()
    {
        $errors = $this->validate($_POST, [
            'username' => 'required',
            'password' => 'required'
        ]);
       
        if (!empty($errors)) {
            $this->render('auth/login', [
                'errors' => $errors,
                'old' => $_POST
            ]);
            return;
        }
       
        $user = User::authenticate($_POST['username'], $_POST['password']);
       
        if ($user) {
            session_start();
            $_SESSION['user'] = $user->toArray();
           
            if (isset($_POST['remember'])) {
                setcookie('user_id', $user->id, time() + 86400 * 30, '/');
            }
           
            $this->redirect('/tasks');
        } else {
            $this->render('auth/login', [
                'error' => 'Неверное имя пользователя или пароль',
                'old' => $_POST
            ]);
        }
    }

    public function registerForm()
    {
        if ($this->getCurrentUser()) {
            $this->redirect('/tasks');
        }
       
        $this->render('auth/register');
    }

    public function register()
    {
        $errors = $this->validate($_POST, [
            'username' => 'required|min:3',
            'email' => 'required|email',
            'password' => 'required|min:6',
            'password_confirmation' => 'required'
        ]);
       
        if ($_POST['password'] !== $_POST['password_confirmation']) {
            $errors['password_confirmation'][] = 'Пароли не совпадают';
        }
       
        // Проверка уникальности
        $existingUser = User::where('username', '=', $_POST['username'])->get();
        if (!empty($existingUser)) {
            $errors['username'][] = 'Имя пользователя уже занято';
        }
       
        $existingEmail = User::where('email', '=', $_POST['email'])->get();
        if (!empty($existingEmail)) {
            $errors['email'][] = 'Email уже зарегистрирован';
        }
       
        if (!empty($errors)) {
            $this->render('auth/register', [
                'errors' => $errors,
                'old' => $_POST
            ]);
            return;
        }
       
        if (User::register($_POST['username'], $_POST['email'], $_POST['password'])) {
            $_SESSION['success'] = 'Регистрация прошла успешно! Войдите в систему.';
            $this->redirect('/login');
        } else {
            $this->render('auth/register', [
                'error' => 'Ошибка при регистрации',
                'old' => $_POST
            ]);
        }
    }

    public function logout()
    {
        session_start();
        session_destroy();
        setcookie('user_id', '', time() - 3600, '/');
        $this->redirect('/login');
    }
}

5. Разработка представлений​

Базовый шаблон​

PHP:
<?php
// app/Views/layouts/main.php
<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>To-Do List - <?= $title ?? 'Главная' ?></title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css" rel="stylesheet">
    <link href="/public/css/style.css" rel="stylesheet">
</head>
<body>
    <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
        <div class="container">
            <a class="navbar-brand" href="/tasks">
                <i class="bi bi-check2-square"></i> To-Do List
            </a>
           
            <?php if ($user = \App\Core\Controller::getCurrentUser()): ?>
            <div class="navbar-collapse">
                <ul class="navbar-nav me-auto">
                    <li class="nav-item">
                        <a class="nav-link" href="/tasks">
                            <i class="bi bi-list-task"></i> Мои задачи
                        </a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="/tasks/create">
                            <i class="bi bi-plus-circle"></i> Новая задача
                        </a>
                    </li>
                </ul>
               
                <div class="navbar-text me-3">
                    Привет, <strong><?= htmlspecialchars($user['username']) ?></strong>!
                </div>
               
                <a href="/logout" class="btn btn-outline-light btn-sm">
                    <i class="bi bi-box-arrow-right"></i> Выйти
                </a>
            </div>
            <?php endif; ?>
        </div>
    </nav>

    <main class="container mt-4">
        <?php if (isset($_SESSION['success'])): ?>
            <div class="alert alert-success alert-dismissible fade show" role="alert">
                <?= $_SESSION['success'] ?>
                <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
            </div>
            <?php unset($_SESSION['success']); ?>
        <?php endif; ?>

        <?php if (isset($_SESSION['error'])): ?>
            <div class="alert alert-danger alert-dismissible fade show" role="alert">
                <?= $_SESSION['error'] ?>
                <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
            </div>
            <?php unset($_SESSION['error']); ?>
        <?php endif; ?>

        <?= $content ?>
    </main>

    <footer class="bg-light mt-5 py-3">
        <div class="container text-center text-muted">
            <small>To-Do List &copy; <?= date('Y') ?></small>
        </div>
    </footer>

    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <script src="/public/js/app.js"></script>
</body>
</html>

Страница списка задач​

PHP:
<?php
// app/Views/tasks/index.php
ob_start();
?>

<div class="row">
    <div class="col-md-3">
        <div class="card mb-4">
            <div class="card-header">
                <h5 class="card-title mb-0">Статистика</h5>
            </div>
            <div class="card-body">
                <div class="mb-3">
                    <span class="badge bg-primary rounded-pill">Всего: <?= $statistics['total'] ?></span>
                </div>
                <div class="mb-2">
                    <span class="badge bg-success">Завершено: <?= $statistics['completed'] ?></span>
                </div>
                <div class="mb-2">
                    <span class="badge bg-warning">В работе: <?= $statistics['in_progress'] ?></span>
                </div>
                <div class="mb-2">
                    <span class="badge bg-secondary">В ожидании: <?= $statistics['pending'] ?></span>
                </div>
                <?php if ($statistics['overdue'] > 0): ?>
                <div class="mb-2">
                    <span class="badge bg-danger">Просрочено: <?= $statistics['overdue'] ?></span>
                </div>
                <?php endif; ?>
            </div>
        </div>

        <div class="card">
            <div class="card-header">
                <h5 class="card-title mb-0">Фильтры</h5>
            </div>
            <div class="card-body">
                <form method="get" action="/tasks">
                    <div class="mb-3">
                        <label class="form-label">Статус</label>
                        <select class="form-select" name="status">
                            <option value="">Все</option>
                            <option value="pending" <?= $filters['status'] == 'pending' ? 'selected' : '' ?>>В ожидании</option>
                            <option value="in_progress" <?= $filters['status'] == 'in_progress' ? 'selected' : '' ?>>В работе</option>
                            <option value="completed" <?= $filters['status'] == 'completed' ? 'selected' : '' ?>>Завершено</option>
                        </select>
                    </div>
                   
                    <div class="mb-3">
                        <label class="form-label">Приоритет</label>
                        <select class="form-select" name="priority">
                            <option value="">Все</option>
                            <option value="low" <?= $filters['priority'] == 'low' ? 'selected' : '' ?>>Низкий</option>
                            <option value="medium" <?= $filters['priority'] == 'medium' ? 'selected' : '' ?>>Средний</option>
                            <option value="high" <?= $filters['priority'] == 'high' ? 'selected' : '' ?>>Высокий</option>
                        </select>
                    </div>
                   
                    <div class="mb-3">
                        <label class="form-label">Метка</label>
                        <select class="form-select" name="tag_id">
                            <option value="">Все</option>
                            <?php foreach ($tags as $tag): ?>
                            <option value="<?= $tag->id ?>" <?= $filters['tag_id'] == $tag->id ? 'selected' : '' ?>>
                                <?= htmlspecialchars($tag->name) ?>
                            </option>
                            <?php endforeach; ?>
                        </select>
                    </div>
                   
                    <button type="submit" class="btn btn-primary w-100">Применить</button>
                    <a href="/tasks" class="btn btn-secondary w-100 mt-2">Сбросить</a>
                </form>
            </div>
        </div>
    </div>

    <div class="col-md-9">
        <div class="d-flex justify-content-between align-items-center mb-4">
            <h1>Мои задачи</h1>
            <div class="d-flex">
                <div class="input-group me-2" style="width: 300px;">
                    <input type="text" class="form-control" id="search-input" placeholder="Поиск задач...">
                    <button class="btn btn-outline-secondary" type="button" id="search-button">
                        <i class="bi bi-search"></i>
                    </button>
                </div>
                <a href="/tasks/create" class="btn btn-success">
                    <i class="bi bi-plus-circle"></i> Новая задача
                </a>
            </div>
        </div>

        <div id="search-results" class="mb-4" style="display: none;">
            <div class="card">
                <div class="card-header">
                    <h5 class="card-title mb-0">Результаты поиска</h5>
                </div>
                <div class="card-body" id="search-results-body">
                    <!-- Результаты поиска будут загружены через AJAX -->
                </div>
            </div>
        </div>

        <?php if (empty($tasks)): ?>
            <div class="alert alert-info">
                <i class="bi bi-info-circle"></i> У вас пока нет задач. <a href="/tasks/create">Создайте первую задачу</a>.
            </div>
        <?php else: ?>
            <div class="table-responsive">
                <table class="table table-hover">
                    <thead class="table-light">
                        <tr>
                            <th width="50">#</th>
                            <th>Задача</th>
                            <th width="120">Статус</th>
                            <th width="120">Приоритет</th>
                            <th width="120">Срок</th>
                            <th width="150">Метки</th>
                            <th width="150">Действия</th>
                        </tr>
                    </thead>
                    <tbody>
                        <?php foreach ($tasks as $task): ?>
                        <tr class="<?= $task->isOverdue() ? 'table-danger' : '' ?>">
                            <td><?= $task->id ?></td>
                            <td>
                                <a href="/tasks/<?= $task->id ?>" class="text-decoration-none">
                                    <strong><?= htmlspecialchars($task->title) ?></strong>
                                </a>
                                <?php if ($task->isOverdue()): ?>
                                    <span class="badge bg-danger ms-2">Просрочено</span>
                                <?php endif; ?>
                                <div class="text-muted small mt-1">
                                    <?= nl2br(htmlspecialchars(mb_substr($task->description, 0, 100))) ?>...
                                </div>
                            </td>
                            <td>
                                <div class="status-select" data-task-id="<?= $task->id ?>">
                                    <?= $task->getStatusBadge() ?>
                                </div>
                            </td>
                            <td><?= $task->getPriorityBadge() ?></td>
                            <td>
                                <?php if ($task->due_date): ?>
                                    <?= date('d.m.Y', strtotime($task->due_date)) ?>
                                <?php else: ?>
                                    <span class="text-muted">—</span>
                                <?php endif; ?>
                            </td>
                            <td>
                                <?php foreach ($task->tags() as $tag): ?>
                                    <span class="badge me-1" <?= $tag->getColorStyle() ?>>
                                        <?= htmlspecialchars($tag->name) ?>
                                    </span>
                                <?php endforeach; ?>
                            </td>
                            <td>
                                <div class="btn-group btn-group-sm">
                                    <a href="/tasks/<?= $task->id ?>" class="btn btn-outline-primary">
                                        <i class="bi bi-eye"></i>
                                    </a>
                                    <a href="/tasks/<?= $task->id ?>/edit" class="btn btn-outline-warning">
                                        <i class="bi bi-pencil"></i>
                                    </a>
                                    <button type="button" class="btn btn-outline-danger delete-task"
                                            data-id="<?= $task->id ?>"
                                            data-title="<?= htmlspecialchars($task->title) ?>">
                                        <i class="bi bi-trash"></i>
                                    </button>
                                </div>
                            </td>
                        </tr>
                        <?php endforeach; ?>
                    </tbody>
                </table>
            </div>
        <?php endif; ?>
    </div>
</div>

<div class="modal fade" id="statusModal" tabindex="-1">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title">Изменить статус</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
            </div>
            <div class="modal-body">
                <input type="hidden" id="task-id">
                <div class="mb-3">
                    <label class="form-label">Новый статус</label>
                    <select class="form-select" id="new-status">
                        <option value="pending">В ожидании</option>
                        <option value="in_progress">В работе</option>
                        <option value="completed">Завершено</option>
                    </select>
                </div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
                <button type="button" class="btn btn-primary" id="save-status">Сохранить</button>
            </div>
        </div>
    </div>
</div>

<div class="modal fade" id="deleteModal" tabindex="-1">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title">Подтверждение удаления</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
            </div>
            <div class="modal-body">
                <p>Вы уверены, что хотите удалить задачу "<span id="task-title"></span>"?</p>
                <p class="text-danger"><small>Это действие нельзя отменить.</small></p>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
                <form id="delete-form" method="post" style="display: inline;">
                    <?php // CSRF токен должен быть добавлен в реальном проекте ?>
                    <button type="submit" class="btn btn-danger">Удалить</button>
                </form>
            </div>
        </div>
    </div>
</div>

<?php
$content = ob_get_clean();
$title = 'Мои задачи';
include __DIR__ . '/../layouts/main.php';
?>