Lia.php

<?php

use Lia\Events;

class Lia {

    public string $cache_dir = __DIR__.'/../../cache/';

    /** array <string namespace, \Lia\App $app> */
    public array $apps;

    /** 
     * array<\Lia\Events $event_name, array $callable_list> 
     * callable_list is an array of callables.
     * Each event defines its own callback signature & is typically defined via an interface
     * Example: Lia\ResponseInterface has signatures for events emitted by Liaison during deliver()
     * Handling an event does not require the interface be implemented.
     *
     * For complex systems, I recommend using an interface.
     */
    public array $events;

    /** array<string $method_name, callable $callable> */
    public array $methods;

    /**
     * Whether ready() has been called or not. 
     * `deliver()` only calls `ready()` when $is_ready is false.
     */
    public bool $is_ready = false;


    /**
     * Add a global method to Liaison. Overwrites if it already exists.
     *
     * @param $method_name method name
     * @param $callable any callable
     */
    public function addMethod(string $method_name, callable|string $callable){
        $this->methods[$method_name] = $callable;
    }

    public function addApp(\Lia\AppInterface $app){
        $this->apps[$app->getNamespace()] = $app;
    }


    /** 
     * Load an addon via it's fully qualified name.
     *
     * @param $addon_fqn, a fully qualified name like 'lia:router'. namespace ('lia') and addon name ('router') MUST be separated by a colon (':').
     * @return \Lia\AddonInterface an addon 
     */
    public function addon(string $addon_fqn): \Lia\AddonInterface {
        $pos = strpos($addon_fqn, ":");
        if ($pos===false)throw new \Exception("Addon name '$addon_fqn' MUST contain a colon (':').");
        $app_namespace = substr($addon_fqn,0,$pos);
        $addon_name = substr($addon_fqn,$pos+1);
        if (!isset($this->apps[$app_namespace])){
            // @TODO add `$lia->app()` method and call that instead of error checking in addon()
            throw new \Exception("App '$app_namespace' is not set. Addon '$addon_fqn' cannot be loaded.");
        }
        $addon = $this->apps[$app_namespace]->getAddon($addon_name);
        if (!($addon instanceof Lia\AddonInterface)){
            throw new \Exception("Addon '$addon_fqn' MUST implement 'Lia\\AddonInterface'");
        }
        return $addon;
    }

    /** Emit an event 
     *
     * @param $event_name the string event name
     * @param ...$args any args to pass to registered callables
     *
     * @return array<int index, mixed value> of responses from each execution of the vent
     */
    public function emit(string $event_name, ...$args): array {
        $responses = [];
        foreach ($this->events[$event_name]??[] as $callable){
            $responses[] = $callable(...$args);
        }

        return $responses;
    }

    public function hook(string $event_name, callable|array $callable){
        $this->events[$event_name][] = $callable;
    }

    /** ready up all apps */
    public function ready(){
        foreach ($this->apps as $app){
            $app->onLiaisonReady($this);
        }

        $this->emit(Events::Ready->value, $this);

        $this->is_ready = true;
    }

    /** 
     * Call onFinish() on all apps, then emit Events::Finish event.
     * Does NOT stop execution. Use terminate() to stop execution or `exit` on your own
     *
     */
    public function finish(){
        foreach ($this->apps as $app){
            $app->onFinish($this);
        }
        $this->emit(Events::Finish->value, $this);
    }

    /** 
     * Call onTerminate() on all apps, then emit Events::terminate
     * `exit`s.
     */
    public function terminate(){
        foreach ($this->apps as $app){
            $app->onTerminate($this);
        }

        $this->emit(Events::terminate->value, $this);

        exit;
    }

    /**
     * 
     */
    public function deliver(\Lia\Http\Request $request = new \Lia\Http\Request(), ?\Lia\Http\Response $response = null){
        $response = is_null($response) ? new \Lia\Http\Response($request) : $response; 
        if (!$this->is_ready){
            $this->ready();
        }

        foreach ($this->apps as $app){
            $app->onStartRequest($request,$response);
        }

        $this->emit(Events::StartRequest->value, $request, $response);

        $event_responses = $this->emit(Events::GetHttpRoutes->value, $request, $response);
        $routes = array_merge(...$event_responses);

        if (count($routes)==0){
            $this->emit(Events::NoHttpRoutes->value, $request, $response);
        } else if (count($routes)>1){
            $this->emit(Events::MultipleHttpRoutes->value, $request, $response, $routes);
        } else {
            $route = $routes[0];
            $this->emit(Events::SingleHttpRoute->value, $request, $response, $route);
        }

        $this->emit(Events::ApplyTheme->value, $request, $response, $routes);

        $response->send();

        $this->emit(Events::EndRequest->value, $request, $response, $routes);

        $this->finish();
    }

    /**
     * 
     */
    public function execute(\Lia\CliExecution $cli){
        $this->ready();

        foreach ($this->apps as $app){
            $app->onCliReady($this);
        }

        $this->emit(Events::CliReady->value, $this);

        // @TODO actually execute the cli responder
        // @TODO maybe onCliFinished() # yes, probably

        $this->finish();
    }

    /**
     * Throw an exception & report error to user.
     */
    public function throw(\Exception $e, string $user_message = ""){
        // @TODO log and/or print message
        throw $e;
    }

}