1. Перший HTTP-запит
Хочеться одразу HTML, форми, сесію — але тоді не видно, де кінчається
«просто відповісти на URL» і починається решта. Тому перший крок —
найменша робоча збірка: один маршрут GET /, відповідь
Hello, world!. Без config, PathManager, View, Session
чи Database — щоб побачити HTTP-цикл окремо від усього іншого.
Лише Core + extension Http, зібрані через Stack. Йдемо від простого до складнішого — п’ять файлів у порядку, як їх читає запит. На кожному — спочатку навіщо файл існує, потім код.
Що входить у збірку і навіщо
-
addHttp()— ізолює HTTP-шар (Router, kernel, response helpers) у окрему capability: без цього виклику в контейнері немає маршрутизації. Див. Http brick -
HttpServiceProvider—ResponseFactory,UrlGeneratorз’являються лише після цього brick; controller не тягне «глобальні» хелпери з повітря -
HttpKernelServiceProvider— підключає Router і route file без прихованих «auto routes»: шлях до routes ви задаєте самі -
Два core resolvers:
ServerRequestArgumentResolver,RouteParameterArgumentResolver— явний ланцюжок аргументів handler-а (request / параметри URL), без магії
1. public/index.php
Web-сервер має один обов’язок: завантажити додаток, отримати response і віддати його клієнту. Якщо зібрати Stack прямо тут — кожна зміна capabilities чіпатиме SAPI-файл, а тести без HTTP emitter стануть незручними. Тому entry point тримаємо тонким: autoload → app → handle → emit.
<?php declare(strict_types=1);
use Concept\Core\App;
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
require_once __DIR__ . '/../vendor/autoload.php';
/** @var App $app */
$app = require_once __DIR__ . '/../bootstrap/app.php';
$response = $app->handle();
(new SapiEmitter)->emit($response);
App::handle() бере request з globals, передає в Router
і повертає PSR-7 response. Уся збірка сервісів — у
bootstrap/app.php: цей файл можна не чіпати, коли
додасте View чи Session.
2. bootstrap/app.php
Розділяємо створення App і реєстрацію providers:
той самий bootstrap можна викликати з тестів чи CLI без HTTP emitter.
Один Bootstrap-клас — єдина точка, де видно, які capabilities
увімкнені; index.php про це не знає.
<?php declare(strict_types=1);
use Concept\App\Bootstrap\ApplicationStackBootstrap;
use Concept\Core\App;
use League\Container\Container;
$app = App::create();
/** @var Container $container */
$container = $app->getContainer();
(new ApplicationStackBootstrap())->boot($container);
return $app;
Поки що без Early error handling, Foundation (config/path) і Runtime — їх додамо, коли з’явиться реальна потреба, а не «на всяк випадок». Зараз важливо побачити мінімальний шлях до першого response.
3. routes/web.php
HTTP surface тримаємо окремим файлом: URL → handler видно без читання Bootstrap чи контролерів. Коли з’являться middleware — списки теж житимуть тут: поведінка запиту не ховається в Stack. Інакше «що відбувається на /contact» довелося б шукати в кількох місцях.
<?php declare(strict_types=1);
use Concept\App\Controllers\IndexController;
use League\Route\Router;
/** @var Router $router */
$router->get('/', [IndexController::class, 'index'])->setName('home');
Файл підключає HttpKernel під час boot Stack (див. наступний крок).
Змінна $router уже є в scope — її створює kernel перед
require route file; вам лишається лише оголосити маршрути.
4. ApplicationStackBootstrap.php
Ми використовуємо addHttp(), щоб ізолювати HTTP-шар:
Router, request і response factory з’являються лише коли capability
явно увімкнена. Це дозволяє тестувати маршрути й handlers без View,
Session чи БД — і розуміти, що саме «дає» цей brick.
Шлях до route file — literal string: на цьому кроці config ще не потрібен і не повинен «підсовувати» шляхи непомітно. Коли пізніше з’явиться PathManager, ви вже знатимете, що він замінює.
<?php declare(strict_types=1);
namespace Concept\App\Bootstrap;
use Concept\Stack\ConceptStack;
use League\Container\Container;
final class ApplicationStackBootstrap
{
public function boot(Container $container): void
{
$rootPath = __DIR__ . '/../../../';
$stack = ConceptStack::create();
$stack->addHttp()
->setRoutes([$rootPath . 'routes/web.php']);
foreach ($stack->providers() as $provider) {
$container->addServiceProvider($provider);
}
}
}
addHttp() збирає Http extension і HttpKernel з базовим
resolver chain. providers() повертає готові ServiceProvider —
їх додаємо в container явно, а не «десь у фоні». Додаткові опції
builder-а (setInterceptors, withFormRequests, …) —
у Http brick; у рецепт потрапляють
лише коли крок їх потребує.
5. IndexController.php
Handler не знає про Stack і не читає config: він отримує лише те, що оголосив у constructor. Так легше підміняти залежності в тестах і видно контракт методу без «глобальних хелперів». Перша відповідь — сирий текст у body: HTML з’явиться, коли підключимо View.
<?php declare(strict_types=1);
namespace Concept\App\Controllers;
use Concept\Extensions\Http\Response\ResponseFactory;
use Psr\Http\Message\ResponseInterface;
class IndexController
{
public function __construct(
private readonly ResponseFactory $responseFactory,
) {}
public function index(): ResponseInterface
{
$response = $this->responseFactory->createResponse();
$response->getBody()->write('Hello, world!');
return $response;
}
}
ResponseFactory — з Http extension; container resolve-ить
його при dispatch, бо ми вже викликали addHttp().
Запуск і перевірка
Тег step-01-http містить мінімальний HTTP-стек. Очікуємо
plain text Hello, world!, не HTML і не 500.
Запуск
git clone … concept-recipes && cd concept-recipes
git checkout step-01-http
rm -rf storage/cache storage/sessions storage/logs
composer install
cp docker-compose.yml.example docker-compose.yml
docker compose up -d --build
Розробка Core/Stack/Extensions локально (path repos + symlinks):
cp docker-compose.dev.yml.example docker-compose.yml,
cp composer-dev.json.example composer-dev.json,
COMPOSER=composer-dev.json composer install — див. README у репозиторії.
Перевірка в браузері
Відкрийте
http://localhost:8089/
— у вікні має з’явитись один рядок Hello, world! без HTML-розмітки.
Перевірка з CLI
curl -s http://localhost:8089/
# Hello, world!
Ланцюжок запиту
Нижче — шлях одного GET / від Apache/PHP до emitter.
Кожен наступний крок додасть вузли в середину (View, middleware, БД),
але кінці ланцюжка лишаться тими самими.
public/index.php
→ bootstrap/app.php
→ ApplicationStackBootstrap::boot()
→ ConceptStack::addHttp() → providers()
→ App::handle()
→ Router::dispatch(request)
→ RouteStrategy → IndexController::index()
→ SapiEmitter
Повний опис — у Життєвий цикл запиту.
Крок 2 — HTML-сторінки
(addView() + Twig): розмітка виходить з controller,
HTTP-шар лишається як був. Огляд усіх кроків —
Огляд.