OldPackage.php

<?php

namespace Lia;

/**
 * Encaspulate multiple components, views, routes & other bits in a single directory as a package.
 *
 * @featured
 * @tag setup
 */
class OldPackage implements \LiaisonInterface\PackageLifeCycle {

    /**
     * The configured name of this package or uniqid() if not specified
     */
    public $name;
    /**
     * The root dir for the package
     * @featured
     */
    public $dir;
    /**
     * the liaison instance
     * @featured
     */
    public $lia;

    /**
     * All compos added. `['name'=>$compoObject, 'name2'=>$otherCompo]`
     * @featured
     */
    protected $compos = [];

    /**
     * Components implementing `LiaisonInterface\PackageLifeCycle`
     * @warning only initialized after first call to `lifecycle()`
     */
    protected $lifecycleCompos = [];

    /**
     * Configurations for the package
     * This implementation is going to change, but I don't know how that's gonna work yet. I just know I don't like it.
     */
    //@export_start(Usage.Package.DefaultConfigs)
    protected $config = [
        'namespace'=>'',
        'dir'=>
            [
                'component' => 'core',
                // 'public' => 'public',
                'autoload'=> 'class',
                'view' => 'view',
                'cache' => 'cache',
                'package_lifecycle' => 'package_lifecycle',
                'public'=>[  // @TODO rethink config structure. public maybe shouldn't be nested under dir? Idk....
                    'dir' => 'public',
                    'baseUrl'=> '',
                ]
            ],
        
        'route'=>
            [
                'force_trail_slash'=>true,
                'index_names'=>
                [
                    'index'
                ],
                'hidden_extensions'=>
                [
                    'php'
                ],
            ],
        'class'=>
            [
                'view' => '\\Lia\\Obj\\View'
            ],
        'views'=>
            [
                // 'conflict' => 'throw'
            ]
    ];
    //@export_end(Package.DefaultConfigs)

    /**
     * Initialize package.
     *
     * - Configs are loaded from a config file in the package. Any keys set in the $config array will replace values set in the config file
     * - Setup is only performed if dir is non-null
     *
     * @param \Liaison $lia a Liaison instance
     * @param string $dir the root directory for the package or null to skip setup. 
     * @param array $configs Configs to set. Pass ['name'=>'SomeName'], to later use `$lia->package('SomeName')` to retrieve it
     *
     * @featured
     * @note removed type from first param to support incremental backward-compatible changes
     */
    public function __construct( $lia, ?string $dir=null, array $configs = []){
        $this->dir = $dir; 
        $this->lia = $lia; 
        if ($dir!=null){
            $this->setup($configs, $dir);
        }
    }

    /**
     * Setup the package. 
     * @warning Can be called multiple times but shouldn't be.
     */
    public function setup(array $configs, ?string $dir = null){
        if ($dir!==null)$this->dir = $dir;
        if (!is_dir($this->dir)){
            $class = get_class($this);
            throw new \Lia\Exception("Package dir must be set via `new ${class}(\$lia, \$dir)` or ${class}->setup(\$configs, \$dir) call. '$dir' is not valid");
        }
        $lia = $this->lia;
        $this->setup_config_file($this->dir.'/config.json');
        $configs = \Liaison\Utility\DotNotation::nestedFromDotted($configs);
        $this->config = array_replace_recursive($this->config, $configs);

        $this->setup_components($this->dir('component'));
        $this->lifecycle('onComponentsLoaded');

        $this->setup_autoload($this->dir('autoload'));
        $this->setup_views(realpath($this->dir('view')),
                ['lia'=>$lia, 'package'=>$this]
        );
        $this->setup_public_routes($this->dir('public'));

        $this->lifecycle('onPrePackageReady');
        $this->lifecycle('onPackageReady');

        $name = $this->config['name'] ?? uniqid();
        if ($this->get('namespace')==null)$this->set('namespace',$name);
        $this->name = $name;

        if ($lia->hasApi('lia:package.add')){
            $lia->api('lia:package.add', $this, $this->get('namespace'));
        }
    }

    /** @override */
    public function onComponentsLoaded(){}
    /** @override */
    public function onPrePackageReady(){}
    /** @override */
    public function onPackageReady(){}

    /**
     * Execute the given lifecycle method on each component in this package
     * Execute same-named php file in the package's `package_lifecycle` dir
     * @param $name the name of the lifecycle method
     */
    protected function lifecycle($name){
        if ($this->lifecycleCompos==null){
            foreach ($this->compos as $c){
                if ($c instanceof \LiaisonInterface\PackageLifeCycle)
                    $this->lifecycleCompos[] = $c;
            }
        }
        $this->$name();
        foreach ($this->lifecycleCompos as $compo){
            $compo->$name();
        }

        //@todo test running hook files in lifecycle() package method
        if (is_dir($hookDir=$this->dir('package_lifecycle'))){
            if (is_file($hookFile=$hookDir.'/'.$name.'.php')){
                require($hookFile);
            }
        }
    }

//
// utility functions for processing/setup
//
    /**
     * Add a component to the package. 
     *
     * @param string $name name to use for fetching the component
     * @param mixed $component Generally a \Lia\Compo, but may be anything.
     * @tag setup
     */
    public function addComponent($name, $component){
        $name = strtolower($name);
        $this->compos[$name] = $component;
    }

    /**
     * Load Config file. Currently `$package->dir.'/config.json'`.
     * @param string $configFile the config file to load
     * @tag setup
     */
    protected function setup_config_file(string $configFile){
        if (file_exists($configFile)){
            $json = file_get_contents($configFile);
            $configs = json_decode($json,true);
            $configs = \Liaison\Utility\DotNotation::nestedFromDotted($configs);
            $merged = array_replace_recursive($this->config,$configs);
            $this->config = $merged;
        }
    }
    /**
     * Convert a relative file path into a pattern for the Router component
     *
     * @todo move file path => pattern conversion into router
     * @tag utility, routing
     */
    public function fileToPattern($relFile){
        $pattern = $relFile;
        $ext = pathinfo($relFile,PATHINFO_EXTENSION);
        $hidden = $this->config['route']['hidden_extensions'];
        if (in_array($ext,$hidden)){
            $pattern = substr($pattern,0,-(strlen($ext)+1));
            $ext = '';
        }

        $indexNames = $this->config['route']['index_names'];
        $base = basename($pattern);
        if (in_array($base,$indexNames)){
            $pattern = substr($pattern,0,-strlen($base));
        }

        if ($ext==''
            &&$this->config['route']['force_trail_slash'])$pattern .= '/';

        $pattern = str_replace(['///','//'], '/', $pattern);
        return $pattern;
    }


// get stuff/info from package
    /**
     * Set a config on the package (Does NOT propagate to Liaison)
     * @param $dotKey 
     * @featured
     */
    public function set($key, $value){
        $array = \Liaison\Utility\DotNotation::nestedFromPair($key,$value);
        $this->config = array_replace_recursive($this->config, $array);
    }
    
    /**
     * Get a config from the package (NOT from Liaison)
     * @featured
     */
    public function get($key){
        return \Liaison\Utility\DotNotation::getNestedValue($key, $this->config);
    }
    /**
     * Get the configured class for the given key.
     * Shorthand for `get('class.$key');`
     * @featured
     */
    public function class($forKey){
        return $this->config['class'][$forKey];
    }
    /**
     * Get the named component from the package
     *
     * @param $name usually the component's class name (without namespace)
     * @featured
     */
    public function compo($name){
        $name = strtolower($name);
        $compo = $this->compos[$name] ?? null;

        return $compo;

        if ($compo==null){
            $packageName = $this->get('name') ?? '--unnamed-package--';
            throw new \Lia\Exception("A compo with name '{$name}' does not exist for package {$packageName}");
        }
        return $compo;
    }
    /**
     * Get the path to a directory in this package, or the package root dir if `$forKey==null`
     *
     * @param string $dirName configured directory key/name.
     * @return string directory path
     * @featured
     */
    public function dir($forKey=null){
        if ($forKey==null)return $this->dir;
        $dir = $this->config['dir'][$forKey] ?? null;
        if ($dir===null)return null;
        if (is_array($dir))$dir = $dir['dir'];
        return $this->dir.'/'.$dir;
    }
    
    /**
     * Prepends base url to the given path & sets $getparams as querystring. Replaces all double shashes (`//`) with single (`/`)
     *
     * @param $getParams key=>value array or a query string
     * @return a url path (no domain)
     * @todo create a version of this method in the Router component
     * @featured
     */
    public function url($path, $getParams=null){
        $get = $getParams;
        $qs = '';
        if (is_array($get)){
            $qs = http_build_query($get);
        } else {
            $qs = ''.$get;
        }
        $qs = substr($qs,0,1)=='?' ? $qs : '?'.$qs;
        $base = $this->get('dir.public.baseUrl');
        $url = '/'.$base.'/'.$path;
        $url = str_replace(['////','///','//'], '/', $url);
        $url = $qs=='?' ? $url : $url.$qs;
        return $url;
    }

//
//load all the pieces of a package
//
    /**
     * Adds configured 'autoload' dir to the Autoloader component
     *
     * @param $dir the directory to setup for autoloading
     * @tag setup
     */
    public function setup_autoload($dir){
        if ($this->lia->hasApi('lia:autoload.addDir'))
            $this->lia->api('lia:autoload.addDir', $dir);
    }

    /**
     * Load all components from the configured 'component' dir
     *
     * @param $dir the directory components are in
     * @tag setup
     */
    protected function setup_components(string $dir){
        $classes = \Lia\Utility\ClassFinder::classesFromDir($dir);
        // var_dump($dir);
        // var_dump($classes);
        // exit;
        if (count($classes)==0)return;
        $components = [];
        foreach ($classes as $info){
            // if (!in_array('Lia\\iCore\\Compo',$info['interfaces']))continue;
            $className = $info['class'];
            $compo = new $className($this);
        }
    }

    /**
     * Add all files in configured 'public' dir to the Router component
     *
     * @param $dir the directory to scan for public routed files
     * @param $prefix the prefix for all urls
     * @tag setup
     */
    public function setup_public_routes($dir, $prefix=null){
        $prefix = $prefix ?? $this->config['dir']['public']['baseUrl'];
        $lia = $this->lia;
        $files = \Lia\Utility\Files::all($dir,$dir);
        foreach ($files as $relFile){
            $pattern = $this->fileToPattern($relFile);
            $pattern = $prefix.$pattern;
            $pattern=$this->lia->clean_url($pattern);
            $path = $dir.$relFile;
            $lia->api('lia:route.add', $pattern, $path, $this);
        }
    }

    /**
     * Add all views in configured 'view' dir to the View Component
     *
     * @param string $dir a view directory to setup
     * @param array $args a key=>value array of args to extract to all views
     * @todo add a way to configure $args for your package, without having to override the package class
     * @tag setup
     */
    public function setup_views(string $dir, array $args){
        $lia = $this->lia;
        $files = \Lia\Utility\Files::all($dir,$dir, '.php');
        //set view conflict mode
        $hasConfigApi = $lia->hasApi('lia:config.get');
        $oldConflictMode = $hasConfigApi ? $lia->api('lia:config.get', 'lia:view.conflictMode') : null;
        $conflictMode = $this->config['views']['conflict'] ?? $oldConflictMode ?? 'throw';
        if ($hasConfigApi)$lia->api('lia:view.setConflictMode', $conflictMode);
        foreach ($files as $f){
            //remove leading `/` and trailing `.php` in a pretty simple, dumb way
            $viewName = substr($f,1,-4);
            $viewName = $this->get('namespace').':'.$viewName;
            $class = $this->class('view');
            $dir = $dir;
            if ($hasConfigApi&&$viewName=='theme')$lia->api('lia:view.setConflictMode', 'overwrite');
            $lia->api('lia:view.add', $class, $dir, $viewName, $args);
            if ($hasConfigApi&&$viewName=='theme')$lia->api('lia:view.setConflictMode', $conflictMode);
        }
        $mth = array_map(function($v){if (!is_string($v)&&!is_null($v))return 'not-string';}, $lia->methods);
        // $mth = $lia->methods;
        $lia->api('lia:view.setConflictMode', $oldConflictMode);
        //@export_end(Usage.View.AddView)
    }
}