Практическое построение фреймворка на PHP: от идеи до реализации
Основные компоненты PHP фреймворка
Разработка собственного фреймворка начинается с выбора архитектуры. Ниже рассмотрены ключевые части: маршрутизация, контейнер внедрения зависимостей, шаблонизатор и обработка ошибок. Для каждой части приводится основное решение (класс rbase) и альтернативные варианты (класс rvar).
Как организовать маршрутизацию запросов?
Вариант 1: простой роутер на операторе switch. Подходит для очень маленьких проектов.
$uri = $_SERVER['REQUEST_URI'];
switch (true) {
case $uri === '/':
$controller = new HomeController();
break;
case preg_match('#^/user/(\d+)$#', $uri, $matches):
$controller = new UserController($matches[1]);
break;
default:
http_response_code(404);
exit;
}разработка php фреймворка (разработка php фреймворка)
Вариант 2: роутер на основе массива правил с регулярными выражениями.
class Router {
private array $routes = [];
public function add(string $pattern, callable $handler): void {
$this->routes[$pattern] = $handler;
}
public function dispatch(string $uri): void {
foreach ($this->routes as $pattern => $handler) {
if (preg_match($pattern, $uri, $params)) {
$handler($params);
return;
}
}
throw new \RuntimeException('Route not found');
}
}Основное решение: композиция роутера с контейнером внедрения зависимостей. Роутер хранит маршруты, а после сопоставления извлекает контроллер из контейнера.
class Router {
private array $routes = [];
public function add(string $method, string $pattern, string $controllerClass, string $action): void {
$this->routes[] = compact('method', 'pattern', 'controllerClass', 'action');
}
public function resolve(Container $container): void {
$method = $_SERVER['REQUEST_METHOD'];
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
foreach ($this->routes as $route) {
if ($route['method'] !== $method) continue;
if (preg_match($route['pattern'], $uri, $params)) {
$controller = $container->get($route['controllerClass']);
$controller->{$route['action']}(...array_slice($params, 1));
return;
}
}
throw new \RuntimeException('Not Found', 404);
}
}Как реализовать контейнер внедрения зависимостей?
Вариант 1: ручное создание объектов. Простой, но нарушает принцип инверсии зависимостей.
$db = new PDO('sqlite:db.sqlite');
$repository = new UserRepository($db);
$controller = new UserController($repository);Вариант 2: простой контейнер на ассоциативном массиве. Не поддерживает автозаполнение.
$container = [
'db' => new PDO('sqlite:db.sqlite'),
'repository' => function () use ($container) {
return new UserRepository($container['db']);
},
];
$controller = $container['repository']();Основное решение: PSR-11 контейнер с поддержкой автозаполнения через Reflection.
class Container {
private array $definitions = [];
private array $instances = [];
public function set(string $id, callable $factory): void {
$this->definitions[$id] = $factory;
}
public function get(string $id): object {
if (!isset($this->instances[$id])) {
$this->instances[$id] = $this->definitions[$id]($this);
}
return $this->instances[$id];
}
public function autowire(string $class): object {
$ref = new ReflectionClass($class);
$constructor = $ref->getConstructor();
if (!$constructor) {
return $ref->newInstance();
}
$params = [];
foreach ($constructor->getParameters() as $param) {
$type = $param->getType();
if ($type && !$type->isBuiltin()) {
$params[] = $this->get($type->getName());
} else {
// можно задавать значения по умолчанию
$params[] = $param->getDefaultValue();
}
}
return $ref->newInstanceArgs($params);
}
}Как работать с шаблонами?
Вариант 1: вывод HTML прямо внутри PHP (спагетти-код).
$name = 'World';
echo '<!DOCTYPE html><html><head><title>Hello</title></head><body><h1>Hello ' . htmlspecialchars($name) . '</h1></body></html>';Вариант 2: простой шаблонизатор с буферизацией вывода (отдельные файлы .phtml).
function render(string $template, array $data): string {
extract($data);
ob_start();
include __DIR__ . '/templates/' . $template . '.phtml';
return ob_get_clean();
}
echo render('home', ['name' => 'World']);Основное решение: использование готового шаблонизатора с экранированием и наследованием, например Twig.
use Twig\Environment;
use Twig\Loader\FilesystemLoader;
$loader = new FilesystemLoader(__DIR__ . '/templates');
$twig = new Environment($loader, ['cache' => __DIR__ . '/cache']);
echo $twig->render('home.twig', ['name' => 'World']);Как обрабатывать ошибки?
Вариант 1: использование стандартных функций set_error_handler и set_exception_handler.
set_exception_handler(function (Throwable $e) {
http_response_code(500);
echo '<h1>500 Internal Server Error</h1><p>' . htmlspecialchars($e->getMessage()) . '</p>';
});Основное решение: создание специализированного класса ErrorHandler, который логирует ошибку и возвращает JSON или HTML с кодом ответа.
class ErrorHandler {
private $logger;
public function __construct(callable $logger) {
$this->logger = $logger;
}
public function handle(Throwable $e): void {
($this->logger)($e);
http_response_code(500);
header('Content-Type: application/json');
echo json_encode(['error' => 'Internal server error']);
}
}
// использование
$handler = new ErrorHandler(fn($e) => error_log((string)$e));
set_exception_handler([$handler, 'handle']);Расширенные примеры реализации компонентов фреймворка
Полноценный роутер с поддержкой middleware
Роутер может выполнять цепочку middleware перед вызовом контроллера. Пример основан на PSR-15.
interface MiddlewareInterface {
public function handle(callable $next): void;
}
class Router {
private array $routes = [];
private array $middlewares = [];
public function addMiddleware(MiddlewareInterface $middleware): void {
$this->middlewares[] = $middleware;
}
public function add(string $method, string $pattern, callable $handler): void {
$this->routes[] = compact('method', 'pattern', 'handler');
}
public function run(): void {
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$method = $_SERVER['REQUEST_METHOD'];
foreach ($this->routes as $route) {
if ($route['method'] !== $method) continue;
if (preg_match($route['pattern'], $uri, $params)) {
$core = function () use ($route, $params) {
($route['handler'])($params);
};
// оборачиваем middleware снаружи внутрь
$pipeline = array_reduce(
array_reverse($this->middlewares),
function ($next, MiddlewareInterface $mw) {
return function () use ($mw, $next) {
$mw->handle($next);
};
},
$core
);
$pipeline();
return;
}
}
http_response_code(404);
}
}
// Пример middleware для логирования
class LoggingMiddleware implements MiddlewareInterface {
public function handle(callable $next): void {
error_log('Request: ' . $_SERVER['REQUEST_URI']);
$next();
}
}Контейнер с поддержкой PSR-11 и автозаполнением
Реализация, соответствующая интерфейсу Psr\Container\ContainerInterface, с автоматическим разрешением зависимостей через Reflection и обработкой циклических ссылок.
use Psr\Container\ContainerInterface;
class Container implements ContainerInterface {
private array $definitions = [];
private array $instances = [];
private array $resolving = []; // для обнаружения циклов
public function set(string $id, callable $factory): void {
$this->definitions[$id] = $factory;
}
public function get(string $id): object {
if (isset($this->resolving[$id])) {
throw new \RuntimeException("Ciclic dependency for $id");
}
if (!isset($this->instances[$id])) {
$this->resolving[$id] = true;
if (isset($this->definitions[$id])) {
$this->instances[$id] = $this->definitions[$id]($this);
} else {
$this->instances[$id] = $this->autowire($id);
}
unset($this->resolving[$id]);
}
return $this->instances[$id];
}
public function has(string $id): bool {
return isset($this->definitions[$id]) || class_exists($id);
}
private function autowire(string $class): object {
$ref = new ReflectionClass($class);
if (!$ref->isInstantiable()) {
throw new \InvalidArgumentException("Class $class is not instantiable");
}
$constructor = $ref->getConstructor();
if (!$constructor) {
return $ref->newInstance();
}
$args = [];
foreach ($constructor->getParameters() as $param) {
$type = $param->getType();
if ($type && !$type->isBuiltin()) {
$args[] = $this->get($type->getName());
} elseif ($param->isDefaultValueAvailable()) {
$args[] = $param->getDefaultValue();
} else {
throw new \RuntimeException("Cannot resolve parameter \${$param->getName()} for class $class");
}
}
return $ref->newInstanceArgs($args);
}
}Шаблонизатор с наследованием (простая реализация)
Минималистичная система шаблонов, поддерживающая блоки и extends, вдохновлённая Smart или Twig.
class Template {
private string $basePath;
public function __construct(string $basePath) {
$this->basePath = $basePath;
}
public function render(string $template, array $data = []): string {
extract($data);
ob_start();
include $this->basePath . '/' . $template . '.php';
return ob_get_clean();
}
// Пример использования в шаблоне:
// {% extends 'layout' %}
// {% block content %}...{% endblock %}
}
// Функция-помощник для парсинга extends и block (упрощённо)
function parseTemplate(string $content, Template $engine): string {
// можно реализовать замену с помощью регулярных выражений
return $content;
}Результат работы шаблонизатора с наследованием (входной шаблон):
Базовая структура применена: блок content заменён на переданный контент.
Обработчик ошибок с поддержкой разных форматов
Класс, который определяет формат ответа в зависимости от заголовка Accept.
class HttpErrorHandler {
public function __invoke(Throwable $e): void {
$statusCode = ($e instanceof \InvalidArgumentException) ? 400 : 500;
http_response_code($statusCode);
$accept = $_SERVER['HTTP_ACCEPT'] ?? 'text/html';
if (strpos($accept, 'application/json') !== false) {
header('Content-Type: application/json');
echo json_encode([
'error' => $e->getMessage(),
'code' => $statusCode,
]);
} else {
header('Content-Type: text/html; charset=utf-8');
echo sprintf(
'<h1>Ошибка %d</h1><p>%s</p>',
$statusCode,
htmlspecialchars($e->getMessage())
);
}
}
}
// Регистрация
set_exception_handler(new HttpErrorHandler());