Мінімальний додаток

Ця сторінка — не «швидкий старт продукту», а лабораторія: повний робочий 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

Що додати далі

Коли core-only уже зрозумілий — нарощуйте те, чого бракує саме вашому сценарію, а не «весь стек одразу»:

  • Власний ServiceProvider для реєстрації контролерів і сервісів
  • PSR-15 middleware на групах маршрутів
  • RouteInterceptorInterface у constructor HttpKernel
  • Власний ArgumentResolverInterface у chain
  • EventDispatcherInterface + слухачі RequestHandled

Зручніша збірка з capabilities — 1. Перший HTTP-запит (Stack + addHttp()). Огляд кроків — Огляд.