Location: PHPKode > scripts > Klass > Klass.php
<?php
/**
 * Klass
 * Copyright (c) 2008 Maxime Bouroumeau-Fuseau
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * @author Maxime Bouroumeau-Fuseau
 * @copyright 2008 (c) Maxime Bouroumeau-Fuseau
 * @license http://www.opensource.org/licenses/mit-license.php
 * @link http://www.pimpmycode.fr
 */

/**
 * Provides dependency injection and automatic accessors
 * 
 * @copyright 2008 (c) Maxime Bouroumeau-Fuseau
 * @license http://www.opensource.org/licenses/mit-license.php
 */
class Klass
{
	/**
	 * Objects registered so they can be injected in other classes
	 *
	 * @var array
	 */
	protected static $_registeredDependencies = array();
	
	/**
	 * Information about accessors to avoid computing them at each call
	 *
	 * @var array
	 */
	protected $__accessorsInfo = array();
	
	/**
	 * The ReflectionClass object of the current object
	 *
	 * @var ReflectionClass
	 */
	protected $__reflectedClass;
	
	/**
	 * Creates and injects an instance of a class.
	 *
	 * @see Klass::apply()
	 * @param string $className
	 * @param array $constructorParams OPTIONAL Constructor parameters
	 * @return object
	 */
	public static function createArgs($className, $constructorParams = null)
	{
		if (!class_exists($className)) {
			throw new Exception('Class ' . $className . ' does not exist');
		}
		$class = new ReflectionClass($className);
		
		// creates the class instance
		if ($constructorParams !== null) {
			$instance = $class->newInstanceArgs($constructorParams);
		} else {
			$instance = $class->newInstance();
		}
		
		self::apply($instance, $class);
		return $instance;
	}
	
	/**
	 * Creates and injects an instance of a class. The created
	 * class will automatically be registered.
	 *
	 * @see Klass::createArgs()
	 * @param string $className
	 * @param mixed ... Any number of parameter - will be used as constructor parameters
	 * @return object
	 */
	public static function create($className)
	{
		$arguments = func_get_args();
		array_shift($arguments);
		$constructorParams = count($arguments) > 0 ? $arguments : null;
		return self::createArgs($className, $constructorParams);
	}
	
	/**
	 * Injects an instance of a class with all registered objects.
	 * 
	 * Properties to inject should have an @Inject tag inside their doc comment.
	 * The type of the dependency is gathered from the @var tag if present.
	 * The type can also be specified in parentheses after the @Inject tag: @Inject(type)
	 * In all cases, the type must be specified
	 *
	 * @param object $object
	 * @param ReflectionClass $reflectedClass OPTIONAL The ReflectionClass object associated to the object (to avoid recreating it)
	 */
	public static function inject($object, ReflectionClass $reflectedClass = null)
	{
		if ($reflectedClass === null) {
			if ($object instanceof Klass) {
				$reflectedClass = $object->__getClass();
			} else {
				$reflectedClass = new ReflectionClass(get_class($object));
			}
		}
		
		foreach ($reflectedClass->getProperties() as $property) {
			// chechks if injection is enabled of this property
			if (!preg_match('/@Inject(\((.+)\)|)/', $property->getDocComment(), $injectTag)) {
				continue;
			}
			
			if (empty($injectTag[1])) {
				// no type specified, checking of the @var tag in the doc comment
				if (!preg_match('/@var (.+)\s*$/m', $property->getDocComment(), $varTag)) {
					throw new Exception('Wrong injection definition, missing type in ' 
						. $reflectedClass->getName() . '::' . $property->getName());
				}
				$type = $varTag[1];
			} else {
				$type = $injectTag[2];
			}
			
			// no matching object found for injection
			if (!isset(self::$_registeredDependencies[$type])) {
				continue;
			}
			
			if ($property->isPublic()) {
				// public property
				$object->{$property->getName()} = self::$_registeredDependencies[$type];
				continue;
			}
			
			// private or protected property, checkin if there's a setter method
			$setterMethodName = 'set' . ucfirst(ltrim($property->getName(), '_'));
			if ($object instanceof Klass || $reflectedClass->hasMethod($setterMethodName)) {
				// using the setter
				call_user_func(array($object, $setterMethodName), self::$_registeredDependencies[$type]);
				return;
			}
			
			throw new Exception('Cannot inject dependency in ' . $reflectedClass->getName() 
				. '::' . $property->getName() . ' because the property is not accessible');
		}
	}
	
	/**
	 * Registers an object so it can be injected in other classes.
	 * If an object is already registered for the same type, the new object will 
	 * replace the old one.
	 *
	 * @param mixed $object
	 * @param string|array $types OPTIONAL The types under which the object should be registred
	 */
	public static function registerDependency($object, $types = null)
	{
		if ($types === null) {
			if (!is_object($object)) {
				throw new Exception('When $object is not an object, a type must be specified');
			}
			$types = array(get_class($object));
		}
		
		foreach ($types as $type) {
			self::$_registeredDependencies[$type] = $object;
		}
	}
	
	/**
	 * Injects dependencies and Registers the class if the @Dependency tag is 
	 * found in the class doc comment.
	 * 
	 * It is also possible to register the object for a type different than its type. 
	 * To do so, specify the type in parentheses after the @Dependency tag: @Dependency(type)
	 *
	 * @param object $object
	 * @param ReflectionClass $reflectedClass OPTIONAL The ReflectionClass object associated to the object (to avoid recreating it)
	 */
	public static function apply($object, ReflectionClass $reflectedClass = null)
	{
		if ($reflectedClass === null) {
			if ($object instanceof Klass) {
				$reflectedClass = $object->__getClass();
			} else {
				$reflectedClass = new ReflectionClass(get_class($object));
			}
		}
		
		// injects dependencies (before registering to avoid cyclic injection)
		self::inject($object, $reflectedClass);
		
		// if the @Dependency tag is found in doc comment, registers the class
		if (preg_match('/@Dependency(\((.+)\)|)/', $reflectedClass->getDocComment(), $dependencyTag)) {
			if (!empty($dependencyTag[1])) {
				$name = $dependencyTag[2];
			} else {
				$name = $reflectedClass->getName();
			}
			self::registerDependency($object, $name);
		}
	}
	
	/**
	 * Returns the type of a variable or its class name if it's an object.
	 * This method does not use gettype() as its not recommended in the PHP manual
	 *
	 * @see gettyppe()
	 * @param mixed $var
	 * @return string
	 */
	public static function getType($var)
	{
		if (is_array($var)) {
			return 'array';
		} else if (is_bool($var)) {
			return 'boolean';
		} else if (is_double($var)) {
			return 'double';
		} else if (is_float($var)) {
			return 'float';
		} else if (is_int($var)) {
			return 'integer';
		} else if (is_long($var)) {
			return 'long';
		} else if (is_object($var)) {
			return get_class($var);
		} else if (is_resource($var)) {
			return 'resource';
		} else if (is_string($var)) {
			return 'string';
		}
		return 'unknown';
	}
	
	/**
	 * Constructor
	 * 
	 * @see Klass::apply()
	 */
	public function __construct()
	{
		Klass::apply($this);
	}
	
	/**
	 * Overload method to provide automatic getters and setters
	 *
	 * @see Klass::__createAccessor()
	 * @param string $methodName
	 * @param array $parameters
	 */
	public function __call($methodName, $parameters)
	{
		if (!preg_match('/(set|get|is)([A-Z])([a-zA-Z0-9]+)/', $methodName, $methodInfo)) {
			return;
		}
		
		$fullMethodName = get_class($this) . '::' . $methodName . '()';
		$accessorName = strtolower($methodInfo[2]) . $methodInfo[3];
		$accessor = $methodInfo[1];
		
		// retrieves information about the accessor
		if (!isset($this->__accessorsInfo[$accessorName])) {
			// information not set yet, creates them now
			$this->__createAccessor($accessorName);
		}
		
		if (($accessorInfo = $this->__accessorsInfo[$accessorName]) === false) {
			throw new Exception('Undefined method ' . $fullMethodName);
		}
		
		if (!in_array($accessor, $accessorInfo['availableAccessors'])) {
			// method not available because the associated accessor has not been enabled
			throw new Exception('Undefined method ' . $fullMethodName);
		}
		
		if ($accessor == 'set') {
			// retreives the value if its a setter
			if (count($parameters) == 0) {
				throw new Exception('Missing first parameter in method ' . $fullMethodName);
			}
			$value = $parameters[0];
			
			// checks if the value is of the specified type
			if ($accessorInfo['type'] != 'mixed' && self::getType($value) != $accessorInfo['type']) {
				throw new Exception('Wrong parameter type in ' . $fullMethodName . ', expected ' . $accessorInfo['type']);
			}
			
			$this->{$accessorInfo['propertyName']} = $value;
			return $this;
			
		} else {
			return $this->{$accessorInfo['propertyName']};
		}
	}
	
	/**
	 * Creates the information array about an accessor.
	 * 
	 * For a property to have accessors, it should have the @Accessors tag
	 * in its doc comment. It is possible to limit which accessors are created
	 * for a property by specifying them (separated by commas) between parentheses 
	 * after the tag: @Accessors(set,get)
	 * 
	 * Available accessors are get, set and is.
	 *
	 * @param string $propertyName
	 * @return array Accessor information
	 */
	protected function __createAccessor($propertyName)
	{
		$privatePropertyName = '_' . $propertyName;
		$class = $this->__getClass();
		$property = null;
		
		// retreives the ReflectedProperty object
		if ($class->hasProperty($propertyName)) {
			$property = $class->getProperty($propertyName);
		} else if ($class->hasProperty($privatePropertyName)) {
			$property = $class->getProperty($privatePropertyName);
		}
		
		// the property doesn't exists or it does not have the @Property tag
		if ($property === null || !preg_match('/@Accessors(\((.+)\)|)/', $property->getDocComment(), $accessorTag)) {
			return $this->__accessorsInfo[$propertyName] = false;
		}
			
		// if @var is used, retreives the type from it. If not, use "mixed" as type.
		$type = 'mixed';
		if (preg_match('/@var (.+)\s*$/m', $property->getDocComment(), $varTag)) {
			$type = $varTag[1];
		}
		
		// retreives available accessors
		if (empty($accessorTag[1])) {
			$availableAccessors = array('get', 'set');
			if ($type == 'boolean') {
				$availableAccessors[] = 'is';
			}
		} else {
			$availableAccessors = explode(',', $accessorTag[2]);
		}
		
		return $this->__accessorsInfo[$propertyName] = array(
			'propertyName'			=> $property->getName(),
			'type' 					=> $type,
			'availableAccessors' 	=> $availableAccessors
		);
	}
	
	/**
	 * Returns the ReflectionClass object of the current object
	 *
	 * @return ReflectionClass
	 */
	public function __getClass()
	{
		if ($this->__reflectedClass === null) {
			$this->__reflectedClass = new ReflectionClass(get_class($this));
		}
		return $this->__reflectedClass;
	}
}
Return current item: Klass