Варианты организации PHP кода для сайта

Раздел: PHP -> Веб-разработка

Основное эффективное решение: архитектура MVC с автозагрузкой через Composer

При разработке сайта на PHP наиболее эффективным способом является использование архитектурного шаблона MVC (Model-View-Controller) с автоматической загрузкой классов через Composer по стандарту PSR-4. Это обеспечивает четкое разделение логики, легкость тестирования и масштабирования.

Как реализовать маршрутизацию и автозагрузку без фреймворка?

Для этого создается единая точка входа public/index.php, файл composer.json с автозагрузкой, и простой роутер.

// composer.json
{
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    }
}

// public/index.php
<?php
require __DIR__ . '/../vendor/autoload.php';
use App\Router;
$router = new Router();
$router->add('/', 'HomeController@index');
$router->add('/about', 'AboutController@index');
$router->dispatch();
?>

После выполнения composer dump-autoload классы из папки src/ загружаются автоматически. Роутер анализирует URI и вызывает нужный метод контроллера.

Типичные проблемы и их решения

  • Ошибка автозагрузки: несоответствие пространства имен и пути к файлу. Решение: проверить регистр символов и структуру папок, выполнить composer dump-autoload.
  • Маршрутизация не работает: необходимо настроить веб-сервер (Apache - файл .htaccess, Nginx - правила rewrite) для перенаправления всех запросов на index.php.
  • Ошибка PDO: не указан драйвер или неверные параметры соединения. Решение: проверить DSN и права доступа.

Цели: создание поддерживаемого и расширяемого кода для средних и крупных проектов. Случаи использования: интернет-магазины, порталы, API.

Вариант 1: Процедурный подход (все в одном файле)

Как сделать простой скрипт без лишней структуры?

<?php
$db = new PDO('sqlite:test.db');
$stmt = $db->query('SELECT * FROM users');
while ($row = $stmt->fetch()) {
    echo $row['name'];
}
?>

Проблемы: код сложно поддерживать, дублирование логики, уязвимости SQL-инъекций из-за отсутствия подготовленных запросов.

Случаи использования: одноразовые скрипты, прототипы, где важна скорость разработки.

Вариант 2: Функциональный подход с include

Как разделить код на функции и подключать нужные файлы?

// functions.php
function getUsers($db) {
    $stmt = $db->query('SELECT * FROM users');
    return $stmt->fetchAll();
}
// index.php
require 'config.php';
require 'functions.php';
$users = getUsers($db);
foreach ($users as $user) {
    echo $user['name'];
}

Проблемы: конфликты имен функций при использовании сторонних библиотек, необходимость вручную контролировать порядок подключения.

Случаи: небольшие проекты, где объектная модель избыточна.

Вариант 3: ООП с ручной автозагрузкой

Как использовать классы без Composer и внешних зависимостей?

spl_autoload_register(function ($class) {
    $path = __DIR__ . '/src/' . str_replace('\\', '/', $class) . '.php';
    if (file_exists($path)) {
        require $path;
    }
});
$controller = new \App\Controllers\HomeController();
$controller->index();

Ошибки: неверная структура директорий, проблемы с регистром символов в Linux, требования к ручному обновлению маппинга.

Случаи: legacy-проекты, хостинг без возможности установки Composer.

Вариант 4: Микрофреймворк (например, Slim 4)

Как получить готовый роутер, middleware и контейнер зависимостей?

use Slim\Factory\AppFactory;

$app = AppFactory::create();
$app->get('/hello/{name}', function ($request, $response, $args) {
    $response->getBody()->write("Hello, " . $args['name']);
    return $response;
});
$app->run();

Зависимости: требуется Composer и PHP 7.4+; излишне для простых страниц.

Случаи: разработка REST API, проекты среднего размера с четкими требованиями к HTTP.

Расширенные примеры организации PHP кода

Рассмотрим реализацию простого MVC-приложения «Заметки» с использованием PDO и Twig.

Структура проекта

Пример
project/
├── composer.json
├── public/
│   ├── .htaccess
│   └── index.php
├── src/
│   ├── Controllers/
│   │   └── NoteController.php
│   ├── Models/
│   │   └── Note.php
│   └── Router.php
└── views/
    ├── base.html.twig
    └── notes.html.twig

Файл composer.json

Пример
{
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    },
    "require": {
        "twig/twig": "^3.0"
    }
}

Точка входа public/index.php

Пример
<?php
require __DIR__ . '/../vendor/autoload.php';

use App\Router;
use App\Controllers\NoteController;
use App\Models\Note;
use Twig\Environment;
use Twig\Loader\FilesystemLoader;

// Инициализация Twig
$loader = new FilesystemLoader(__DIR__ . '/../views');
$twig = new Environment($loader);

// Инициализация PDO
$pdo = new PDO('sqlite:' . __DIR__ . '/../database.sqlite');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

// Создание таблицы (упрощенно)
$pdo->exec('CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, title TEXT, content TEXT)');

// Маршрутизация
$router = new Router();
$router->add('/', function () use ($twig, $pdo) {
    $stmt = $pdo->query('SELECT * FROM notes ORDER BY id DESC');
    $notes = $stmt->fetchAll(PDO::FETCH_ASSOC);
    echo $twig->render('notes.html.twig', ['notes' => $notes]);
});
$router->dispatch();
?>

Класс Router

Пример
<?php
namespace App;

class Router {
    private array $routes = [];

    public function add(string $path, callable $handler): void {
        $this->routes[$path] = $handler;
    }

    public function dispatch(): void {
        $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
        if (isset($this->routes[$path])) {
            call_user_func($this->routes[$path]);
        } else {
            http_response_code(404);
            echo '404 Not Found';
        }
    }
}

Шаблон views/notes.html.twig

Пример
<!DOCTYPE html>
<html>
<head><title>Заметки</title></head>
<body>
    <h1>Мои заметки</h1>
    <ul>
        {% for note in notes %}
            <li><strong>{{ note.title }}</strong>: {{ note.content }}</li>
        {% else %}
            <li>Заметок пока нет.</li>
        {% endfor %}
    </ul>
</body>
</html>

Результат выполнения

При первом запуске создается база данных и таблица notes. Вывод пустого списка. После добавления записей через SQLite (например, INSERT INTO notes VALUES (1, 'Заголовок', 'Текст заметки')) при обновлении страницы отобразится список с заметкой.

Дополнительный пример: подготовленные запросы и обработка исключений

Пример
<?php
$stmt = $pdo->prepare('INSERT INTO notes (title, content) VALUES (:title, :content)');
$stmt->execute(['title' => 'Новая заметка', 'content' => 'Текст новой заметки']);
echo 'Запись добавлена, ID: ' . $pdo->lastInsertId();
?>
Вывод: "Запись добавлена, ID: 1" (или следующий ID). В случае ошибки PDO выбросит исключение, которое можно перехватить try-catch.

Пример работы с сессиями и безопасностью

Пример
<?php
session_start();
if (!isset($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
?>
<form method="post" action="/submit">
    <input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
    <input type="text" name="data">
    <button>Отправить</button>
</form>
При отправке формы проверяется соответствие токена. Это защищает от CSRF-атак.

Код PHP сайта - comments

En
код php сайта (php)