phpsoftbox/application

Application core for the PhpSoftBox framework

Maintainers

Package info

github.com/phpsoftbox/application

pkg:composer/phpsoftbox/application

Statistics

Installs: 76

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

dev-master 2026-03-18 21:11 UTC

This package is auto-updated.

Last update: 2026-03-18 21:12:27 UTC


README

Минимальное приложение для работы с PSR-15 пайплайном.

Быстрый старт

use PhpSoftBox\Application\Application;
use PhpSoftBox\Application\ErrorHandler\JsonExceptionHandler;
use PhpSoftBox\Application\ErrorHandler\HtmlExceptionHandler;
use PhpSoftBox\Application\ErrorHandler\ContentNegotiationExceptionHandler;
use PhpSoftBox\Application\Middleware\ErrorHandlerMiddleware;
use PhpSoftBox\Router\Router;

$router = new Router($resolver, $dispatcher, $collector);
$exceptionHandler = new ContentNegotiationExceptionHandler(
    new JsonExceptionHandler($responseFactory, $streamFactory, includeDetails: true),
    new HtmlExceptionHandler($responseFactory, $streamFactory, includeDetails: true),
);

$app = new Application($router, [
    new ErrorHandlerMiddleware($exceptionHandler),
]);

$response = $app->handle($request);

Настройка формата ошибок (Deciders)

ContentNegotiationExceptionHandler поддерживает реестр deciders для выбора формата ответа на ошибку. Decider возвращает ExceptionFormat::JSON, ExceptionFormat::HTML или null (тогда срабатывает стандартная логика по Accept и X-Requested-With).

use PhpSoftBox\Application\ErrorHandler\ContentNegotiationExceptionHandler;
use PhpSoftBox\Application\ErrorHandler\ExceptionFormat;
use PhpSoftBox\Application\ErrorHandler\ExceptionFormatDeciderInterface;
use PhpSoftBox\Application\ErrorHandler\ExceptionFormatDeciderRegistry;
use PhpSoftBox\Application\ErrorHandler\HtmlExceptionHandler;
use PhpSoftBox\Application\ErrorHandler\JsonExceptionHandler;
use Psr\Http\Message\ServerRequestInterface;
use Throwable;

final class ApiExceptionDecider implements ExceptionFormatDeciderInterface
{
    public function __invoke(Throwable $exception, ServerRequestInterface $request): ?ExceptionFormat
    {
        $path = ltrim($request->getUri()->getPath(), '/');
        if (str_starts_with($path, 'api/')) {
            return ExceptionFormat::JSON;
        }

        if ($request->getHeaderLine('X-Inertia') !== '') {
            return ExceptionFormat::HTML;
        }

        return null;
    }
}

$deciders = new ExceptionFormatDeciderRegistry([
    new ApiExceptionDecider(),
]);

$exceptionHandler = new ContentNegotiationExceptionHandler(
    new JsonExceptionHandler($responseFactory, $streamFactory, includeDetails: true),
    new HtmlExceptionHandler($responseFactory, $streamFactory, includeDetails: true),
    $deciders,
);

Репортинг ошибок в ErrorHub

use PhpSoftBox\Application\ErrorHandler\DefaultExceptionHandler;
use PhpSoftBox\Application\ErrorHandler\ErrorHubExceptionReporter;
use PhpSoftBox\Application\ErrorHandler\LoggerExceptionReporter;
use PhpSoftBox\Application\Middleware\ErrorHandlerMiddleware;

$errorhubReporter = new ErrorHubExceptionReporter(
    baseUrl: 'https://errorhub.getstash-dev.ru',
    projectKey: 'YOUR_PROJECT_KEY',
    token: 'YOUR_BEARER_TOKEN', // опционально
    username: null, // опционально (basic auth)
    password: null,
    tags: ['env:prod', 'app:backend'],
);

$fallback = new ContentNegotiationExceptionHandler(
    new JsonExceptionHandler($responseFactory, $streamFactory, includeDetails: true),
    new HtmlExceptionHandler($responseFactory, $streamFactory, includeDetails: true),
);

$exceptionHandler = new DefaultExceptionHandler(
    fallbackHandler: $fallback,
    responseFactory: $responseFactory,
    session: $session,
    reporters: [
        new LoggerExceptionReporter($logger),
        $errorhubReporter,
    ],
);

$app = new Application($router, [
    new ErrorHandlerMiddleware($exceptionHandler),
]);

Если нужно добавить теги на уровне конкретного исключения:

use PhpSoftBox\Application\ErrorHandler\ErrorHubExceptionInterface;
use PhpSoftBox\Application\ErrorHandler\ErrorHubExceptionTrait;

final class BillingException extends \RuntimeException implements ErrorHubExceptionInterface
{
    use ErrorHubExceptionTrait;
}

$e = new BillingException('Payment failed');
$e->setErrorHubTags(['module:billing']);
$e->setErrorHubContext(['order_id' => 42]);

RouterFactory + RouteCache

use PhpSoftBox\Application\RouterFactory;
use PhpSoftBox\Router\Cache\RouteCache;
use PhpSoftBox\Router\Dispatcher;

$cache = new RouteCache($cacheStorage);
$factory = new RouterFactory(new Dispatcher(), $cache);

$router = $factory->create(function ($routes) {
    $routes->get('/users', [UserController::class, 'index']);
});

AppFactory

use PhpSoftBox\Application\AppFactory;

$app = AppFactory::createFromContainer($container, environment: 'prod');

if (!$app->routesCached()) {
    require __DIR__ . '/routes.php';
}

Регистрация middleware

use PhpSoftBox\Application\Application;
use PhpSoftBox\Application\Middleware\ErrorHandlerMiddleware;
use PhpSoftBox\Application\Middleware\RequestSizeLimitMiddleware;
use PhpSoftBox\Session\SessionMiddleware;

$app = new Application($router, container: $container);

$app->add(new ErrorHandlerMiddleware($exceptionHandler), priority: 100);
$app->add(RequestSizeLimitMiddleware::class);

$app->alias('session', SessionMiddleware::class);
$app->middlewareGroup('web', ['session']);

Группы middleware

$webApp = $app->withMiddlewareGroups(['web']);
$response = $webApp->handle($request);

Регистрация роутов через Application

$app->get('/users', [UserController::class, 'index']);
$app->post('/users', [UserController::class, 'store']);

Методы проксируются в RouteCollector, если приложение создано с Router.

Middleware для контроллеров

$app->controllerMiddleware(UserController::class, ['auth']);
$app->controllerMiddleware(UserController::class, ['admin'], only: ['store', 'update']);

Рекомендуемый путь привязки Middleware — регистрация через Router на маршруты/группы; контроллеры/экшены используйте точечно.

Группы middleware для маршрутов

use PhpSoftBox\Application\Middleware\KernelRouteMiddlewareResolver;
use PhpSoftBox\Router\Dispatcher;
use PhpSoftBox\Router\Router;

$app->alias('auth', \PhpSoftBox\Auth\Middleware\AuthMiddleware::class);
$app->middlewareGroup('api', ['auth']);

$dispatcher = new Dispatcher(
    handlerResolver: null,
    middlewareResolver: new KernelRouteMiddlewareResolver($app->middlewareManager(), $container),
);

$router = new Router($resolver, $dispatcher, $collector);

$collector->group('/api', function ($routes) {
    $routes->get('/users', [UserController::class, 'index']);
}, ['api']);

Ответы

В приложении доступны готовые ответы:

  • JsonResponse
  • HtmlResponse
  • XmlResponse
  • TextResponse

Пример:

use PhpSoftBox\Application\Response\JsonResponse;

return new JsonResponse(['ok' => true]);

Ошибки роутера

InvalidRouteParameterException (например, когда параметр не проходит валидацию) в прод-режиме возвращает 404 Not Found. В debug-режиме сообщение исключения возвращается в ответе.

Авторизация приватных каналов Broadcaster

Обычно требуется эндпоинт /broadcast/auth, который выдаёт auth для приватных каналов. Пример с PushrChannelAuth:

use PhpSoftBox\Broadcaster\Pushr\PushrChannelAuth;
use PhpSoftBox\Http\Message\Response;
use Psr\Http\Message\ServerRequestInterface;
use function json_encode;

$app->post('/broadcast/auth', function (ServerRequestInterface $request, \PhpSoftBox\Broadcaster\Channel\ChannelRegistry $channels): Response {
    $data = (array) ($request->getParsedBody() ?? []);

    $socketId = (string) ($data['socket_id'] ?? '');
    $channel = (string) ($data['channel'] ?? '');
    $channelData = $data['channel_data'] ?? null;

    $authorization = $channels->authorize($channel, $request);
    if (!$authorization->authorized()) {
        return new Response(403);
    }

    $channelData = $authorization->channelData() ?? $channelData;

    $auth = PushrChannelAuth::token('app-1', 'secret-1', $socketId, $channel, $channelData);

    return new Response(200, ['Content-Type' => 'application/json'], json_encode([
        'auth' => $auth,
        'channel_data' => $channelData,
    ]));
});

socket_id клиент получает из события connection после подключения к WebSocket.