Lifecycle events

Lifecycle events help you influence on the bootstrapping process using event listeners .

Configs events

When the application finishes collecting configs from modules it triggers an Event passing a raw list of configs (a merged array) to its listeners:

<?php

    // sourced from: src/Application/Bootstrapper.php

    // src/Application/EventManager/ConfigEvent.php
    $setEvent = new ConfigEvent($configsArray); // a raw list of configs
    $eventManager->trigger(
        ConfigEvent::EVENT_SET_CONFIGS,
        $setEvent
    );

    // register processed configs in the `ConfigService`
    $configsService->setConfigs($setEvent->getData());

So it gives us a beautiful opportunity to change the final config list from any custom module. In the example below we will try to implement a listener which changes some of existing config value. So lets imagine we have a module’s config like:

<?php

    return [
        'test' => 'test_value'
    ];

Our target is to change the test config value with a different one. For that we need a listener class, lets say it would be the: Module/CustomModule/EventListener/Application/SetConfigChangerListener.php

<?php

    namespace Tiny\Skeleton\Module\CustomModule\EventListener\Application;

    use Tiny\Skeleton\Application\EventManager\ConfigEvent;

    class SetConfigChangerListener
    {
        /**
         * @param  ConfigEvent  $event
         */
        public function __invoke(ConfigEvent $event)
        {
            $configs = $event->getData();

            // change the the config value
            if (isset($configs['test'])) {
                $configs['test'] = 'new_test_value';
            }

            $event->setData($configs);
        }
    }

Now we only need to register the listener in the config file:

<?php

    // Module/CustomModule/config.php

    use Tiny\Skeleton\Application\EventManager;
    use Tiny\Skeleton\Module\CustomModule\EventListener;

    return [
        'listeners' => [
            // application
            [
                'event'    => EventManager\ConfigEvent::EVENT_SET_CONFIGS,
                'listener' => EventListener\Application\SetConfigChangerListener::class,
            ],
        ]
    ];

Route events

Every time when the application registers a new route (collected from modules configs) it triggers an Event passing an instance of Router\Route to its listeners:

<?php

    // sourced from: src/Application/Bootstrapper.php

    $route = new Router\Route(
        $request,
        $controller,
        $actionList,
        ($route['type'] ?? Router\Route::TYPE_LITERAL),
        ($route['request_params'] ?? []),
        ($route['spec'] ?? ''),
        $context
    );

    // src/Application/EventManager/RouteEvent.php
    $registerEvent = new RouteEvent($route);
    $eventManager->trigger(
        RouteEvent::EVENT_REGISTER_ROUTE,
        $registerEvent
    );

    // register the processed route
    $router->registerRoute($registerEvent->getData());

How can we use that? For instance there is an integration of CORS in the application which just adds the HTTP method OPTIONS to each route automatically. Lets check it closer: (Module/Base/EventListener/Application/RegisterRouteCorsListener.php):

<?php

    // sourced from: src/Module/Base/EventListener/Application/RegisterRouteCorsListener.php

    namespace Tiny\Skeleton\Module\Base\EventListener\Application;

    use Tiny\Skeleton\Application\EventManager\RouteEvent;
    use Tiny\Http\Request;
    use Tiny\Router\Route;

    class RegisterRouteCorsListener
    {

        /**
         * @var Request
         */
        private Request $request;

        /**
         * RegisterRouteCorsListener constructor.
         *
         * @param  Request  $request
         */
        public function __construct(Request $request)
        {
            $this->request = $request;
        }

        /**
         * @param  RouteEvent  $event
         */
        public function __invoke(RouteEvent $event)
        {
            // whenever we receive the 'OPTIONS' request from a browser we assign the 'OPTIONS' method to each route
            if ($this->request->isOptions()) {
                /** @var Route $route */
                $route = $event->getData();

                if (is_array($route->getActionList())) {
                    // modify the route
                    $route->setActionList(
                        array_merge(
                            $route->getActionList(), [
                                Request::METHOD_OPTIONS => 'index', // now we also support OPTIONS, and you don't need to define it manually
                            ]
                        )
                    );

                    $event->setData($route);
                }
            }
        }

    }

The listener is is registered in the config file:

<?php

    // sourced from: src/Module/Base/config/listeners.php

    use Tiny\Skeleton\Application\EventManager;
    use Tiny\Skeleton\Module\Base\EventListener;

    return [
        'listeners' => [
            // application
            [
                'event'    => EventManager\RouteEvent::EVENT_REGISTER_ROUTE,
                'listener' => EventListener\Application\RegisterRouteCorsListener::class,
            ],
        ]
    ];

Router events

On the router initialization step the router tries to find a matched route analyzing a request string and registered routes. There are three possible events triggered by the router init method:

  • RouteEvent::EVENT_BEFORE_MATCHING_ROUTE - triggers before start matching routes.
  • RouteEvent::EVENT_AFTER_MATCHING_ROUTE - triggers after a route is found.
  • RouteEvent::EVENT_ROUTE_EXCEPTION - triggers when a route cannot be found.

the full method looks like:

<?php

    // sourced from: src/Application/Bootstrapper.php

    try {
        // src/Application/EventManager/RouteEvent.php
        $beforeEvent = new RouteEvent();
        $eventManager->trigger(
            RouteEvent::EVENT_BEFORE_MATCHING_ROUTE,
            $beforeEvent
        );

        // return a modified route
        if ($beforeEvent->getData()) {
            return $beforeEvent->getData();
        }

        // find a matched route
        $route = $router->getMatchedRoute();

        $afterEvent = new RouteEvent($route);
        $eventManager->trigger(
            RouteEvent::EVENT_AFTER_MATCHING_ROUTE,
            $afterEvent
        );

        return $afterEvent->getData();
    } catch (Throwable $e) {
        $routeExceptionEvent = new RouteEvent(
            null, [
                'exception' => $e,
            ]
        );
        $eventManager->trigger(
            RouteEvent::EVENT_ROUTE_EXCEPTION,
            $routeExceptionEvent
        );

        // return a modified route
        if ($routeExceptionEvent->getData()) {
            return $routeExceptionEvent->getData();
        }

        throw $e;
    }

You can subscribe to any of those events and return a custom route which depends on you needs. But in our example we will register a listener for handling a 404 page (Not found) when the RouteEvent::EVENT_ROUTE_EXCEPTION is triggered.

So let’s create a new listener class in your module (suppose it’s a CustomModule):

<?php

namespace Tiny\Skeleton\Module\CustomModule\EventListener\Application;

use Tiny\Skeleton\Application\EventManager\RouteEvent;
use Tiny\Router\Route;
use Tiny\Skeleton\Module\CustomModule\Controller\NotFoundController;

class RouteExceptionNotRegisteredListener
{
    /**
     * @param  RouteEvent  $event
     */
    public function __invoke(RouteEvent $event)
    {
        // by default the 'NotFoundController' will be assigned for all non existing routes
        $route = new Route(
            '',
            NotFoundController::class,
            'index'
        );
        $route->setMatchedAction('index');

        // return our custom route
        $event->setData(
            $route
        );
    }

}

Now we need to register it in the configs:

<?php

    // Module/CustomModule/config.php

    use Tiny\Skeleton\Application\EventManager;
    use Tiny\Skeleton\Module\CustomModule\EventListener;

    return [
        'listeners' => [
            // application
            [
                'event'    => EventManager\RouteEvent::EVENT_ROUTE_EXCEPTION,
                'listener' => EventListener\Application\RouteExceptionNotRegisteredListener::class,
            ],
        ]
    ];

Controller events

When a matched route is found by the router it calls a related controller’s method to get a response which will be returned and displayed. There are three possible events triggered by the controller init method:

  • RouteEvent::EVENT_BEFORE_CALLING_CONTROLLER - triggers before execution a controller’s method.
  • RouteEvent::EVENT_AFTER_CALLING_CONTROLLER - triggers after the controller’s execution.
  • RouteEvent::EVENT_CONTROLLER_EXCEPTION - triggers when the execution gives exceptions.

the full method looks like:

<?php

    // sourced from: src/Application/Bootstrapper.php

    try {
        $beforeEvent = new ControllerEvent(
            null, [
                'route' => $route,
            ]
        );
        $eventManager->trigger(
            ControllerEvent::EVENT_BEFORE_CALLING_CONTROLLER,
            $beforeEvent
        );

        // return a modified response
        if ($beforeEvent->getData()) {
            return $beforeEvent->getData();
        }

        // call the controller's action
        $controller->{$route->getMatchedAction()}($response, $request);

        $afterEvent = new ControllerEvent(
            $response, [
                'route' => $route,
            ]
        );
        $eventManager->trigger(
            ControllerEvent::EVENT_AFTER_CALLING_CONTROLLER,
            $afterEvent
        );

        return $afterEvent->getData();
    } catch (Throwable $e) {
        $requestExceptionEvent = new ControllerEvent(
            null, [
                'exception' => $e,
                'route'     => $route,
            ]
        );
        $eventManager->trigger(
            ControllerEvent::EVENT_CONTROLLER_EXCEPTION,
            $requestExceptionEvent
        );

        // return a modified response
        if ($requestExceptionEvent->getData()) {
            return $requestExceptionEvent->getData();
        }

        throw $e;
    }

Again you may use any of those events to implement a custom logic. In example below we will try to implement a very simple listener which checks if a user is logged in before execution a controller’s method. And if it not the user will be redirected to a login page.

We need to create a new listener class in your module (suppose it’s a CustomModule):

<?php

namespace Tiny\Skeleton\Module\CustomModule\EventListener\Application;

use Tiny\Skeleton\Application\EventManager\RouteEvent;
use Tiny\Http;
use Tiny\Router\Route;
use AuthService;

class BeforeCallingControllerAuthGuardListener
{

    /**
     * @var Http\AbstractResponse
     */
    private Http\AbstractResponse $response;

    /**
     * @var AuthService
     */
    private AuthService $authService;

    /**
     * @var Http\ResponseHttpUtils
     */
    private Http\ResponseHttpUtils $httpUtils;

    /**
     * BeforeCallingControllerAuthGuardListener constructor.
     *
     * @param  Http\AbstractResponse   $response
     */
    public function __construct(
        Http\AbstractResponse $response,
        AuthService $authService,
        Http\ResponseHttpUtils $httpUtils
    ) {
        $this->response = $response;
        $this->authService = $authService;
        $this->httpUtils = $httpUtils;
    }

    /**
     * @param  ControllerEvent  $event
     */
    public function __invoke(ControllerEvent $event)
    {
        if (!$this->authService->isAuthenticated()) {
            // return empty response and send the location header
            $this->httpUtils->sendHeaders([
                'Location: http://www.example.com/login'
            ]);
            $event->setData($this->response);
        }
    }

}

As you can see in our demonstration we use dependency injections. To make it clear you need to read the chapter - Factories. Also don’t forget to register the listener in the configs:

<?php

    // Module/CustomModule/config.php

    use Tiny\Skeleton\Application\EventManager;
    use Tiny\Skeleton\Module\CustomModule\EventListener;

    return [
        'listeners' => [
            // application
            [
                'event'    => EventManager\ControllerEvent::EVENT_BEFORE_CALLING_CONTROLLER,
                'listener' => EventListener\Application\BeforeCallingControllerAuthGuardListener::class,
            ],
        ]
    ];

Response events

The final step in the Life Cycle events which triggers an Event passing an instance of the Response object received from a controller to its listeners.

<?php

    // sourced from: src/Application/Bootstrapper.php

    // src/Application/EventManager/ControllerEvent.php
    $beforeEvent = new ControllerEvent(
        $response, // a controller's response
        [
            'route' => $route
        ]
    );
    $eventManager->trigger(
        ControllerEvent::EVENT_BEFORE_DISPLAYING_RESPONSE,
        $beforeEvent
    );

    /** @var Http\AbstractResponse $response */
    $response = $beforeEvent->getData();
    $responseString = $response->getResponseForDisplaying();

    return null !== $responseString ? $responseString : '';

It’s a good place to inject something helpful in the Response. In example bellow we add a Google analytic code without touching html templates. This approach allows us to easily remove or modify the analytic code and we really don’t care what templates are used.

A new listener would be like: (suppose it’s a CustomModule):

<?php

namespace Tiny\Skeleton\Module\CustomModule\EventListener\Application;

use Tiny\Skeleton\Application\EventManager\ControllerEvent;
use Tiny\Router\Route;
use Tiny\Skeleton\Application\Bootstrapper;
use Tiny\Http\AbstractResponse;
use Tiny\View\View;

class BeforeDisplayingResponseGoogleAnalyticListener
{

    /**
     * @param  ControllerEvent  $event
     */
    public function __invoke(ControllerEvent $event)
    {
        /** @var Route $route */
        $route = $event->getParams()['route'];

        // we only need to inject content in `http` responses (all other like: `cli`, `http_api` should be skipped)
        if ($route->getContext() === Bootstrapper::ROUTE_CONTEXT_HTTP) {
            /** @var AbstractResponse $response */
            $response = $event->getData();
            $controllerResponse = $response->getResponse();

            if ($controllerResponse instanceof View) {
                $pageContent = $controllerResponse->__toString();

                // add the analytic code
                $pageContent .= '<you analytic code here>';

                // modify the response
                $response->setResponse($pageContent);
                $event->setData($response);
            }
        }
    }

}

And register the listener in the configs:

<?php

    // Module/CustomModule/config.php

    use Tiny\Skeleton\Application\EventManager;
    use Tiny\Skeleton\Module\CustomModule\EventListener;

    return [
        'listeners' => [
            // application
            [
                'event'    => EventManager\ControllerEvent::EVENT_BEFORE_DISPLAYING_RESPONSE,
                'listener' => EventListener\Application\BeforeDisplayingResponseGoogleAnalyticListener::class,
            ],
        ]
    ];