Мінімальний додаток
Ця сторінка — не «швидкий старт продукту», а лабораторія: повний
робочий HTTP-flow лише на php-concept/core-2.
Без Stack, без View extension, без config. Навіщо: побачити, що саме
дає ядро, перш ніж накладати capabilities. У прикладі три типові
відповіді — text, JSON, HTML — щоб не змішувати «як працює Core»
з «як малює Twig».
Коли цього достатньо: мікросервіс / health-check / навчальний scratch. Коли ні — і потрібні сесія, форми, БД — беріть рецепт першого HTTP-запиту і Stack; цей приклад лишайте як референс ядра.
Встановлення
Core сам по собі не emit-ить response у SAPI: emitter — окрема
відповідальність entry point. Тому другий пакет — лише для
public/index.php.
composer require php-concept/core-2
composer require laminas/laminas-httphandlerrunner # для public/index.php
Структура
Розділення як у «справжньому» додатку: entry тонкий, bootstrap збирає контейнер, routes — окремий файл. Так пізніше легше перейти на Stack, не переписуючи каркас.
my-app/
├── bootstrap
├──── app.php
├── public
├──── index.php
├── routes
├──── web.php
├── src
├──── Controllers
├─────── PingController.php
├─────── PageController.php
├── templates
└──── home.php
Контролер
PingController не тягне ResponseFactory з extension Http —
будує Diactoros-відповіді напряму. Навіщо: показати, що Core
вимагає лише ResponseInterface, а не конкретний factory.
<?php declare(strict_types=1);
namespace App\Controllers;
use Laminas\Diactoros\Response\JsonResponse;
use Laminas\Diactoros\Response\TextResponse;
use Psr\Http\Message\ResponseInterface;
final class PingController
{
public function pingText(): ResponseInterface
{
return new TextResponse('pong');
}
public function pingJson(): ResponseInterface
{
return new JsonResponse(['message' => 'pong']);
}
}
HTML-контролер (повний flow з шаблоном)
HTML тут — через ob_start + PHP-шаблон, не Twig.
Коли: щоб продемонструвати HTML-відповідь без View brick.
Для продакшену з layout/partials підключайте extension View
(див. рецепти) — ядро навмисно цього не вміє.
<?php declare(strict_types=1);
namespace App\Controllers;
use Laminas\Diactoros\Response\HtmlResponse;
use Psr\Http\Message\ResponseInterface;
final class PageController
{
public function __construct(
private readonly string $templatesDir,
) {}
public function home(): ResponseInterface
{
$title = 'Core 2 Minimal App';
ob_start();
require $this->templatesDir . '/home.php';
$html = (string) ob_get_clean();
return new HtmlResponse($html);
}
}
<!-- templates/home.php -->
<!doctype html>
<html lang=\"uk\">
<head>
<meta charset=\"utf-8\">
<title><?= htmlspecialchars($title, ENT_QUOTES, 'UTF-8') ?></title>
</head>
<body>
<h1><?= htmlspecialchars($title, ENT_QUOTES, 'UTF-8') ?></h1>
<p>Сторінка згенерована мінімальним core-only додатком.</p>
</body>
</html>
Маршрути
HTTP surface окремим файлом: видно всі URL без читання bootstrap.
Kernel під час boot зробить require і передасть
$router у scope — вам лишається оголосити маршрути.
<?php declare(strict_types=1);
use App\Controllers\PageController;
use App\Controllers\PingController;
use League\Route\Router;
/** @var Router $router */
$router->get('/ping-text', [PingController::class, 'pingText']);
$router->get('/ping-json', [PingController::class, 'pingJson']);
$router->get('/', [PageController::class, 'home']);
Bootstrap
Тут зібрано все, що в Stack зробив би addHttp(): реєстрація
HttpKernelServiceProvider з явними шляхами і базовим
resolver chain. PageController реєструємо вручну — йому
потрібен templatesDir, якого ReflectionContainer «не знає».
Навіщо два core resolvers одразу: щоб handler міг приймати
request і параметри URL без магії в RouteStrategy.
<?php declare(strict_types=1);
use App\Controllers\PageController;
use Concept\Core\App;
use Concept\Core\Http\Routing\Resolvers\RouteParameterArgumentResolver;
use Concept\Core\Http\Routing\Resolvers\ServerRequestArgumentResolver;
use Concept\Core\Providers\Http\HttpKernelServiceProvider;
$root = dirname(__DIR__);
$app = App::create();
$container = $app->getContainer();
$container->add(PageController::class, fn() => new PageController(
templatesDir: $root . '/templates',
))->setShared(true);
$app->registerServiceProviders([
new HttpKernelServiceProvider(
routePaths: [$root . '/routes/web.php'],
resolvers: [
new ServerRequestArgumentResolver(),
new RouteParameterArgumentResolver(),
],
),
]);
return $app;
Точка входу
Той самий тонкий SAPI-файл, що й у рецептах: autoload → app → handle → emit. Збірка лишається в bootstrap — entry не чіпають, коли додасте middleware чи інший controller.
<?php declare(strict_types=1);
use Concept\Core\App;
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
require __DIR__ . '/../vendor/autoload.php';
/** @var App $app */
$app = require __DIR__ . '/../bootstrap/app.php';
$response = $app->handle();
(new SapiEmitter())->emit($response);
Запуск
Мета перевірки — три різні Content-Type з одного ядра, без «магії» View. Якщо text/JSON/HTML відповідають — Core працює; далі можна підключати Stack.
php -S localhost:8080 -t public
curl -s http://localhost:8080/ping-text
# pong
curl -s http://localhost:8080/ping-json
# {"message":"pong"}
curl -i http://localhost:8080/
# HTTP/1.1 200 OK
# Content-Type: text/html; charset=utf-8
# ... HTML сторінка з templates/home.php ...
Що відбувається
Ланцюжок нижче — контракт ядра. Stack і extensions додадуть вузли
навколо (providers з bricks, middleware), але середина
RouteStrategy лишиться тією самою.
public/index.php
→ bootstrap/app.php
→ register PageController у контейнері
→ HttpKernelServiceProvider (Router, ServerRequest, RouteStrategy)
→ App::handle()
→ Router::dispatch(container request)
→ RouteStrategy::invokeRouteCallable()
→ runInterceptors()
→ prepareRequest()
→ resolveArguments()
→ PageController::home() / PingController::ping*
→ SapiEmitter
Офіційна документація
- Concept Core repository
- League Route
- League Container docs
- Laminas Diactoros docs
- PHP Output Buffering (ob_start)
Що додати далі
Коли core-only уже зрозумілий — нарощуйте те, чого бракує саме вашому сценарію, а не «весь стек одразу»:
- Власний
ServiceProviderдля реєстрації контролерів і сервісів - PSR-15 middleware на групах маршрутів
RouteInterceptorInterfaceу constructor HttpKernel- Власний
ArgumentResolverInterfaceу chain EventDispatcherInterface+ слухачіRequestHandled
Зручніша збірка з capabilities —
1. Перший HTTP-запит
(Stack + addHttp()). Огляд кроків —
Огляд.