<?php
/**
* Web Application Component Toolkit
*
* @link http://www.phpwact.org/
*
* @author Wact Development Team
* @link http://www.phpwact.org/team
*
* @copyright Copyright 2006, Jeff Moore
* @license http://opensource.org/licenses/mit-license.php MIT
*/


/**
 * A dependency injection capable registry
 */
class Wact_Config_Registry {

    /**
     * A list of singletons
     * @var array
     */
    protected $singletons = array();

    /**
     * A list of factories
     * @var array
     */
    protected $factories = array();
    
    /**
     * A fallback config registry
     * @var array
     */
    protected $fallback;
    
    /**
     * @param Wact_Config_Registry $fallback
     */
    function __construct($fallback = NULL) {
        $this->fallback = $fallback;
        
        // Allow ourselves to be injected
        $this->singletons[__CLASS__] = $this;
    }
    
    /**
     * Create a new configuration context to work with based on the current one
     * @return Wact_Config_Registry
     */
    public function newContext() {
        return new self($this);
    }

    /**
     * Define a factory
     *
     * @param string $type The type to define the factory for
     * @return Wact_Class_Registry The factory just defined
     */
    public function define($type)
    {
        if (!isset($this->factories[$type])) {
            $this->factories[$type] = new Wact_Config_Registry_Factory($type);
        }
        return $this->factories[$type];
    }

    /**
     * If a defintion for a type has not been found, this function will
     * take a stab at automatically defining one.
     * Override this in a subclass to prevent autoDefines, or log auto defines.
     * @param string $type
     * @return Wact_Class_Registry The factory just defined
     */
    protected function autoDefine($type) {
        return $this->define($type)->asClass($type);
    }
    
    /**
     * create an instance of a class using a particular Registry as context
     *
     * @param Wact_Config_Registry $context
     * @param string $type
     * @param array $parameters
     * @return mixed
     */
    protected function contextualCreate($context, $class, $parameters = array()) {
        if (isset($this->factories[$class])) {
            return $this->factories[$class]->createInstance($context, $parameters);
        }
        if (isset($this->fallback)) {
            return $this->fallback->contextualCreate($context, $class, $parameters);
        }

        // default factories accumulate only on registries without fallbacks
        return $this->autoDefine($class)->createInstance($context, $parameters);
    }
    
    /**
     * create an instance of a class
     *
     * @param string $class
     * @param array $parameters
     * @return mixed
     */
    public function create($type, $parameters = array()) {
        if (method_exists($this, 'create' . $type)) {
            return call_user_func_array(array($this, 'create' . $type), $parameters);
        }
        return $this->contextualCreate($this, $type, $parameters);
    }
    
    /**
     * Does the named singleton exist in this registry or in a fallback?
     * @param string $type
     * @return boolean 
     */
    public function singletonExists($type) {
        if (isset($this->singletons[$type])) {
            return TRUE;
        }
        if (isset($this->fallback)) {
            return $this->fallback->singletonExists($type);
        }
        return FALSE;
    }

    /**
     * Does the named type have a factory in this registry or in a fallback?
     * @param string $type
     * @return boolean 
     */
    public function factoryExists($type) {
        if (isset($this->factories[$type])) {
            return TRUE;
        }
        if (isset($this->fallback)) {
            return $this->fallback->factoryExists($type);
        }
        return FALSE;
    }

    /**
     * Is there a definition for this type in this registry or in a fallback?
     * @param string $type
     * @return boolean 
     */
    public function isDefined($type) {
        if (isset($this->factories[$type]) || isset($this->singletons[$type])) {
            return TRUE;
        }
        if (isset($this->fallback)) {
            return $this->fallback->isDefined($type);
        }
        return FALSE;
    }

    /**
     * Request for a singleton instance
     * If the requested singleton does not exist, an effort will be made to
     * create it.
     * The created singleton will be added to the current registry.
     * @param string $type
     * @params array $params Constructor parameters; used only if the singleton doesn't already exist
     * @return mixed
     */
    public function get($type, $params = array()) {
        if (!isset($this->singletons[$type])) {
            if (isset($this->fallback) && $this->fallback->singletonExists($type)) {
                return $this->fallback->get($type);
            }
            $this->singletons[$type] = $this->create($type, $params);
        }
        return $this->singletons[$type];
    }

    /**
     * Request for a singleton instance
     * If the requested singleton does not exist, a default value will be
     * returned instead.
     * @param string $type
     * @params mixed $default
     * @return mixed
     */
    public function ifsetor($type, $default = NULL) {
        if (isset($this->singletons[$type])) {
            return $this->singletons[$type];
        } else {
            if (isset($this->fallback) && $this->fallback->singletonExists($type)) {
                return $this->fallback->ifsetor($type);
            } else {
                return $default;
            }
        }
    }
    
    /**
     * Allows a specific singleton to be specified.
     * @param string $type
     * @param mixed $value
     */
    public function set($type, $value) {
        if (isset($this->singletons[$type])) {
            throw new Wact_Config_Exception('Cannot replace already instantiated singleton of type "{type}"', $type);
        }
        $this->singletons[$type] = $value;
    }
    
    /**
     * Any public property read becomes a request for a singleton instance
     * If the requested singleton does not exist, an effort will be made to
     * create it.
     * The created singleton will be added to the current registry.
     * @param string $type
     * @return mixed
     */
    public function __get($type) {
        return $this->get($type);
    }
    
    /**
     * Allows a specific singleton to be specified.
     * @param string $type
     * @param mixed $value
     */
    public function __set($type, $value) {
        $this->set($type, $value);
    }

    /**
     * @param string $type
     */
    public function __isset($type) {
        throw new Wact_Config_Exception('isset operation is undefined for type "{type}"', $type);
    }
    
    /**
     * Removing already established singltons is not allowed
     * @param string $type
     */
    public function __unset($type) {
        throw new Wact_Config_Exception('Cannot replace already instantiated singleton of type "{type}"', $type);
    } 

    /**
     * Any public method invocation prefixed by create becomes a request to
     * create a new instance of the class corresponding to the method name.
     * @param string $method
     * @param array $args
     */
    public function __call($method, $args) {
        if (stripos($method, 'create') === 0) {
            $type = substr($method, 6);
            return $this->create($type, $args);
        }
    }
    
}

?>