tobento / app-http
App http support.
Requires
- php: >=8.0
- laminas/laminas-httphandlerrunner: ^1.4
- nyholm/psr7: ^1.4
- nyholm/psr7-server: ^1.0
- psr/container: ^2.0
- psr/http-factory: ^1.0
- psr/http-message: ^1.0
- psr/http-server-handler: ^1.0
- psr/http-server-middleware: ^1.0
- tobento/app: ^1.0
- tobento/app-migration: ^1.0
- tobento/service-config: ^1.0.3
- tobento/service-cookie: ^1.0.1
- tobento/service-error-handler: ^1.0
- tobento/service-middleware: ^1.0.2
- tobento/service-migration: ^1.0
- tobento/service-requester: ^1.0.2
- tobento/service-responser: ^1.0
- tobento/service-routing: ^1.0.5
- tobento/service-session: ^1.0.1
- tobento/service-translation: ^1.0.3
- tobento/service-uri: ^1.0.1
Requires (Dev)
- mockery/mockery: ^1.6
- phpunit/phpunit: ^9.5
- tobento/app-console: ^1.0.2
- tobento/app-encryption: ^1.0
- tobento/app-translation: ^1.0.1
- tobento/app-view: ^1.0.5
- tobento/service-collection: ^1.0
- tobento/service-form: ^1.0
- tobento/service-view: ^1.0
- vimeo/psalm: ^4.0
README
Http, routing, middleware and session support for the app.
Table of Contents
- Getting Started
- Documentation
- Credits
Getting Started
Add the latest version of the app http project running this command.
composer require tobento/app-http
Requirements
- PHP 8.0 or greater
Documentation
App
Check out the App Skeleton if you are using the skeleton.
You may also check out the App to learn more about the app in general.
Http Boot
The http boot does the following:
- PSR-7 implementations
- PSR-17 implementations
- installs and loads http config file
- base and current uri implementation
- http error handling implementation
- emits response
use Tobento\App\AppFactory; // Create the app $app = (new AppFactory())->createApp(); // Adding boots $app->boot(\Tobento\App\Http\Boot\Http::class); // Run the app $app->run();
Http Config
Check out app/config/http.php
to change needed values.
Request And Response
You may access the PSR-7 and PSR-17 interfaces by the app:
use Tobento\App\AppFactory; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\StreamFactoryInterface; use Psr\Http\Message\UploadedFileFactoryInterface; use Psr\Http\Message\UriFactoryInterface; use Psr\Http\Message\UriInterface; use Tobento\Service\Uri\BaseUriInterface; use Tobento\Service\Uri\CurrentUriInterface; use Tobento\Service\Uri\PreviousUriInterface; // Create the app $app = (new AppFactory())->createApp(); // Adding boots $app->boot(\Tobento\App\Http\Boot\Http::class); $app->booting(); // PSR-7 $request = $app->get(ServerRequestInterface::class); $response = $app->get(ResponseInterface::class); // returns UriInterface $baseUri = $app->get(BaseUriInterface::class); $currentUri = $app->get(CurrentUriInterface::class); $previousUri = $app->get(PreviousUriInterface::class); // Session Boot is needed, otherwise it is always same as base uri. // PSR-17 $responseFactory = $app->get(ResponseFactoryInterface::class); $streamFactory = $app->get(StreamFactoryInterface::class); $uploadedFileFactory = $app->get(UploadedFileFactoryInterface::class); $uriFactory = $app->get(UriFactoryInterface::class); // Run the app $app->run();
Check out the Uri Service to learn more about the base and current uri.
Swap PSR-7 And PSR-17 Implementation
You might swap the PSR-7 and PSR-17 implementation to any alternative.
Check out the App - Customization to learn more about it.
Requester And Responser Boot
The requester and responser boot does the following:
- RequesterInterface implementation
- ResponserInterface implementation and adds its middleware
use Tobento\App\AppFactory; use Tobento\Service\Requester\RequesterInterface; use Tobento\Service\Responser\ResponserInterface; // Create the app $app = (new AppFactory())->createApp(); // You may add the session boot to enable // flash messages and flash input data. $app->boot(\Tobento\App\Http\Boot\Session::class); $app->boot(\Tobento\App\Http\Boot\RequesterResponser::class); $app->booting(); $requester = $app->get(RequesterInterface::class); $responser = $app->get(ResponserInterface); // Run the app $app->run();
Check out the Requester Service to learn more about it.
Check out the Responser Service to learn more about it.
Responser Messages
Messages will be translated if you have installed the App Message - Translating Messages.
Added Middleware
Middleware Boot
The middleware boot does the following:
- PSR-15 HTTP handlers (middleware) implementation
- dispatches middleware
use Tobento\App\AppFactory; // Create the app $app = (new AppFactory())->createApp(); // Adding boots $app->boot(\Tobento\App\Http\Boot\Middleware::class); $app->booting(); // add middleware aliases using app macro: $app->middlewareAliases([ 'alias' => FooMiddleware::class, ]); // add middleware group using app macro: $app->middlewareGroup(name: 'api', middlewares: [ Middleware::class, ]); // add middleware using app macro: $app->middleware(BarMiddleware::class); // Run the app $app->run();
Check out the Middleware Service to learn more about the middleware implementation.
Add Middleware via Config
You can configure middleware in the config file app/config/middleware.php
which are applied to all routes and requests:
return [ // ... 'middlewares' => [ // priority => middleware // via fully qualified class name: 8000 => \Tobento\App\Http\Middleware\SecurePolicyHeaders::class, // with build-in parameters: 7900 => [AnotherMiddleware::class, 'name' => 'Sam'], // by alias: 7800 => 'aliasedMiddleware', // by group name: 7800 => 'groupedMiddlewares', // by class instance: 7700 => new SomeMiddleware(), ], ];
Add Middleware via Boot
You might create a boot for adding middleware:
use Tobento\App\Boot; use Tobento\App\Http\Boot\Middleware; class MyMiddlewareBoot extends Boot { public const BOOT = [ // you may ensure the middleware boot. Middleware::class, ]; public function boot(Middleware $middleware) { $middleware->add(MyMiddleware::class); } }
Middleware Aliases
use Tobento\App\Boot; use Tobento\App\Http\Boot\Middleware; class MyMiddlewareBoot extends Boot { public const BOOT = [ // you may ensure the middleware boot. Middleware::class, ]; public function boot(Middleware $middleware) { $middleware->addAliases([ 'alias' => MyMiddleware::class, ]); // add by alias: $middleware->add('alias'); } }
Add Aliases via Config
You can configure middleware aliases in the config file app/config/middleware.php
:
return [ // ... 'aliases' => [ 'alias' => MyMiddleware::class, ], ];
Middleware Groups
use Tobento\App\Boot; use Tobento\App\Http\Boot\Middleware; class MyMiddlewareBoot extends Boot { public const BOOT = [ // you may ensure the middleware boot. Middleware::class, ]; public function boot(Middleware $middleware) { $middleware->addGroup(name: 'api', middlewares: [ Middleware::class, // with build-in parameters: [AnotherMiddleware::class, 'name' => 'Sam'], // by alias: 'aliasedMiddleware', // by class instance: new SomeMiddleware(), ]); // add by group: $middleware->add('api'); } }
Add Groups via Config
You can configure middleware groups in the config file app/config/middleware.php
:
return [ // ... 'groups' => [ 'name' => [ SomeMiddleware::class, ], ], ];
Available Middleware
Previous Uri Session Middleware
The Tobento\App\Http\Middleware\PreviousUriSession::class
middleware is automatically added by the Session Boot which stores the uri history in the session.
Get Previous Uri
$previousUri = $app->get(PreviousUriInterface::class);
Exclude From Previous Uri History
You may exclude a certain uri from the history by adding a X-Exclude-Previous-Uri
header on the response:
$response = $response->withHeader('X-Exclude-Previous-Url', '1');
Secure Policy Headers Middleware
This middleware will add the following secure policy headers to the response:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Content-Security-Policy: base-uri 'none'; default-src 'self'; img-src 'self' data:; script-src 'nonce-***' 'self'; object-src 'none'; style-src 'nonce-***' 'self';
In the app/config/middleware.php
file:
'middlewares' => [ 8000 => \Tobento\App\Http\Middleware\SecurePolicyHeaders::class, ],
Using inline scripts and styles
The middleware will add a Content-Security-Policy
header with a nonce for script-src
and style-src
. Therefore, to allow inline scripts or styles you must add the nonce to the html:
If using the App View bundle, you can retrieve the nonce from the view:
<style nonce="<?= $view->esc($view->get('cspNonce', '')) ?>"> ... </style> <script nonce="<?= $view->esc($view->get('cspNonce', '')) ?>"> ... </script>
Routing Boot
The routing boot does the following:
- boots http and middleware boot
- RouterInterface implementation
- adds routing macro
- adds http error handler for routing exceptions
use Tobento\App\AppFactory; use Tobento\Service\Routing\RouterInterface; // Create the app $app = (new AppFactory())->createApp(); // Adding boots $app->boot(\Tobento\App\Http\Boot\Routing::class); $app->booting(); // using interface: $router = $app->get(RouterInterface::class); $router->get('blog', function() { return ['page' => 'blog']; }); // using macros: $app->route('GET', 'foo', function() { return ['page' => 'foo']; }); // Run the app $app->run();
Check out the Routing Service to learn more about the routing.
Routing via Boot
You might create a boot for defining routes:
use Tobento\App\Boot; use Tobento\Service\Routing\RouterInterface; use Tobento\Service\Routing\RouteGroupInterface; class RoutesBoot extends Boot { public const BOOT = [ // you may ensure the routing boot. \Tobento\App\Http\Boot\Routing::class, ]; public function boot(RouterInterface $router) { // Add routes on the router $router->get('blog', [Controller::class, 'method']); // Add routes with the provided app macros $this->app->route('GET', 'blog', [Controller::class, 'method']); $this->app->routeGroup('admin', function(RouteGroupInterface $group) { $group->get('blog/{id}', function($id) { // do something }); }); $this->app->routeResource('products', ProductsController::class); $this->app->routeMatched('blog.edit', function() { // do something after the route has been matched. }); $url = $this->app->routeUrl('blog.edit', ['id' => 5])->get(); } }
Then adding your routes boot on the app:
use Tobento\App\AppFactory; // Create the app $app = (new AppFactory())->createApp(); // Adding boots $app->boot(RoutesBoot::class); // Run the app $app->run();
Domain Routing
You may specify the domains for routing in the app/config/http.php
file.
Check out the Routing Service - Domain Routing section to learn more about domain routing.
Route Handler
You may add route handlers to interact with the route handling.
Here is some benefits of adding handlers:
- you may cache responses
- on certain requests you may throw an exception to be later catched by an error handler (e.g. validation)
- and much more
First, create a route handler:
use Tobento\App\AppInterface; use Tobento\Service\Routing\RouteInterface; use Tobento\Service\Routing\RouteHandlerInterface; use Tobento\App\Http\Routing\DeclaredHandlerParameters; use Tobento\App\Http\Routing\ArgumentsHandlerParameters; use Psr\Http\Message\ServerRequestInterface; final class CustomRouteHandler implements RouteHandlerInterface { public function __construct( private AppInterface $app ) {} /** * Handles the route. * * @param RouteInterface $route * @param null|ServerRequestInterface $request * @return mixed The return value of the handler called. */ public function handle(RouteInterface $route, null|ServerRequestInterface $request = null): mixed { // You may interfere with the arguments for the route handler to be called: $arguments = $route->getParameter('_arguments'); var_dump($arguments instanceof ArgumentsHandlerParameters); // bool(true) // You may use the declared route handler parameters: $declared = $route->getParameter('_declared'); var_dump($declared instanceof DeclaredHandlerParameters); // bool(true) // Let further handlers handle it: return [$route, $request]; // Or prevent further handlers from being called // and returning the route handler result: $result = $this->app->call($route->getHandler(), $arguments->getParameters()); return [$route, $request, $result]; } }
Finally, add the route handler:
use Tobento\App\Http\Routing\RouteHandlerInterface; // Create the app $app = (new AppFactory())->createApp(); // Adding boots: $app->boot(\Tobento\App\Http\Boot\Routing::class); // Add route handler using the app on method: $app->on(RouteHandlerInterface::class, function(RouteHandlerInterface $handler) { $handler->addHandler(CustomRouteHandler::class); })->priority(1500); // Run the app $app->run();
The only handler added is the Tobento\App\Http\Routing\RequestRouteHandler::class
with a priority of 1000
.
Route List Command
If you have installed the App Console you may run the route:list command providing an overview of all the routes that are defined by your application:
php ap route:list
Displays only specific routes by its name with additional information:
php ap route:list --name=blog.show
Session Boot
The session boot does the following:
- SessionInterface implementation
- adds session middleware
use Tobento\App\AppFactory; use Tobento\Service\Session\SessionInterface; use Psr\Http\Message\ServerRequestInterface; // Create the app $app = (new AppFactory())->createApp(); // Adding boots $app->boot(\Tobento\App\Http\Boot\Middleware::class); $app->boot(\Tobento\App\Http\Boot\Routing::class); $app->boot(\Tobento\App\Http\Boot\Session::class); $app->booting(); $app->route('GET', 'foo', function(SessionInterface $session) { $session->set('key', 'value'); return ['page' => 'foo']; }); // or you may get the session from the request attributes: $app->route('GET', 'bar', function(ServerRequestInterface $request) { $session = $request->getAttribute(SessionInterface::class); $session->set('key', 'value'); return ['page' => 'bar']; }); // Run the app $app->run();
Check out the Session Service to learn more about the session in general.
Session Config
Check out app/config/session.php
to change needed values.
Session Lifecycle
The session gets started and saved by the session middleware whereby interacting with session data is available after.
Session Error Handling
You may add an error handler for handling exceptions caused by the session middleware.
use Tobento\App\Boot; use Tobento\App\Http\HttpErrorHandlersInterface; use Tobento\Service\Session\SessionStartException; use Tobento\Service\Session\SessionExpiredException; use Tobento\Service\Session\SessionValidationException; use Tobento\Service\Session\SessionSaveException; use Throwable; class HttpErrorHandlerBoot extends Boot { public const BOOT = [ // you may ensure the http boot. \Tobento\App\Http\Boot\Http::class, ]; public function boot() { $this->app->on(HttpErrorHandlersInterface::class, function(HttpErrorHandlersInterface $handlers) { $handlers->add(function(Throwable $t) { if ($t instanceof SessionStartException) { // You may do something if starting session fails. } elseif ($t instanceof SessionExpiredException) { // This is already handled by the session middleware, // so you might check it out. } elseif ($t instanceof SessionValidationException) { // You may do something if session validation fails. } elseif ($t instanceof SessionSaveException) { // You may do something if saving session fails. } return $t; })->priority(2000); // you might add a priority. }); } }
You may handle these exceptions with the Error Handler - Handle Other Exceptions instead.
Check out the Throwable Handlers to learn more about handlers in general.
Cookies Boot
The cookies boot does the following:
- implements cookie interfaces based on cookies config file
- adds middleware based on cookies config file
use Tobento\App\AppFactory; use Tobento\Service\Cookie\CookiesFactoryInterface; use Tobento\Service\Cookie\CookieFactoryInterface; use Tobento\Service\Cookie\CookieValuesFactoryInterface; use Tobento\Service\Cookie\CookiesProcessorInterface; // Create the app $app = (new AppFactory())->createApp(); // Adding boots $app->boot(\Tobento\App\Http\Boot\Cookies::class); $app->booting(); // The following interfaces are available after booting: $cookiesFactory = $app->get(CookiesFactoryInterface::class); $cookieFactory = $app->get(CookieFactoryInterface::class); $cookieValuesFactory = $app->get(CookieValuesFactoryInterface::class); $cookiesProcessor = $app->get(CookiesProcessorInterface::class); // Run the app $app->run();
Check out the Cookie Service to learn more it.
Cookies Config
Check out app/config/cookies.php
to change needed values.
Cookies Usage
Read and write cookies
use Tobento\App\AppFactory; use Tobento\Service\Cookie\CookieValuesInterface; use Tobento\Service\Cookie\CookiesInterface; use Psr\Http\Message\ServerRequestInterface; // Create the app $app = (new AppFactory())->createApp(); // Adding boots $app->boot(\Tobento\App\Http\Boot\Routing::class); $app->boot(\Tobento\App\Http\Boot\Cookies::class); $app->booting(); $app->route('GET', 'bar', function(ServerRequestInterface $request) { // read cookies: $cookieValues = $request->getAttribute(CookieValuesInterface::class); $value = $cookieValues->get('foo'); // or var_dump($request->getCookieParams()); // write cookies: $cookies = $request->getAttribute(CookiesInterface::class); $cookies->add('name', 'value'); return ['page' => 'bar']; }); // Run the app $app->run();
Check out the Cookie Values to learn more it.
Check out the Cookies to learn more it.
Cookies Encryption
First install the app-encryption bundle:
composer require tobento/app-encryption
Then, just boot the Encryption::class
if you want to encrypt and decrypt all cookies values. That's all.
// ... $app->boot(\Tobento\App\Encryption\Boot\Encryption::class); $app->boot(\Tobento\App\Http\Boot\Cookies::class); // ...
Whitelist cookie
To whitelist a cookie (disable encryption), use the CookiesProcessorInterface::class
after the booting:
use Tobento\Service\Cookie\CookiesProcessorInterface; $cookiesProcessor = $app->get(CookiesProcessorInterface::class); $cookiesProcessor->whitelistCookie(name: 'name'); // or $cookiesProcessor->whitelistCookie(name: 'name[foo]'); $cookiesProcessor->whitelistCookie(name: 'name[bar]');
Configuration
The encrypting and decrypting is done with the implemented CookiesProcessor::class
processed by the specified middleware in the app/config/cookies.php
file.
use Tobento\Service\Cookie; use Tobento\Service\Encryption; use Psr\Container\ContainerInterface; return [ 'middlewares' => [ Cookie\Middleware\Cookies::class, ], 'interfaces' => [ //... Cookie\CookiesProcessorInterface::class => Cookie\CookiesProcessor::class, // or you may use a specified encrypter only for cookies: Cookie\CookiesProcessorInterface::class => static function(ContainerInterface $c): Cookie\CookiesProcessorInterface { $encrypter = null; if ( $c->has(Encryption\EncryptersInterface::class) && $c->get(Encryption\EncryptersInterface::class)->has('cookies') ) { $encrypter = $c->get(Encryption\EncryptersInterface::class)->get('cookies'); } return new Cookie\CookiesProcessor( encrypter: $encrypter, whitelistedCookies: [], ); }, ], //... ];
You may check out the App Encryption to learn more about it.
Error Handler Boot
By default, the error handler will render exceptions in json or plain text format. If you want to render exception views to support html and xml formats check out the Render Exception Views section.
// ... $app->boot(\Tobento\App\Http\Boot\ErrorHandler::class); // ...
It handles the following exceptions as well as the Http Exceptions:
Http Exceptions
There are several HTTP exceptions you can throw from your controllers and middleware, which are handled by the default error handler:
Example:
use Tobento\App\Http\Exception\HttpException; use Tobento\App\Http\Exception\NotFoundException; class SomeController { public function index() { throw new HttpException(statusCode: 404); // or: throw new NotFoundException(); } }
Render Exception Views
In order to render exceptions in html or xml format, the ViewInterface::class
must be available within the app. You might install the App View bundle or just implement the ViewInterface::class
:
composer require tobento/service-view
use Tobento\Service\View; use Tobento\Service\Dir\Dirs; use Tobento\Service\Dir\Dir; // ... $app->set(View\ViewInterface::class, function() { return new View\View( new View\PhpRenderer( new Dirs( new Dir('home/private/views/'), ) ), new View\Data(), new View\Assets('home/public/src/', 'https://www.example.com/src/') ); }); // ...
It renders the following view if exist:
Handle Other Exceptions
You might handle other exceptions by just exending the error handler:
Messages will be translated if you have installed the App Translation using uses the *
as resource name. Check out the Translation Resources and Translation Files Resources to learn more about it.
use Tobento\App\Http\Boot\ErrorHandler; use Tobento\Service\Requester\RequesterInterface; use Tobento\Service\Responser\ResponserInterface; use Psr\Http\Message\ResponseInterface; use Throwable; class CustomErrorHandler extends ErrorHandler { public function handleThrowable(Throwable $t): Throwable|ResponseInterface { $requester = $this->app->get(RequesterInterface::class); if ($t instanceof SomeException) { return $requester->wantsJson() ? $this->renderJson(code: 404) : $this->renderView(code: 404); // or with custom message: return $requester->wantsJson() ? $this->renderJson(code: 404, message: 'Custom') : $this->renderView(code: 404, message: 'Custom'); // or with custom message and message parameters: return $requester->wantsJson() ? $this->renderJson(code: 404, message: 'Custom :value', parameters: [':value' => 'foo']) : $this->renderView(code: 404, message: 'Custom :value', parameters: [':value' => 'foo']); } // using the responser: if ($t instanceof SomeOtherException) { $responser = $this->app->get(ResponserInterface::class); return $responser->json( data: ['key' => 'value'], code: 200, ); } return parent::handleThrowable($t); } }
And boot your custom error handler instead of the default:
// ... $app->boot(CustomErrorHandler::class); // ...
Prioritize Error Handler
You may create an error handler and use the HANDLER_PRIORITY
constant to define a priority.
The default priority is 1500
, higher gets handled first.
use Tobento\App\Http\Boot\ErrorHandler; use Tobento\Service\Requester\RequesterInterface; use Tobento\Service\Responser\ResponserInterface; use Psr\Http\Message\ResponseInterface; use Throwable; class PrioritizedErrorHandler extends ErrorHandler { protected const HANDLER_PRIORITY = 5000; public function handleThrowable(Throwable $t): Throwable|ResponseInterface { $requester = $this->app->get(RequesterInterface::class); if ($t instanceof SomeException) { return $requester->wantsJson() ? $this->renderJson(code: 404) : $this->renderView(code: 404); } // using the responser: if ($t instanceof SomeOtherException) { $responser = $this->app->get(ResponserInterface::class); return $responser->json( data: ['key' => 'value'], code: 200, ); } // return throwable to let other handler handle it: return $t; } }
And boot your error handler:
// ... $app->boot(PrioritizedErrorHandler::class); $app->boot(DefaultErrorHandler::class); // you could boot it after the default, // it gets called first if priority is higher as default: $app->boot(PrioritizedErrorHandler::class); // ...