Map-based autoloader across php and phar resources

⌈⌋ branch:  Canonic Autoloader


Artifact Content

  • File stub.php — part of check-in [18e74838b1] at 2015-01-22 21:58:36 on branch trunk — Build in trigger script for DPKG and RPM (untested). (user: mario

Artifact 0c06015feca4b117cda5a440b4bf0c7c2f4afe4b:


<?php
/**
 * api: php
 * id: shared
 * title: Canonic Autoloader / shared.phar
 * description: Map-based autoloader across .php and .phar resources.
 * version: 0.3.4-31
 * depends: php:phar | php (>= 5.3)
 * category: cli
 * license: Public Domain
 * url: https://fossil.include-once.org/canonic_autoloader/
 * author: mario#include-once:org
 * architecture: all
 * main: .phar/stub.php
 * pack: stub.php=, autoload.update.php, autoload.map.php
 *
 *
 * Self-contained autoloader which provides .php and .phar class
 * loading based on a map.
 *
 * Usable for per-project classpath and /usr/share/php handling.
 * 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 still allows
 * non-namespaced code to work. 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 map:
 *
 *   php ./shared.phar
 *
 * For development (non-deployment) setups, configure automatic
 * updates per environment setting, e.g. via .htaccess:
 *
 *   SetEnv AUTOLOADER_UPDATE 1
 *
 * The phar could also be split up into separate autoload, map
 * and update includes.
 *
 */



// Declared just once for relocatable instantation
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=>"const");

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

        

        /**
         * 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);
            $this->auto_update |= !empty($_SERVER["AUTOLOADER_UPDATE"])
                                  and is_writeable($this->basefn); // apt for the default case of using shared.phar
        }



        /**
         * 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))
            {
                // Convert relative paths
                if ($path[0] != "/") {

                    // Relative directory, if no phar: prefix
                    if (strncmp($path, "phar://", 7)) {
                        $path = "$this->basedir/$path";
                    }

                    // If non-absolute phar:// url, inject basedir
                    elseif ($path[7] !== "/") {
                        $path = "phar://$this->basedir/" . substr($path, 7);
                    }
                }

                return include_once($path);
            }

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

        

        /**
         * 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 (isset($this["fp"]) 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(
                $i = new Canonic_Autoloader( include(self::pharBase($basefn) . self::MAP_FILE), $basefn )
            );
            self::$instances[] = $i;
        }
        
        // keep a reference
        static $instances = array();

     
        /**
         * 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 relocatability.
            class_exists("Canonic_Classmap", false) 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) . "/";
            }
        }


        /**
         * Loads meta data `map` from fpm/xpm-generated .phar/composer packages.
         * The map[] has the same structure as shared.phars` itself.
         *
         */
        public static function addPhar($phar_fn) {

            // open and scan for metadata
            if ($main = reset(self::$instances) and $p = new Phar($phar_fn, 0, NULL)
            and $p->hasMetadata() and $meta = $p->getMetadata() and !empty($meta["map"]))
            {
                // add to instance classmap by prepending phar location and scheme
                foreach ($main->types as $type) {
                    foreach (array_change_key_case($meta["map"][$type]) as $id=>$fn) {
                        $main[$type][$id] = "phar://$phar_fn/$fn";
                    }
                }
            }
        }

    }
}



// 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(); ?>