Построение веб-приложения на PHP: обзор архитектурных решений
Ключевые архитектурные подходы для PHP-приложений
Основное эффективное решение: шаблон MVC с автозагрузкой Composer
Современная разработка веб-приложений на PHP чаще всего строится вокруг паттерна MVC (Model-View-Controller). Этот подход разделяет логику приложения, представление и управление запросами. Использование автозагрузки через Composer (стандарт PSR-4) упрощает организацию кода и подключение библиотек.
Пример минимальной структуры:
project/
composer.json
public/
index.php
src/
Controllers/
HomeController.php
Models/
User.php
Views/
home.php
vendor/Com index php action (компоненты index.php в php)
Файл composer.json определяет автозагрузку:
{
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}Application web php (веб-приложение на php)
Точка входа public/index.php – фронт-контроллер:
<?php
require __DIR__ . '/../vendor/autoload.php';
$router = new App\Router();
$router->addRoute('/', 'HomeController', 'index');
$router->dispatch($_SERVER['REQUEST_URI']);Класс маршрутизатора:
<?php
namespace App;
class Router {
private array $routes = [];
public function addRoute(string $path, string $controller, string $action): void {
$this->routes[$path] = ['controller' => $controller, 'action' => $action];
}
public function dispatch(string $uri): void {
$uri = parse_url($uri, PHP_URL_PATH);
if (isset($this->routes[$uri])) {
$controllerClass = 'App\\Controllers\\' . $this->routes[$uri]['controller'];
$action = $this->routes[$uri]['action'];
$controller = new $controllerClass();
$controller->$action();
} else {
http_response_code(404);
echo 'Страница не найдена';
}
}
}Контроллер HomeController:
<?php
namespace App\Controllers;
use App\Models\User;
class HomeController {
public function index(): void {
$users = User::getAll();
include __DIR__ . '/../Views/home.php';
}
}Модель User (упрощенно с PDO):
<?php
namespace App\Models;
use PDO;
class User {
public static function getAll(): array {
$db = new PDO('mysql:host=localhost;dbname=test', 'root', '');
return $db->query('SELECT * FROM users')->fetchAll(PDO::FETCH_ASSOC);
}
}Представление Views/home.php:
<h1>Список пользователей</h1>
<ul>
<?php foreach ($users as $user): ?>
<li><?= htmlspecialchars($user['name']) ?></li>
<?php endforeach; ?>
</ul>Этот подход решает проблему смешивания кода и упрощает тестирование. Для автозагрузки достаточно выполнить composer dump-autoload.
Типичные ошибки и их решения:
- Неправильное имя пространства имен в composer.json – проверьте соответствие директории.
- Ошибка 404 при маршрутизации – убедитесь, что
dispatchвызывается с корректным URI (без query string). - Игнорирование XSS в представлениях – используйте
htmlspecialcharsпри выводе данных. - SQL-инъекции – применяйте подготовленные запросы вместо прямого
query.
Как построить простое приложение без классов и фреймворков?
Процедурный стиль подходит для небольших прототипов или скриптов. Все файлы подключаются через include или require, логика смешана с выводом. Пример:
<?php
// index.php
$action = $_GET['action'] ?? 'home';
if ($action === 'home') {
echo '<h1>Домашняя страница</h1>';
} elseif ($action === 'users') {
$db = mysqli_connect('localhost', 'root', '', 'test');
$result = mysqli_query($db, 'SELECT * FROM users');
while ($row = mysqli_fetch_assoc($result)) {
echo $row['name'];
}
}Цель: быстрый запуск, минимум настроек. Случаи использования: одноразовые скрипты, простые формы обратной связи.
Проблемы: код сложно поддерживать, дублирование блоков, уязвимость к SQL-инъекциям. Решение – переход на PDO и организованную структуру.
Как организовать микро-архитектуру без фреймворка, но с разделением логики?
Можно написать собственную автозагрузку через spl_autoload_register и легковесный маршрутизатор. Пример автозагрузки:
spl_autoload_register(function ($class) {
$prefix = 'App\\';
$baseDir = __DIR__ . '/src/';
$len = strlen($prefix);
if (strncmp($prefix, $class, $len) !== 0) return;
$relativeClass = substr($class, $len);
$file = $baseDir . str_replace('\\', '/', $relativeClass) . '.php';
if (file_exists($file)) require $file;
});Маршрутизатор с поддержкой параметров:
class Router {
private array $routes = [];
public function get(string $pattern, callable $handler): void {
$this->routes[] = ['pattern' => $pattern, 'handler' => $handler];
}
public function run(string $uri): void {
foreach ($this->routes as $route) {
$regex = preg_replace('/\{([a-z]+)\}/', '(?P<$1>[^/]+)', $route['pattern']);
if (preg_match("~^$regex$~", $uri, $matches)) {
call_user_func($route['handler'], $matches);
return;
}
}
http_response_code(404);
}
}Использование:
$router = new Router();
$router->get('/user/{id}', function ($params) {
echo 'Пользователь ID: ' . $params['id'];
});
$router->run($_SERVER['REQUEST_URI']);Цель: получение гибкости без излишней сложности фреймворка. Случаи: средние проекты, где важна скорость и контроль.
Ошибки: рекурсивная автозагрузка при неправильном сопоставлении классов – проверяйте пространства имен. Регулярные выражения могут конфликтовать с реальными URI – используйте ограниченные паттерны.
Как ускорить разработку с помощью готового фреймворка (Laravel)?
Laravel предоставляет полный набор инструментов: маршрутизация, ORM (Eloquent), шаблонизатор Blade, механизм миграций. Установка:
composer create-project laravel/laravel example-appПример роута в routes/web.php:
Route::get('/users', [UserController::class, 'index']);Контроллер:
<?php
namespace App\Http\Controllers;
use App\Models\User;
class UserController extends Controller {
public function index() {
$users = User::all();
return view('users.index', compact('users'));
}
}Представление Blade (resources/views/users/index.blade.php):
<h1>Пользователи</h1>
<ul>
@foreach ($users as $user)
<li>{{ $user->name }}</li>
@endforeach
</ul>Цель: максимальная продуктивность и надёжность. Случаи: сложные коммерческие приложения, требующие безопасности и поддержки.
Типичные проблемы: избыточная функциональность для маленьких проектов, сложная настройка окружения. Решение – использовать Lumen для микросервисов.
Развернутые примеры реализации компонентов
Маршрутизатор с middleware и группами
Расширенный маршрутизатор поддерживает добавление промежуточного ПО (middleware) и группировку путей. Пример на основе паттерна цепочки обязанностей.
interface MiddlewareInterface {
public function handle(Request $request, callable $next): Response;
}
class AuthMiddleware implements MiddlewareInterface {
public function handle(Request $request, callable $next): Response {
if (!$request->hasUser()) {
return new Response('Не авторизован', 403);
}
return $next($request);
}
}
class Router {
private array $routes = [];
private array $globalMiddlewares = [];
public function addMiddleware(MiddlewareInterface $middleware): void {
$this->globalMiddlewares[] = $middleware;
}
public function addRoute(string $method, string $pattern, callable $handler, array $middlewares = []): void {
$this->routes[] = compact('method', 'pattern', 'handler', 'middlewares');
}
public function dispatch(Request $request): Response {
$uri = $request->getUri();
$method = $request->getMethod();
foreach ($this->routes as $route) {
if ($route['method'] !== $method) continue;
$regex = preg_replace('/\{([a-z]+)\}/', '(?P<$1>[^/]+)', $route['pattern']);
if (preg_match("~^$regex$~", $uri, $matches)) {
$request->setParams(array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY));
$handler = $route['handler'];
foreach (array_merge($this->globalMiddlewares, $route['middlewares']) as $middleware) {
$handler = function ($req) use ($middleware, $handler) {
return $middleware->handle($req, $handler);
};
}
return $handler($request);
}
}
return new Response('Страница не найдена', 404);
}
}Результат: при обращении к защищенному маршруту без аутентификации будет возвращена ошибка 403.
HTTP 403 Forbidden Не авторизован
Работа с базой данных через PDO и подготовленные запросы
Безопасное взаимодействие с MySQL с использованием подготовленных выражений предотвращает SQL-инъекции.
class Database {
private PDO $pdo;
public function __construct(string $dsn, string $user, string $pass) {
$this->pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
}
public function findById(string $table, int $id): ?array {
$stmt = $this->pdo->prepare("SELECT * FROM $table WHERE id = :id");
$stmt->execute([':id' => $id]);
$result = $stmt->fetch();
return $result ?: null;
}
public function insert(string $table, array $data): int {
$columns = implode(', ', array_keys($data));
$placeholders = ':' . implode(', :', array_keys($data));
$stmt = $this->pdo->prepare("INSERT INTO $table ($columns) VALUES ($placeholders)");
$stmt->execute($data);
return (int) $this->pdo->lastInsertId();
}
}Пример использования:
$db = new Database('mysql:host=localhost;dbname=test', 'root', '');
$user = $db->findById('users', 42);
if ($user) {
echo 'Имя: ' . htmlspecialchars($user['name']);
} else {
echo 'Пользователь не найден';
}Результат (при наличии пользователя с id=42):
Имя: Иван
Команда для создания и выполнения миграций (без фреймворка)
Скрипт миграций позволяет версионировать изменения схемы БД.
<?php
// migrate.php
$migrationsDir = __DIR__ . '/migrations';
$files = scandir($migrationsDir);
$db = new PDO('mysql:host=localhost;dbname=test', 'root', '');
// Создать таблицу для отслеживания выполненных миграций
$db->exec("CREATE TABLE IF NOT EXISTS migrations (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) UNIQUE)");
foreach ($files as $file) {
if (pathinfo($file, PATHINFO_EXTENSION) === 'sql') {
$stmt = $db->prepare("SELECT COUNT(*) FROM migrations WHERE name = :name");
$stmt->execute([':name' => $file]);
if ($stmt->fetchColumn() == 0) {
$sql = file_get_contents($migrationsDir . '/' . $file);
$db->exec($sql);
$stmt = $db->prepare("INSERT INTO migrations (name) VALUES (:name)");
$stmt->execute([':name' => $file]);
echo "Выполнена миграция: $file\n";
}
}
}Пример файла миграции 001_create_users.sql:
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL
);Результат выполнения:
Выполнена миграция: 001_create_users.sql
Реализация шаблонизатора с поддержкой блоков и наследования
Собственный шаблонизатор на PHP может заменить Blade в небольших проектах.
class Template {
private string $basePath;
public function __construct(string $basePath) {
$this->basePath = rtrim($basePath, '/') . '/';
}
public function render(string $template, array $data = []): string {
ob_start();
extract($data);
include $this->basePath . $template . '.php';
return ob_get_clean();
}
}
// Использование шаблона с секциями
// layout.php:
// <html><head><title><?= $title ?></title></head><body><?= $content ?></body></html>
// content.php:
// <?php $title = 'Главная'; ob_start(); ?>
// <h1>Добро пожаловать</h1>
// <?php $content = ob_get_clean(); include 'layout.php'; ?>
$tpl = new Template(__DIR__ . '/views');
echo $tpl->render('content');Результат:
<html><head><title>Главная</title></head><body><h1>Добро пожаловать</h1></body></html>
Валидация входящих данных
Простой класс валидации, проверяющий обязательные поля и формат email.
class Validator {
private array $errors = [];
public function validate(array $data, array $rules): bool {
foreach ($rules as $field => $ruleSet) {
foreach ($ruleSet as $rule) {
if ($rule === 'required' && empty($data[$field])) {
$this->errors[$field][] = 'Поле обязательно';
}
if ($rule === 'email' && !empty($data[$field]) && !filter_var($data[$field], FILTER_VALIDATE_EMAIL)) {
$this->errors[$field][] = 'Некорректный email';
}
}
}
return empty($this->errors);
}
public function getErrors(): array {
return $this->errors;
}
}Пример:
$validator = new Validator();
$data = ['name' => '', 'email' => 'invalid'];
$rules = ['name' => ['required'], 'email' => ['required', 'email']];
if (!$validator->validate($data, $rules)) {
print_r($validator->getErrors());
}Результат:
Array
(
[name] => Array
(
[0] => Поле обязательно
)
[email] => Array
(
[0] => Поле обязательно
[1] => Некорректный email
)
)