Варианты организации 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-атак.