Разработка модуля ядра приложения в PHP
Роль модуля ядра приложения PHP
Центральный элемент любой веб-системы на PHP - файл index.php, выступающий точкой входа. Архитектура ядра определяет, как обрабатываются запросы, загружаются классы и маршрутизируется управление. Ниже разобраны основные подходы с примерами.
Как организовать фронт-контроллер с единой точкой входа?
Наиболее эффективное решение - создание файла index.php, который принимает все HTTP-запросы (через .htaccess или конфигурацию веб-сервера) и передаёт управление ядру приложения. Классическая реализация включает:
- Определение констант (пути к корню, папкам).
- Подключение автозагрузчика Composer или собственного.
- Создание и запуск роутера, который разбирает URI и вызывает соответствующий контроллер.
// index.php
<?
define('ROOT', __DIR__);
define('APP', ROOT . '/app');
require_once __DIR__ . '/vendor/autoload.php';
use App\Core\Router;
$router = new Router();
$router->dispatch($_SERVER['REQUEST_URI']);
Index php app core module (модуль ядра приложения php)
Роутер в свою очередь сопоставляет путь с конфигурацией маршрутов и вызывает нужный метод контроллера.
// Пример роутера
class Router
{
protected array $routes = [];
public function add(string $path, callable $handler): void
{
$this->routes[$path] = $handler;
}
public function dispatch(string $uri): void
{
$uri = parse_url($uri, PHP_URL_PATH);
if (isset($this->routes[$uri])) {
call_user_func($this->routes[$uri]);
} else {
http_response_code(404);
echo 'Страница не найдена';
}
}
}
// Использование
$router = new Router();
$router->add('/', function() { echo 'Главная'; });
$router->add('/about', function() { echo 'О нас'; });
Типичная ошибка:
- Забыть настроить перенаправление запросов на index.php (модуль mod_rewrite).
- Игнорирование обработки строки запроса (query string) - роутер должен отбрасывать
?param=value. - Отсутствие кэширования маршрутов - на продакшене стоит компилировать маршруты в массив для ускорения.
Решение: проверить наличие файла .htaccess с правилами RewriteEngine On и RewriteRule ^(.*)$ index.php [QSA,L]. Для упрощения использовать библиотеку nikic/FastRoute - она автоматически обрабатывает подстановки и методы HTTP.
Как реализовать ядро без внешнего роутера, используя прямой вызов скриптов?
Вариант для простых сайтов: каждый PHP-файл в корне - отдельная страница. index.php содержит только начальный HTML, а для обработки форм используются отдельные скрипты (например, login.php, register.php).
<? // index.php ?>
Простой сайт
Добро пожаловать
Проблемы такого подхода: дублирование кода (подключение БД, проверка сессии в каждом файле), неудобство изменения структуры URL, отсутствие единой точки обработки ошибок.
Типичная ошибка:
Забыть подключить общий файл конфигурации в каждом скрипте - приводит к ошибкам, если меняется подключение к БД.
Решение: использовать require_once в каждом файле или перейти к фронт-контроллеру.
Как использовать DI-контейнер в качестве ядра приложения?
Современные фреймворки (Laravel, Symfony) строят ядро на основе контейнера внедрения зависимостей. index.php создаёт контейнер, регистрирует сервисы и передаёт запрос в HTTP-kernel.
// index.php с контейнером (пример на PHP-DI)
<?
require_once __DIR__ . '/vendor/autoload.php';
use DI\ContainerBuilder;
use App\Http\Kernel;
$builder = new ContainerBuilder();
$builder->addDefinitions(__DIR__ . '/config/services.php');
$container = $builder->build();
$kernel = $container->get(Kernel::class);
$response = $kernel->handle(
Symfony\Component\HttpFoundation\Request::createFromGlobals()
);
$response->send();
Здесь контейнер автоматически разрешает зависимости роутера, контроллеров, middleware. Код ядра становится декларативным.
Типичная ошибка:
Не настроить корректно автозагрузку определений контейнера - при попытке получить класс без регистрации выпадает исключение NotFoundException.
Решение: всегда проверять, что все сервисы, используемые в Kernel::handle, зарегистрированы в конфигурации контейнера. Для отладки подойдёт Container::getEntrySource().
Как построить ядро на базе PSR-7 и Middleware?
Этот вариант популярен в микрофреймворках (Slim, Expressive). index.php создаёт PSR-7 Request, передаёт его через стэк middleware, и каждый middleware может либо обработать запрос, либо передать дальше.
// index.php для Slim 4
<?
use Slim\Factory\AppFactory;
require __DIR__ . '/vendor/autoload.php';
$app = AppFactory::create();
$app->get('/', function ($request, $response) {
$response->getBody()->write('Hello, world!');
return $response;
});
$app->run();
Ядро - это сам $app, который содержит роутер, контейнер (по умолчанию - PHP-DI) и диспетчер middleware. Такой подход легко тестировать и расширять.
Типичная ошибка:
Забыть добавить middleware для обработки ошибок (404, 500) - клиент получит пустой ответ.
Решение: добавить ErrorMiddleware как самый внешний middleware. Например, $app->addErrorMiddleware(true, true, true) в Slim.
Как эмулировать модуль ядра на чистом PHP без фреймворков?
Для микропроектов или учебных целей можно написать собственную минимальную шину запросов. index.php содержит класс Application, который регистрирует обработчики и запускает цикл.
<?
class Application
{
private array $handlers = [];
public function addHandler(callable $handler): void
{
$this->handlers[] = $handler;
}
public function run(): void
{
$requestUri = $_SERVER['REQUEST_URI'];
foreach ($this->handlers as $handler) {
if ($handler($requestUri) === true) {
return;
}
}
http_response_code(404);
}
}
$app = new Application();
$app->addHandler(function ($uri) {
if ($uri === '/') {
echo 'Главная';
return true;
}
});
$app->run();
Такой код компактен, но не подходит для крупных проектов из-за отсутствия модульности и повторного использования компонентов.
Типичная ошибка:
Обработчик возвращает false, а не true при совпадении - выполнение продолжается и может быть вызван другой обработчик.
Решение: всегда явно возвращать true после отправки ответа.
Развёрнутые примеры работы модуля ядра
Ниже представлены расширенные сценарии с выводом результатов для демонстрации поведения разных архитектур.
1. Фронт-контроллер с поддержкой динамических маршрутов
<?
// index.php
spl_autoload_register(function ($class) {
$file = __DIR__ . '/' . str_replace('\\', '/', $class) . '.php';
if (file_exists($file)) {
require $file;
}
});
use App\Core\Router;
$router = new Router();
$router->add('/user/{id}', function($id) {
echo "Пользователь ID: $id";
});
$router->add('/news', function() {
echo 'Список новостей';
});
$uri = $_SERVER['REQUEST_URI'] ?? '/';
$router->dispatch($uri);
// При запросе /user/42: Пользователь ID: 42
Реализация роутера с регулярными выражениями:
// app/Core/Router.php
class Router
{
private array $routes = [];
public function add(string $pattern, callable $handler): void
{
$this->routes[] = [
'pattern' => preg_replace('/\{([a-zA-Z_]+)\}/', '(?P<$1>[^/]+)', $pattern),
'handler' => $handler
];
}
public function dispatch(string $uri): void
{
$uri = parse_url($uri, PHP_URL_PATH);
foreach ($this->routes as $route) {
if (preg_match('#^' . $route['pattern'] . '$#', $uri, $matches)) {
$params = array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY);
call_user_func($route['handler'], ...array_values($params));
return;
}
}
http_response_code(404);
echo '404 Not Found';
}
}
// Запрос /news -> Список новостей // Запрос /user/123 -> Пользователь ID: 123 // Запрос /other -> 404 Not Found
2. Использование контейнера с авторегистрацией
<?
// index.php с PHP-DI
require_once __DIR__ . '/vendor/autoload.php';
use DI\ContainerBuilder;
use Psr\Container\ContainerInterface;
$builder = new ContainerBuilder();
$builder->addDefinitions([
'logger' => function () {
return new \Monolog\Logger('app');
},
App\Service\UserService::class => \DI\autowire(),
]);
$container = $builder->build();
/** @var ContainerInterface $c */
$c = $container;
$service = $c->get(App\Service\UserService::class);
echo $service->getUser(1);
Класс сервиса с зависимостью от логгера:
// app/Service/UserService.php
namespace App\Service;
use Psr\Log\LoggerInterface;
class UserService
{
private LoggerInterface $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function getUser(int $id): string
{
$this->logger->info("Запрос пользователя $id");
return "User#$id";
}
}
// Результат вызова index.php: User#1 // В лог-файле появится запись о запросе.
Типичная ошибка контейнера:
Если класс не описан в определении или не может быть автовайрен (например, имеет скалярные параметры конструктора), контейнер выбросит исключение. Решение: добавить явное определение с помощью \DI\create()->constructor(...).
3. Middleware-цепочка на PSR-15
<?
// index.php с промежуточным ПО
require __DIR__ . '/vendor/autoload.php';
use Laminas\Diactoros\ServerRequestFactory;
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
// Простой middleware, проверяющий токен
class AuthMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): \Psr\Http\Message\ResponseInterface
{
$token = $request->getHeaderLine('X-Auth-Token');
if ($token !== 'secret') {
$response = new \Laminas\Diactoros\Response('Unauthorized', 401);
return $response;
}
return $handler->handle($request);
}
}
// Обработчик, отдающий Hello
class HelloHandler implements RequestHandlerInterface
{
public function handle(ServerRequestInterface $request): \Psr\Http\Message\ResponseInterface
{
$response = new \Laminas\Diactoros\Response();
$response->getBody()->write('Hello, Authenticated User!');
return $response;
}
}
$request = ServerRequestFactory::fromGlobals();
$middleware = new AuthMiddleware();
$handler = new HelloHandler();
$response = $middleware->process($request, $handler);
(new SapiEmitter())->emit($response);
// Без заголовка X-Auth-Token: ответ 401 Unauthorized // С заголовком X-Auth-Token: secret -> 200 Hello, Authenticated User!
Результат работы при запросе curl -H "X-Auth-Token: secret" http://localhost:
HTTP/1.1 200 OK Hello, Authenticated User!
4. Модифицируемое ядро через события
<?
// index.php с паттерном Observer
event 'core.before_dispatch' (function ($uri) {
echo "Начало обработки запроса: $uri
";
});
class Application
{
private array $listeners = [];
public function on(string $event, callable $listener): void
{
$this->listeners[$event][] = $listener;
}
public function dispatch(string $uri): void
{
$this->fire('core.before_dispatch', [$uri]);
echo "Диспетчеризация...";
$this->fire('core.after_dispatch', [$uri]);
}
private function fire(string $event, array $args): void
{
foreach ($this->listeners[$event] ?? [] as $listener) {
call_user_func_array($listener, $args);
}
}
}
$app = new Application();
$app->on('core.after_dispatch', function ($uri) {
echo "
Запрос $uri завершён";
});
$app->dispatch('/test');
Начало обработки запроса: /test Диспетчеризация... Запрос /test завершён