<?php
/**
 * api: php
 * title: Canonic Autoloader / shared.phar
 * description: Map-based autoloader across .php and .phar resources.
 * version: 0.3.1
 * depends: php:phar, php >= 5.3
 * category: cli
 * license: Public Domain
 *
 *
 * Self-contained autoloader which provides .php and .phar class
 * loading based on a map. Loader and map are also contained in a
 * phar.
 *
 * It's meant for /usr/share/php handling, but can also be utilized
 * for per-project autoloading. It's indifferent to PSR-0/PSR-4 or
 * custom directory schemes. Because of the filename map, identifier
 * names are handled in compliance to PHP semantics (unlike PSR-x);
 * and non-namespaced code works as well. Additionally it prepares
 * future rfc:function_autoloading support.
 *
 * Using the autoloader is as simple as:
 *
 *   include_once("shared.phar");
 *
 * Invoke it from the commandline to update the internal classmap:
 *
 *   php ./shared.phar
 *
 * Its phar can also be split up into separate autoload, map, update
 * include files.
 *
 */


//
if (!class_exists("Canonic_Autoloader")) {
    class Canonic_Autoloader extends ArrayObject {


        // Support for rfc:function_autoloading identifier typing.
        public $types = array(1=>"class", 2=>"function", 4=>"constant");

        // Base directory for relative paths or phar:// paths in map.
        public $basedir = "";
        public $basefn = "";
        
        // Try to rebuild map on missing identifiers.
        public $auto_update = 1;

        

        /**
         * Initialize map.
         *
         * @param  array   $map        Contains a type->identifier->filename map.
         * @param  string  $basefn     Location of stub (shared.phar), which classpath basedir (usually __DIR__) is derived from.
         *
         */
        public function __construct($map, $basefn) {
            $this->basedir = dirname($basefn);
            $this->basefn = $basefn;
            parent::__construct($map);
        }



        /**
         * Look to-be loaded classname/function up in idmap,
         * and load include script.
         *
         * @param  string  $name       Identifier to autoload.
         * @param  int     $type       Wether it's a class, function, const identifier.
         * @return void
         *
         */
        public function __invoke($name, $type="class") {

            // Normalize identifier lookup
            $name = strtolower($name);
            if (is_int($type)) {
                $type = $this->types[$type];
            }

            // Check for declaration
            if (isset($this[$type][$name]))
            {
                $path = $this[$type][$name];
            }

            // And load according include script
            if (isset($path))
            {
                return include_once($this->absolute_path($path, $this->basedir));
            }

            // Once more with feeling
            if ($this->auto_update) {
                $this->auto_update() and $this($name);
            }
        }

        
        
        /**
         * Turn relative paths and phar:// locations into absolute.
         *
         * @param  string  $path       Dirname/Phar path.
         * @param  int     $base       Base directory to prepend on relative paths.
         * @return string
         *
         */
        public function absolute_path($path, $base) {
            // it was absolute already
            if ($path[0] == "/") {
                return $path;
            }
            // phar://
            elseif (!strncmp($path, "phar://", 7)) {
                // is relative
                if ($path[7] != "/") {
                    $path = "phar://$base/" . substr($path, 7);
                }
            }
            // relative dirname
            else {
                $path = "$base/$path";
            }
        }



        /**
         * Automatically update classmap ($this) at runtime, for development setups.
         *
         * @return boolean
         *
         */        
        function auto_update() {
            $this->auto_update = 0;
            
            // Only succeeds if current fingerprint doesn't match
            if (is_writeable($this->basefn) and $map = self::update($this->basefn, $this["fp"])) {
                parent::__construct($map);
                return TRUE;
            }
        }




        /**
         * Unpackaged .phar handling.
         *
         * The purpose of the following static methods is to allow having separate
         * `autoload.php` and `autoload.map.php` and `autoload.update.php` scripts
         * reside in the same directory, instead of in the shared.phar/
         *
         * pharBase() does decide on current __FILE__ location if run within .phar
         * context, or as standalone include. The basedir and mapfile location are
         * setup accordingly.
         *
         */
         
        // File locations, either phar: entries or in ./ local dir.
        // Consider these fixated. In case of a global and local shared.phar, only either declaration of Canonic_Classmap can define them.
        const MAP_FILE = "autoload.map.php";
        const UPDATE_LIB = "autoload.update.php";


        /**
         * Create instance and register it.
         *
         */
        public static function hookup($basefn) {
            spl_autoload_register(
                new Canonic_Autoloader( include(self::pharBase($basefn) . self::MAP_FILE), $basefn )
            );
        }

     
        /**
         * Recreate classmap within the phar / Or as separate autoload.map.php file.
         *
         */
        public static function update($basefn, $fp=NULL) {

            // Load utils manually. Now that's too unfortunate, but sensible for clean .phar distribution and some resiliency.
            class_exists("Canonic_Classmap") or
            include_once(self::pharBase($basefn) . self::UPDATE_LIB);

            // Update from subdirectories.
            return Canonic_Classmap::update( self::pharBase($basefn) . self::MAP_FILE , array(dirname($basefn)), $fp );
        }

        
        /**
         * Base directory or phar:// root for instantiation __FILE__ (stub.php)
         *
         */
        public static function pharBase($basefn) {

            // depending on if __FILE__ was a .phar
            if (substr($basefn, -5) == ".phar") {
                return "phar://$basefn/";
            }
            // use its surrounding directory if plain .php file
            else {
                return dirname($basefn) . "/";
            }
        }


    }
}



// Register loader with phar-contained indentifier map.
Canonic_Autoloader::hookup(__FILE__);



// If autoloader.phar is invoked directly from php-cli, trigger update mode.
if (empty($_SERVER["GATEWAY_INTERFACE"]) and isset($_SERVER["argv"][0]) and (basename($_SERVER["argv"][0]) == basename(__FILE__))) {
    Canonic_Autoloader::update(__FILE__);
}



// Phar stub end
__HALT_COMPILER(); ?>