Разработка модуля ядра приложения в PHP

Раздел: Архитектура приложений PHP -> Ядро приложения

Роль модуля ядра приложения PHP

Центральный элемент любой веб-системы на PHP - файл index.php, выступающий точкой входа. Архитектура ядра определяет, как обрабатываются запросы, загружаются классы и маршрутизируется управление. Ниже разобраны основные подходы с примерами.

Как организовать фронт-контроллер с единой точкой входа?

Наиболее эффективное решение - создание файла index.php, который принимает все HTTP-запросы (через .htaccess или конфигурацию веб-сервера) и передаёт управление ядру приложения. Классическая реализация включает:

  1. Определение констант (пути к корню, папкам).
  2. Подключение автозагрузчика Composer или собственного.
  3. Создание и запуск роутера, который разбирает 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 завершён

Модуль ядра приложения PHP - comments

En
Index php app core module (php)