Построение веб-приложения на PHP: обзор архитектурных решений

Раздел: Веб-разработка на 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
        )
)

Веб-приложение на PHP - comments

En
Application web php (php)