tobento/app-http


README

Http, routing, middleware and session support for the app.

Table of Contents

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:

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

Class Description
Tobento\Service\Responser\Middleware\Responser::class Adds the responser to the request attributes.
Tobento\Service\Responser\Middleware\ResponserMergeInput::class Merges the responser input with the request input.

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 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 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');
    }
}

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:

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.

Area Boot

The area boot may be used to create complex admin areas and other applications areas. The boots within the area running in its own application.

Create And Boot Area

First, create your area boot by extending the AreaBoot::class.

use Tobento\App\Http\AreaBoot;

class BackendBoot extends AreaBoot
{
    public const INFO = [
        'boot' => [
            'Backend Area',
        ],
    ];
    
    // Specify your area boots:
    protected const AREA_BOOT = [
        \Tobento\App\Boot\App::class,
        \Tobento\App\Http\Boot\Middleware::class,
        \Tobento\App\Http\Boot\Routing::class,
    ];
    
    protected const AREA_KEY = 'backend';
    
    protected const AREA_SLUG = 'private';
    
    // You may set a domain for the routing e.g. api.example.com
    // In addition, you may the slug to an empty string,
    // otherwise it gets appended e.g. api.example.com/slug
    protected const AREA_DOMAIN = '';
    
    // You may set a migration to be installed on booting e.g Migration::class
    protected const MIGRATION = '';
}

Next, boot your area:

use Tobento\App\AppFactory;

// Create the app
$app = (new AppFactory())->createApp();

// Adding boots
$app->boot(BackendBoot::class);

// Run the app
$app->run();

You may also boot your area boot by another boot:

use Tobento\App\Boot;

class ShopBoot extends Boot
{
    public const INFO = [
        'boot' => [
            'Shop',
        ],
    ];

    public const BOOT = [
        BackendBoot::class,
        FrontendBoot::class,
    ];
    
    public function boot(BackendBoot $backend, FrontendBoot $frontend): void
    {
        $backend->addBoot(ShopBackend::class);
        $frontend->addBoot(ShopFrontend::class);
    }
}

Area Config

You may copy config/area.php to app/config/ directory and rename it to your specified AREA_KEY contstant. You may do so by using a migration.

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 Code Exception
404 Tobento\Service\Routing\RouteNotFoundException
403 Tobento\Service\Routing\InvalidSignatureException
403 Tobento\Service\Session\SessionValidationException
403 Tobento\Service\Form\InvalidTokenException
500 Any other not handled before

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:

View Description
exception/403.php Any specific error with the named code.
exception/403.xml.php Any specific error with the named code in xml format.
exception/error.php If specific does not exist.
exception/error.xml.php If specific does not exist in xml format.

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);
// ...

Credits