🎯 Цель урока

Понять, зачем нужна архитектура MVC для веб-приложений. Создать базовую структуру проекта, реализовать простой роутинг и разделить код по ответственностям (логика, данные, представление).

📚 Теоретическая часть (40 минут)

🏗️ Почему нельзя просто писать весь код в одном файле?

"Спагетти-код" — это когда HTML, PHP-логика, работа с базой данных и проверки форм перемешаны в одном или нескольких файлах без четкой структуры.

🔍 Проблемы монолитного кода:

  • Сложно поддерживать — чтобы изменить отображение, нужно искать код среди логики
  • Трудно тестировать — нельзя проверить отдельные части приложения
  • Невозможно повторно использовать — код заточен под конкретную страницу
  • Опасно для безопасности — легко допустить ошибку при смешивании логики и вывода
  • Затруднена командная работа — несколько разработчиков не могут работать одновременно

💡 Реальная аналогия: Представьте кухню, где продукты, ножи, посуда и готовые блюда разбросаны по всему столу без порядка. Шеф-повар (разработчик) тратит 80% времени на поиск нужного, а не на готовку.

📐 Архитектурный шаблон MVC (Model-View-Controller)

MVC — это способ организации кода, который разделяет приложение на три основных компонента, каждый со своей ответственностью.

Компоненты MVC и их ответственность
Компонент Ответственность Аналогия в ресторане Что содержит
Model (Модель) Работа с данными, бизнес-логика Кухня и склад Классы для работы с БД, расчеты, валидация
View (Вид/Представление) Отображение данных пользователю Зал и официант HTML-шаблоны, CSS, JavaScript для интерфейса
Controller (Контроллер) Прием запроса и координация Модели и Вида Администратор ресторана Логика обработки запросов, выбор шаблона

✨ Как работает поток данных в MVC:

  1. Пользователь совершает действие (переходит по URL, отправляет форму)
  2. Роутер определяет, какой контроллер и метод вызвать
  3. Контроллер принимает запрос, запрашивает данные у Модели
  4. Модель работает с базой данных, выполняет бизнес-логику
  5. Контроллер получает данные от Модели и передает их во Вид
  6. Вид отрисовывает HTML-страницу с полученными данными
  7. Пользователь видит результат в браузере

📝 Важно: В классическом MVC компоненты общаются только в одном направлении: Контроллер → Модель → Контроллер → Вид. Вид никогда не обращается напрямую к Модели.

🛣️ Что такое роутинг (маршрутизация)?

Роутинг — это процесс определения, какой код выполнить для конкретного URL-адреса.

Примеры URL и их обработка:

  • https://site.ru/ → Главная страница
  • https://site.ru/about → Страница "О нас"
  • https://site.ru/posts/5 → Статья с ID 5
  • https://site.ru/contact → Страница контактов

Типы роутинга:

  • "Наивный" (простой) роутинг — через параметры в URL (?page=about)
  • ЧПУ (Человеко-Понятные Урлы) — красивые пути (/about, /posts/5)
  • Роутинг в фреймворках — сложные системы с регулярными выражениями

💡 Простая аналогия: Роутинг — это как секретарь в приемной. Клиент (браузер) говорит "Мне нужно к менеджеру по продажам" (URL /sales), а секретарь (роутер) направляет его в нужный кабинет (контроллер).

⚙️ Подготовка проекта: новая структура папок

Для работы с MVC нужно правильно организовать файлы проекта.

📁 Новая структура папок для блога:

myblog/                      # Корневая папка проекта
├── app/                     # Ядро приложения
│   ├── controllers/         # Контроллеры
│   │   └── HomeController.php
│   ├── models/              # Модели
│   │   └── Post.php
│   └── views/               # Представления (пока обычные PHP-файлы)
│       ├── home.php
│       └── layout.php
├── public/                  # Публичная папка (точка входа)
│   └── index.php           # Единая точка входа
├── config/                  # Конфигурационные файлы
└── vendor/                  # Зависимости (позже добавим через Composer)

🔒 Почему public/index.php — точка входа?

  • Все запросы идут через один файл
  • Повышается безопасность — основной код находится вне public
  • Упрощается конфигурация сервера
  • Легче реализовать роутинг

💻 Практическая часть (2 часа)

Шаг 1: Создание структуры проекта

  1. Создайте новую папку mvc_blog в папке вашего локального сервера:
    Для OpenServer: C:\OpenServer\domains\mvc_blog\
    Для XAMPP: C:\xampp\htdocs\mvc_blog\
  2. Внутри создайте структуру папок как показано выше
  3. Откройте проект в VS Code

📁 Важно: Папка public будет доступна из браузера, а папка app — нет. Это повышает безопасность.

Шаг 2: Создание единой точки входа

В файле public/index.php напишите:

<?php
// public/index.php - Единая точка входа

// Включение отображения ошибок (только для разработки!)
ini_set('display_errors', 1);
error_reporting(E_ALL);

// Базовая конфигурация
define('ROOT_PATH', dirname(__DIR__));
define('APP_PATH', ROOT_PATH . '/app');
define('VIEW_PATH', APP_PATH . '/views');

// Простейший "наивный" роутинг через GET-параметр
$page = $_GET['page'] ?? 'home';

// Обработка роутов
switch ($page) {
    case 'home':
        $controller = 'HomeController';
        $action = 'index';
        break;
    case 'about':
        $controller = 'HomeController';
        $action = 'about';
        break;
    default:
        // Если страница не найдена
        $controller = 'HomeController';
        $action = 'notFound';
        break;
}

// Подключаем контроллер
$controllerFile = APP_PATH . "/controllers/{$controller}.php";
if (file_exists($controllerFile)) {
    require_once $controllerFile;
    
    // Создаем объект контроллера и вызываем метод
    $controllerInstance = new $controller();
    if (method_exists($controllerInstance, $action)) {
        $controllerInstance->$action();
    } else {
        die("Метод {$action} не найден в контроллере {$controller}");
    }
} else {
    die("Контроллер {$controller} не найден");
}
?>

📖 Разбор кода роутинга:

  • $_GET['page'] ?? 'home' — получаем параметр page или используем 'home' по умолчанию
  • switch — определяем, какой контроллер и метод вызвать
  • define() — создаем константы для путей к папкам
  • file_exists() — проверяем, существует ли файл контроллера
  • require_once — подключаем файл контроллера
  • new $controller() — создаем объект контроллера (динамически по имени)
  • method_exists() — проверяем, существует ли метод в контроллере

Шаг 3: Создание первого контроллера

В файле app/controllers/HomeController.php напишите:

<?php
// app/controllers/HomeController.php

class HomeController
{
    public function index()
    {
        // Данные для главной страницы
        $data = [
            'title' => 'Добро пожаловать в MVC блог!',
            'posts' => [
                ['id' => 1, 'title' => 'Первая статья', 'content' => 'Содержание первой статьи...'],
                ['id' => 2, 'title' => 'Вторая статья', 'content' => 'Содержание второй статьи...'],
            ],
            'current_date' => date('d.m.Y'),
        ];
        
        // Подключаем вид (представление)
        $this->render('home', $data);
    }
    
    public function about()
    {
        $data = [
            'title' => 'О нашем блоге',
            'description' => 'Этот блог создан для изучения MVC архитектуры.',
            'author' => 'Команда разработчиков',
        ];
        
        $this->render('about', $data);
    }
    
    public function notFound()
    {
        $data = [
            'title' => 'Страница не найдена',
            'message' => 'Запрошенная страница не существует.',
        ];
        
        $this->render('404', $data);
    }
    
    /**
     * Метод для отображения вида
     */
    private function render($viewName, $data = [])
    {
        // Извлекаем переменные из массива данных
        // ['title' => '...'] превратится в $title = '...';
        extract($data);
        
        // Подключаем основной шаблон (layout)
        require_once VIEW_PATH . '/layout.php';
    }
}
?>

📖 Разбор кода контроллера:

  • class HomeController — объявляем класс контроллера
  • public function index() — метод для главной страницы
  • $data — массив данных, которые передаются в вид
  • $this->render() — вызов метода для отображения вида
  • extract($data) — преобразует массив в переменные (ключи → имена переменных)
  • require_once — подключаем файл шаблона

Шаг 4: Создание базового шаблона (layout)

В файле app/views/layout.php напишите:

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><?php echo $title ?? 'MVC Блог'; ?></title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: Arial, sans-serif; line-height: 1.6; }
        .container { max-width: 1200px; margin: 0 auto; padding: 0 20px; }
        header { background: #2c3e50; color: white; padding: 1rem 0; }
        nav ul { display: flex; list-style: none; gap: 20px; }
        nav a { color: white; text-decoration: none; }
        nav a:hover { text-decoration: underline; }
        main { padding: 2rem 0; }
        .post { background: #f9f9f9; padding: 1rem; margin-bottom: 1rem; border-left: 4px solid #3498db; }
        footer { background: #34495e; color: white; padding: 1rem 0; margin-top: 2rem; }
        .error { color: #e74c3c; padding: 2rem; text-align: center; }
    </style>
</head>
<body>
    <header>
        <div class="container">
            <h1>MVC Блог</h1>
            <nav>
                <ul>
                    <li><a href="/mvc_blog/public/?page=home">Главная</a></li>
                    <li><a href="/mvc_blog/public/?page=about">О блоге</a></li>
                </ul>
            </nav>
        </div>
    </header>
    
    <main class="container">
        <!-- Здесь будет динамический контент из отдельных видов -->
        <?php
        // Определяем, какой конкретный вид загрузить
        $viewFile = VIEW_PATH . '/' . ($viewName ?? 'home') . '.php';
        if (file_exists($viewFile)) {
            require_once $viewFile;
        } else {
            echo '<div class="error">Шаблон не найден</div>';
        }
        ?>
    </main>
    
    <footer>
        <div class="container">
            <p>© <?php echo date('Y'); ?> MVC Блог. Все права защищены.</p>
            <p>Сегодня: <?php echo $current_date ?? date('d.m.Y'); ?></p>
        </div>
    </footer>
</body>
</html>

Шаг 5: Создание отдельных видов (views)

1. Создайте app/views/home.php:

<h2><?php echo $title; ?></h2>
<p>Это главная страница нашего блога на MVC архитектуре.</p>

<h3>Последние статьи:</h3>
<?php if (!empty($posts)): ?>
    <?php foreach ($posts as $post): ?>
        <div class="post">
            <h4><?php echo htmlspecialchars($post['title']); ?></h4>
            <p><?php echo htmlspecialchars($post['content']); ?></p>
            <a href="#">Читать далее...</a>
        </div>
    <?php endforeach; ?>
<?php else: ?>
    <p>Статей пока нет.</p>
<?php endif; ?>

2. Создайте app/views/about.php:

<h2><?php echo $title; ?></h2>
<p><?php echo $description; ?></p>
<p><strong>Автор:</strong> <?php echo $author; ?></p>
<p>Этот проект демонстрирует:</p>
<ul>
    <li>Архитектуру MVC</li>
    <li>Простую маршрутизацию</li>
    <li>Разделение логики и представления</li>
    <li>Использование базового шаблона (layout)</li>
</ul>

3. Создайте app/views/404.php:

<div class="error">
    <h2><?php echo $title; ?></h2>
    <p><?php echo $message; ?></p>
    <a href="/mvc_blog/public/?page=home">Вернуться на главную</a>
</div>

Шаг 6: Тестирование приложения

  1. Откройте браузер и перейдите по адресу:
    http://localhost/mvc_blog/public/?page=home
    Должна открыться главная страница со списком статей.
  2. Проверьте другие страницы:
    http://localhost/mvc_blog/public/?page=about
    http://localhost/mvc_blog/public/?page=non_existent
  3. Нажмите Ctrl+U (просмотр исходного кода) - вы увидите чистый HTML без PHP!

🔍 Что произошло:

  1. Браузер запросил ?page=home
  2. index.php определил, что нужно вызвать HomeController::index()
  3. Контроллер подготовил данные (массив статей)
  4. Контроллер вызвал render('home', $data)
  5. Был подключен layout.php, который в свою очередь подключил home.php
  6. В браузер отправился готовый HTML

🧪 Эксперименты с кодом

Эксперимент 1: Добавьте новую страницу "Контакты"

  1. Добавьте новый case в switch в index.php
  2. Добавьте метод contact() в HomeController
  3. Создайте файл app/views/contact.php
  4. Проверьте: ?page=contact

Эксперимент 2: Измените роутинг

Попробуйте изменить параметр по умолчанию с 'home' на 'about' и посмотрите, что изменится.

Эксперимент 3: Создайте простую Модель

Создайте файл app/models/Post.php:

<?php
class Post
{
    public static function getAll()
    {
        return [
            ['id' => 1, 'title' => 'Статья из модели', 'content' => '...'],
            ['id' => 2, 'title' => 'Еще статья', 'content' => '...'],
        ];
    }
}
?>

Измените в контроллере: $posts = Post::getAll();

📋 Домашнее задание

✏️ Задание на день 1

  1. Создайте структуру MVC-проекта как в уроке и убедитесь, что всё работает
  2. Добавьте страницу "Контакты" с формой (пока без обработки):
    <form method="POST">
        <input type="text" name="name" placeholder="Ваше имя">
        <input type="email" name="email" placeholder="Email">
        <textarea name="message" placeholder="Сообщение"></textarea>
        <button type="submit">Отправить</button>
    </form>
  3. Создайте простую Модель Post как в эксперименте 3
  4. Модифицируйте роутинг так, чтобы по умолчанию открывалась страница "about"
  5. Бонус: Создайте отдельный контроллер PostController с методом show($id) и страницей отдельной статьи. URL: ?page=post&id=1
  6. Бонус 2: Добавьте в навигационное меню автоматическое выделение активной страницы (например, через добавление класса "active")

🧠 Проверка понимания

Вопросы для самопроверки

  1. Какие три компонента MVC и что делает каждый из них?
  2. Почему "наивный" роутинг через GET-параметры не идеален для реальных проектов?
  3. Что делает функция extract() и почему мы её используем?
  4. Зачем нужна единая точка входа (public/index.php)?
  5. Почему мы разделили layout и отдельные views?
  6. Какой путь проходит запрос от браузера до отображения страницы в MVC?
  7. Что произойдет, если пользователь запросит несуществующую страницу?
  8. Почему в папке public только index.php, а остальной код в app/?

💡 Полезные советы по MVC

🚀

Начинайте с простого

Не пытайтесь сразу реализовать идеальный MVC. Ваш "наивный" роутинг — отличный первый шаг. Позже вы замените его на более продвинутый.

🔧

Используйте автозагрузку классов

Вместо множества require_once используйте автозагрузчик. Добавьте в index.php: spl_autoload_register(function($class) { require_once APP_PATH . '/' . str_replace('\\', '/', $class) . '.php'; });

📁

Создайте конфигурационный файл

Вынесите настройки базы данных, пути и константы в отдельный файл config.php. Это упростит развертывание на разных серверах.

🛡️

Безопасность входа в приложение

Всегда проверяйте входные данные в контроллерах. Используйте htmlspecialchars() для вывода пользовательского контента в шаблонах.

🧩

Создайте базовый контроллер

Вынесите общие методы (render, redirect, проверка авторизации) в BaseController. Все остальные контроллеры будут наследовать от него.

🎯

Тестируйте компоненты отдельно

Модели можно тестировать без контроллеров, контроллеры — без видов. Это главное преимущество MVC для разработки и отладки.