Liaison.php

<?php

class Liaison {

    /**
     * Used as a default for get() so an exception can be thrown
     */
    const NOT_SET = 'KDSFunql;kuDF^*&3124tupD*(FFgpib143^(F&b$!@#TADFSSbnFLJKSD:U893y2149hoj;andf*URn';

    /**
     * Create a new Liaison object
     *
     * @param array $options ['bare'=>true] to disable loading of the built-in package
     * @featured
     */
    public function __construct(array $options = []){
        $this->debug = $options['debug'] ?? false;


        $this->addApi('lia:config.get',[$this, 'get']);
        $this->addApiMethod('lia:config.get', 'get');

        $this->addApi('lia:config.set',[$this, 'set']);
        $this->addApiMethod('lia:config.set', 'set');

        $this->addApi('lia:config.default',[$this, 'default']);
        $this->addApiMethod('lia:config.default', 'default');

        $this->addApi('lia:config.append',[$this, 'append']);
        $this->addApiMethod('lia:config.append', 'append');

        if (($options['bare']??false))return;
        new \Lia\Package($this, dirname(__DIR__),['name'=>'lia']);
    }

    /**
     * All mediators
     * [ 'api.apiKey.handler' => $callable,
     *   'method.methodName' => $callable,
     *   'prefix.prefixName' => $callable,
     *   'submediate.method.methodName' => $callable
     * ]
     *
     */
    protected $mediators = [];

    /**
     * Global APIs.
     * Accessible by calling $lia->api('api.name', 'handler', ...$args);
     * [ 'api.name' => [ 'handler' => $callable, 'handler2' => $callable2]];
     */
    protected $api = [];

    /**
     * Global prefixes. 
     * Accessible by declaring public function prefixWhatever(){} in a component
     * ['prefix' => [0=>'api.name',1=>'handler'] , 'prefix2'...]
     *
     */
    protected $prefix = [];

    /**
     * Global methods executable by $lia->methodName()
     * ['methodName' => [0=>'api.name',1=>'handler'] , 'method2'...]
     *
     */
    protected $method = [];

    /**
     * Map of values for set/get/append
     */
    protected $valuesMap = [];

//
// Get & Set
//

    /**
     * set the default $value for the $key. This will never overwrite a previously set value.
     */
    public function default($key, $value){
        if (!\Lia\Utility\NS::has($this->valuesMap, $key)){
            $this->set($key,$value);
        }
    }
    
    /**
     * Get a stored value.
     *
     * @param $key the key to retrieve
     * @param $default any value to use if the key is not found.
     * @throws if $key is not available and $default is not passed
     */
    public function get($key, $default=self::NOT_SET){
        if ($key=='compo'){
            echo "\nget compo\n";
            var_dump($key);
            echo "\n";

            var_dump(array_keys($this->valuesMap['compo']));
            $key = array_keys($this->valuesMap['compo'])[1];
            var_dump(array_keys($this->valuesMap['compo'][$key]));
            exit;
            exit;
        }
        if (\Lia\Utility\NS::has($this->valuesMap, $key)){
            return \Lia\Utility\NS::get($this->valuesMap,$key);
        }
        if ($default==self::NOT_SET){
            throw new \Lia\Exception\Base("Config '{$key}' is not set.");
        } else {
            return $default;
        }
    }

    /**
     * Set a value to $key
     *
     * @param $key the key to set, generally in form of `namespace:some.key`. Namespace & suggested key syntax are optional
     * @param $value the value to set
     */
    public function set($key, $value){
        \Lia\Utility\NS::set($this->valuesMap, $key, $value);
    }

    /**
     * Append a value to an indexed array. Create the array if not $key not set
     *
     * @todo Write a test for config::append()
     */
    public function append($key, $value){
        $existing = \Lia\Utility\NS::get($this->valuesMap, $key);
        if ($existing===null)$existing = [];
        if (!is_array($existing)){
            throw new \Lia\Exception\Base("Cannot append to '$key'. It is already set & is not an array");
        }

        $existing[] = $value;
        \Lia\Utility\NS::set($this->valuesMap, $key, $existing);
    }

    /**
     * Append a value to an indexed array. Create the array if not $key not set
     *
     * @todo Write a test for config::append()
     */
    public function arset($nsKey, $arrayKey, $value){
        echo "\nBEGIN arset\n";
        $existing = \Lia\Utility\NS::get($this->valuesMap, $nsKey);
        if ($existing===null)$existing = [];
        if (!is_array($existing)){
            throw new \Lia\Exception\Base("Cannot append to '$nsKey'. It is already set & is not an array");
        }

        var_dump($nsKey);
        var_dump($arrayKey);
        echo "\n\n";
        // var_dump(is_object($value));
        $existing[$arrayKey] = $value;
        \Lia\Utility\NS::set($this->valuesMap, $nsKey, $existing);
        if ($nsKey=='lia:package'){
            // echo "\n\nIS LIA PACKAGE\n";
            // var_dump($value);
            // echo "\n\n\n";
//
            // echo "\n--------------------------------\n";
//
            // var_dump($this->valuesMap['package']);
            // echo "\n--------------------------------\n";
        }
    }

    public function arget($nsKey, $arrayKey, $default=self::NOT_SET){
        $array = $this->get($nsKey,$default);
        if ($nsKey=='package'){
            // echo "\n/////////////////////////////////////\n";
            // var_dump(array_keys($this->valuesMap));
            // var_dump($this->valuesMap['package']);
            // echo "\n\n\n";
            // echo "zeep\n";
            // var_dump($nsKey);
            // var_dump($arrayKey);
            // var_dump($array);
            // exit;
        }
        if (!isset($array[$arrayKey])&&$default===self::NOT_SET){
            throw new \Lia\Exception\Base("Cannot get '$arrayKey' from '$nsKey'. '$nsKey' found, but '$arrayKey' not set.");
        }
        return $array[$arrayKey];
    }

    // public function setter($key, $globalMethodName, $setFunction = 'set'){
        // $this->addMethod($globalMethodName,
            // function(...$args) use ($key, $setFunction){
                // $this->$setFunction($key, ...$args);
            // }
        // );
    // }

//
// Short & Sweet
//
    /** Get the full array of global apis */
    public function getApiList(){
        return $this->api;
    }
    /** Get the full array of global prefixes */
    public function getApiPrefixes(){
        return $this->prefix;
    }
    /** Get the full list of global methods */
    public function getApiMethods(){
        return $this->method;
    }

//
//convenience methods for web developers
//
    /**
     * Add a global method to Liaison.
     * 
     * @return string The uniqid() apikey the method points to. Handler is 'method'
     */
    public function addMethod(string $globalMethodName, callable $callable){
        $key = uniqid();
        $this->addApi($key, $callable);
        $this->addApiMethod($key, $globalMethodName);
        return $key;
    }

    /**
     * Add a global prefix to Liaison
     *
     * @return string The uniqid() apiKey the prefix points to. Handler is 'prefix'
     */
    public function addPrefix($prefixName, $callable){
        $key = uniqid();
        $this->addApi($key, $callable);
        $this->addApiPrefix($key,$prefixName);
        return $key;
    }

//
//adding to api
//
    /**
     * Run a conflict through a registered mediator.
     * See addMediator() for more info
     * 
     * @return the winner, as determined by the registered callable
     * @throw \Lia\Exception\Base if a mediator is not set for the given key
     */
    protected function mediate(string $mediatorKey, $old, $new){
        if($old==null)return $new;
        //@TODO consider adding partial wildcard mediators, like api.* & prefix.*
            // This could even be api.apiKey.* if I want the same mediator for each handler
            // I'm concerned about performance, so I'm reluctant to add this feature.
            // But, It would make sense that mediation is an advanced feature that comes with some reasonable downsides
        $mediator = $this->mediators[$mediatorKey] ?? $this->mediators['*'] ?? null;
        if ($mediator==null){
            throw new \Lia\Exception\Base("No mediator found to handle '{$mediatorKey}', but a duplicate was added.");
        }
        $winner = $mediator($mediatorKey, $old, $new);
        return $winner;
    }

    /**
     * Add a mediator to handle conflicts. 
     *
     * Built-in mediatorPrefixes are: submediate, api, prefix, and method
     *
     * @param $mediatorKey `mediationPrefix:namespace:api.name`
     * @param $callable function($mediatorKey, $oldThing, $newThing) <- usually new&old thing will be callables
     * @todo How to handle no namespace on the api? 
     */
    public function addMediator(string $mediatorKey, callable $callable){
        if (isset($this->mediators[$mediatorKey])){
            $callable = $this->mediate('submediate:'.$mediatorKey, $this->mediators[$mediatorKey], $callable);
        }
        $this->mediators[$mediatorKey] = $callable;
    }

    /**
     * Add an API
     *
     * @param $api `namespace:api.name`. Namespace is optional.
     * @param $callable to be called when the named api is invoked
     * @featured
     */
    public function addApi(string $api, callable $callable){
        if (\Lia\Utility\NS::has($this->api,$api)){
            $callable = $this->mediate('api:'.$api, 
                \Lia\Utility\NS::get($this->api, $api),
                $callable);
        }
        \Lia\Utility\NS::set($this->api, $api, $callable);
    }

    /**
     * Add a global method, pointing to an api
     *
     * @param $api api to be invoked. `namespace:api.name`. namespace optional
     * @param $methodName `$lia->$methodName()` will invoke the $api
     * @featured
     */
    public function addApiMethod(string $api, string $methodName){
        if (isset($this->method[$methodName])){
            $pointer = $this->mediate('method:'.$methodName, $this->method[$methodName], $api);
        } else {
            $pointer = $api;
        }
        $this->method[$methodName] = $pointer;
    }

    /**
     * Add a global prefix to point to a particular api 
     *
     * @param $api api to pass the prefixed method to. `namespace:api.name`. namespace optional
     * @param $prefix declare `prefixWhatever` or `prefix_Whatever` & the method will be passed to the $api
     *
     * @featured
     */
    public function addApiPrefix(string $api, string $prefix){
        if (isset($this->prefix[$prefix])){
            $pointer = $this->mediate('prefix:'.$prefix, $this->prefix[$prefix], $api);
        } else {
            $pointer = $api;
        }
        $this->prefix[$prefix] = $pointer;
    }

//
// removing/modifying the api
//
    /**
     * Remove the given api.
     * Does not unset associated prefixes/methods.
     *
     * @param $api the api to remove
     */
    public function removeApi($api){
        return \Lia\Utility\NS::unset($this->api,$api);
    }
    /**
     * remove the given method name (doesn't affect the api)
     */
    public function removeMethod($methodName){
        unset($this->method[$methodName]);
    }
    /**
     * remove the given prefix name (doesn't affect the api)
     */
    public function removePrefix($prefixName){
        unset($this->prefix[$prefixName]);
    }

//
//calling the api
//
    /** Check if the given API exists 
     * @featured
     */
    public function hasApi(string $api){
        return \Lia\Utility\NS::has($this->api, $api);
    }
    /**
     * Check if the apiNamespace exists
     * @param $apiKeyOrNamespace a namespace like `namespace` or an api key like `namespace:some.key`
     */
    public function hasApiNamespace(string $apiOrNamespace){
        return \Lia\Utility\NS::hasNamespace($this->api, $apiOrNamespace);    
    }
    /**
     * Call the given api
     *
     * @param $api `namespace:api.name`. namespace optional.
     * @param $args whatever the api wants
     * @return whatever the api returns
     * @throw Lia\Exception\Base if the api handler is not set
     * @featured
     */
    public function api(string $api, ...$args){
        $apiCallable = \Lia\Utility\NS::get($this->api,$api,$ns);
        if ($apiCallable==null){
            throw new \Lia\Exception\Base("Api '$api' doesn't exist.");
        }
        return $apiCallable(...$args);
    }

    /**
     * Call the global method
     * 
     * @throw Lia\Exception\Base if the global method is not set
     */
    public function __call($methodName, $args){
        if (!isset($this->method[$methodName])){
            throw new \Lia\Exception\Base("Method '{$methodName}' has not been set on liaison.");
        }
        $m = $this->method[$methodName];
        return $this->api($m, ...$args);
    }

    /**
     * This is entirely for the sake of testing the Base Exception class. It should clearly NEVER be called in production
     */
    public function testThrowBaseExceptionClass($level=0){
        if ($level<4){
            $this->testThrowBaseExceptionClass(++$level);
            return;
        }
        throw new \Lia\Exception\Base("Testing the base exception class with nested calling.");
    }
}