<?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;
}
}