[!!!][+TASK] Extbase (Object): Rewritten Object Container
authorSebastian Kurfürst <sebastian@typo3.org>
Mon, 20 Dec 2010 16:56:37 +0000 (16:56 +0000)
committerSebastian Kurfürst <sebastian@typo3.org>
Mon, 20 Dec 2010 16:56:37 +0000 (16:56 +0000)
I have re-written the core of the Object Manager,
implementing proper dependency injection now, covered
by some unit tests (which could still be improved)...

Now, the object lifecycle method ("initializeObject")
is automatically called once an object has all of its
dependencies injected.

Additionally, Extbase (again) works in the backend
with an empty page tree.

Please thoroughly test this change!

typo3/sysext/extbase/Classes/Core/Bootstrap.php
typo3/sysext/extbase/Classes/DomainObject/AbstractDomainObject.php
typo3/sysext/extbase/Classes/Object/Container/Container.php
typo3/sysext/extbase/Classes/Object/ObjectManager.php
typo3/sysext/extbase/Classes/Persistence/Repository.php
typo3/sysext/extbase/Tests/Unit/Object/Container/ContainerTest.php
typo3/sysext/extbase/Tests/Unit/Object/Container/Fixtures/Testclasses.php
typo3/sysext/extbase/ext_localconf.php

index efec0ba..7026608 100644 (file)
@@ -156,10 +156,11 @@ class Tx_Extbase_Core_Bootstrap {
                if (!is_array($typoScriptSetup['config.']['tx_extbase.']['objects.'])) {
                        throw new RuntimeException('Object configuration was not found in "config.tx_extbase.objects". Please make sure, that the TypoScript setup is loaded', 1290623010);
                }
+               $objectContainer = t3lib_div::makeInstance('Tx_Extbase_Object_Container_Container');
                foreach ($typoScriptSetup['config.']['tx_extbase.']['objects.'] as $classNameWithDot => $classConfiguration) {
                        if (isset($classConfiguration['className'])) {
                                $originalClassName = rtrim($classNameWithDot, '.');
-                               Tx_Extbase_Object_Container_Container::getContainer()->registerImplementation($originalClassName, $classConfiguration['className']);
+                               $objectContainer->registerImplementation($originalClassName, $classConfiguration['className']);
                        }
                }
        }
index 3b2b45b..09601f8 100644 (file)
@@ -55,16 +55,6 @@ abstract class Tx_Extbase_DomainObject_AbstractDomainObject implements Tx_Extbas
        private $_isClone = FALSE;
 
        /**
-        * The generic constructor. If you want to implement your own __constructor() method in your Domain Object you have to call
-        * $this->initializeObject() in the first line of your constructor.
-        *
-        * @var array
-        */
-       public function __construct() {
-               $this->initializeObject();
-       }
-
-       /**
         * This is the magic __wakeup() method. It's invoked by the unserialize statement in the reconstitution process
         * of the object. If you want to implement your own __wakeup() method in your Domain Object you have to call
         * parent::__wakeup() first!
@@ -75,13 +65,7 @@ abstract class Tx_Extbase_DomainObject_AbstractDomainObject implements Tx_Extbas
                $this->initializeObject();
        }
 
-       /**
-        * A template method to initialize an object. This can be used to manipulate the object after
-        * reconstitution and before the clean state of it's properties is stored.
-        *
-        * @return void
-        */
-       protected function initializeObject() {
+       public function initializeObject() {
        }
 
        /**
index 62e5d92..f091086 100644 (file)
 ***************************************************************/
 
 /**
- * TYPO3 Dependency Injection container
- * Initial Usage:
- *  $container = Tx_Extbase_Object_Container_Container::getContainer()
+ * Internal TYPO3 Dependency Injection container
  *
  * @author Daniel Pötzinger
+ * @author Sebastian Kurfürst
  */
-class Tx_Extbase_Object_Container_Container {
-
-       /**
-        * PHP singleton impelementation
-        *
-        * @var Tx_Extbase_Object_Container_Container
-        */
-       static private $containerInstance = null;
+class Tx_Extbase_Object_Container_Container implements t3lib_Singleton {
 
        /**
         * internal cache for classinfos
@@ -58,23 +50,19 @@ class Tx_Extbase_Object_Container_Container {
 
        /**
         * reference to the classinfofactory, that analyses dependencys
-        * @var classInfoFactory
+        *
+        * @var Tx_Extbase_Object_Container_ClassInfoFactory
         */
        private $classInfoFactory;
 
        /**
         * holds references of singletons
+        *
         * @var array
         */
        private $singletonInstances = array();
 
        /**
-        * holds references of objects that still needs setter injection processing
-        * @var array
-        */
-       private $setterInjectionRegistry = array();
-
-       /**
         * Constructor is protected since container should
         * be a singleton.
         *
@@ -82,99 +70,90 @@ class Tx_Extbase_Object_Container_Container {
         * @param void
         * @return void
         */
-       protected function __construct() {
-               $this->classInfoFactory = new Tx_Extbase_Object_Container_ClassInfoFactory();
-               $this->cache = new Tx_Extbase_Object_Container_ClassInfoCache();
+       public function __construct() {
+               $this->classInfoFactory = t3lib_div::makeInstance('Tx_Extbase_Object_Container_ClassInfoFactory');
+               $this->cache = t3lib_div::makeInstance('Tx_Extbase_Object_Container_ClassInfoCache');
        }
 
        /**
-        * Returns an instance of the container singleton.
-        *
-        * @return Tx_Extbase_Object_Container_Container
-        */
-       static public function getContainer() {
-               if (self::$containerInstance === NULL) {
-                       self::$containerInstance = new Tx_Extbase_Object_Container_Container();
-               }
-               return self::$containerInstance;
-       }
-
-       private function __clone() {}
-
-       /**
         * gets an instance of the given class
         * @param string $className
         * @return object
         */
-       public function getInstance($className) {
-               $givenConstructorArguments=array();
-               if (func_num_args() > 1) {
-                               $givenConstructorArguments = func_get_args();
-                               array_shift($givenConstructorArguments);
-               }
-               $object = $this->getInstanceFromClassName($className, $givenConstructorArguments, 0);
-               $this->processSetterInjectionRegistry();
-               return $object;
-       }
+       public function getInstance($className, $givenConstructorArguments = array()) {
+               $className = $this->getImplementationClassName($className);
 
-       /**
-        * register a classname that should be used if a dependency is required.
-        * e.g. used to define default class for a interface
-        *
-        * @param string $className
-        * @param string $alternativeClassName
-        */
-       public function registerImplementation($className,$alternativeClassName) {
-               $this->alternativeImplementation[$className] = $alternativeClassName;
-       }
-
-       /**
-        * gets an instance of the given class
-        * @param string $className
-        * @param array $givenConstructorArguments
-        */
-       private function getInstanceFromClassName($className, array $givenConstructorArguments=array(), $level=0) {
-               if ($level > 30) {
-                       throw new Tx_Extbase_Object_Container_Exception_TooManyRecursionLevelsException('Too many recursion levels. This should never happen, please file a bug!' . $className, 1289386945);
-               }
                if ($className === 'Tx_Extbase_Object_Container_Container') {
                        return $this;
                }
+
                if (isset($this->singletonInstances[$className])) {
+                       if (count($givenConstructorArguments) > 0) {
+                               throw new Tx_Extbase_Object_Exception('Object "' . $className . '" fetched from singleton cache, thus, explicit constructor arguments are not allowed.', 1292857934);
+                       }
                        return $this->singletonInstances[$className];
                }
-
-               $className = $this->getClassName($className);
-
                $classInfo = $this->getClassInfo($className);
 
-               $constructorArguments = $this->getConstructorArguments($classInfo->getConstructorArguments(), $givenConstructorArguments, $level);
-               $instance = $this->newObject($className, $constructorArguments);
+               $instance = $this->instanciateObject($className, $classInfo, $givenConstructorArguments);
+               $this->injectDependencies($instance, $classInfo);
 
-               if ($level > 0 && !($instance instanceof t3lib_Singleton)) {
-                       throw new Tx_Extbase_Object_Exception_WrongScope('Object "' . $className . '" is of not of scope singleton, but only singleton instances can be injected into other classes.', 1289387047);
+               if (method_exists($instance, 'initializeObject') && is_callable(array($instance, 'initializeObject'))) {
+                       $instance->initializeObject();
                }
 
-               if ($classInfo->hasInjectMethods()) {
-                       $this->setterInjectionRegistry[]=array($instance, $classInfo->getInjectMethods(), $level);
+               return $instance;
+       }
+
+       /**
+        * Instanciates an object, possibly setting the constructor dependencies.
+        * Additionally, directly registers all singletons in the singleton registry,
+        * such that circular references of singletons are correctly instanciated.
+        *
+        * @param <type> $className
+        * @param Tx_Extbase_Object_Container_ClassInfo $classInfo
+        * @param array $givenConstructorArguments
+        * @return <type>
+        */
+       protected function instanciateObject($className, Tx_Extbase_Object_Container_ClassInfo $classInfo, array $givenConstructorArguments) {
+               $classIsSingleton = $this->isSingleton($className);
+
+               if ($classIsSingleton && count($givenConstructorArguments) > 0) {
+                       throw new Tx_Extbase_Object_Exception('Object "' . $className . '" has explicit constructor arguments but is a singleton; this is not allowed.', 1292858051);
                }
 
-               if ($instance instanceof t3lib_Singleton) {
+               $constructorArguments = $this->getConstructorArguments($classInfo->getConstructorArguments(), $givenConstructorArguments, $level);
+               array_unshift($constructorArguments, $className);
+               $instance = call_user_func_array(array('t3lib_div', 'makeInstance'), $constructorArguments);
+
+               if ($classIsSingleton) {
                        $this->singletonInstances[$className] = $instance;
                }
                return $instance;
        }
 
+       protected function injectDependencies($instance, Tx_Extbase_Object_Container_ClassInfo $classInfo) {
+               if (!$classInfo->hasInjectMethods()) return;
+
+               foreach ($classInfo->getInjectMethods() as $injectMethodName => $classNameToInject) {
+
+                       $instanceToInject = $this->getInstance($classNameToInject);
+                       if (!$instanceToInject instanceof t3lib_Singleton) {
+                               throw new Tx_Extbase_Object_Exception_WrongScope('Setter dependencies can only be singletons', 1292860859);
+                       }
+                       $instance->$injectMethodName($instanceToInject);
+               }
+       }
+
        /**
-        * returns a object of the given type, called with the constructor arguments.
-        * For speed improvements reflection is avoided
+        * register a classname that should be used if a dependency is required.
+        * e.g. used to define default class for a interface
         *
         * @param string $className
-        * @param array $constructorArguments
+        * @param string $alternativeClassName
         */
-       private function newObject($className, array $constructorArguments) {
-               array_unshift($constructorArguments, $className);
-               return call_user_func_array(array('t3lib_div', 'makeInstance'), $constructorArguments);
+       public function registerImplementation($className,$alternativeClassName) {
+               $this->alternativeImplementation[$className] = $alternativeClassName;
        }
 
        /**
@@ -189,12 +168,23 @@ class Tx_Extbase_Object_Container_Container {
                foreach ($constructorArgumentInformation as $argumentInformation) {
                        $argumentName = $argumentInformation['name'];
 
-                       if (count($givenConstructorArguments)) {
-                               // we have a value to set
-                               $parameter = array_shift($givenConstructorArguments);
-                       } elseif (isset($argumentInformation['dependency'])) {
+                       // We have a dependency we can automatically wire,
+                       // AND the class has NOT been explicitely passed in
+                       if (isset($argumentInformation['dependency']) && !(count($givenConstructorArguments) && is_a($givenConstructorArguments[0], $argumentInformation['dependency']))) {
                                // Inject parameter
-                               $parameter = $this->getInstanceFromClassName($argumentInformation['dependency'], array(), $level+1);
+                               if (!$this->isSingleton($argumentInformation['dependency'])) {
+                                       throw new Tx_Extbase_Object_Exception_WrongScope('Constructor dependencies can only be singletons', 1292860858);
+                               }
+                               $parameter = $this->getInstance($argumentInformation['dependency']);
+                       } elseif (count($givenConstructorArguments)) {
+                               // EITHER:
+                               // No dependency injectable anymore, but we still have
+                               // an explicit constructor argument
+                               // OR:
+                               // the passed constructor argument matches the type for the dependency
+                               // injection, and thus the passed constructor takes precendence over
+                               // autowiring.
+                               $parameter = array_shift($givenConstructorArguments);
                        } elseif (isset($argumentInformation['defaultValue'])) {
                                // no value to set anymore, we take default value
                                $parameter = $argumentInformation['defaultValue'];
@@ -206,6 +196,10 @@ class Tx_Extbase_Object_Container_Container {
                return $parameters;
        }
 
+
+       protected function isSingleton($object) {
+               return in_array('t3lib_Singleton', class_implements($object));
+       }
        /**
         * Returns the class name for a new instance, taking into account the
         * class-extension API.
@@ -213,7 +207,7 @@ class Tx_Extbase_Object_Container_Container {
         * @param       string          Base class name to evaluate
         * @return      string          Final class name to instantiate with "new [classname]"
         */
-       protected function getClassName($className) {
+       protected function getImplementationClassName($className) {
                if (isset($this->alternativeImplementation[$className])) {
                        $className = $this->alternativeImplementation[$className];
                }
@@ -226,19 +220,6 @@ class Tx_Extbase_Object_Container_Container {
        }
 
        /**
-        * do inject dependecies to object $instance using the given methods
-        *
-        * @param object $instance
-        * @param array $setterMethods
-        * @param integer $level
-        */
-       private function handleSetterInjection($instance, array $setterMethods, $level) {
-               foreach ($setterMethods as $method => $dependency) {
-                       $instance->$method($this->getInstanceFromClassName($dependency, array(), $level+1));
-               }
-       }
-
-       /**
         * Gets Classinfos for the className - using the cache and the factory
         *
         * @param string $className
@@ -252,20 +233,4 @@ class Tx_Extbase_Object_Container_Container {
                }
                return $this->cache->get($className);
        }
-
-       /**
-        * does setter injection based on the data in $this->setterInjectionRegistry
-        * Its done as long till no setters are left
-        *
-        * @return void
-        */
-       private function processSetterInjectionRegistry() {
-               while (count($this->setterInjectionRegistry)>0) {
-                       $currentSetterData = $this->setterInjectionRegistry;
-                       $this->setterInjectionRegistry = array();
-                       foreach ($currentSetterData as $setterInjectionData) {
-                               $this->handleSetterInjection($setterInjectionData[0], $setterInjectionData[1], $setterInjectionData[2]);
-                       }
-               }
-       }
 }
\ No newline at end of file
index 12916a7..85ae014 100644 (file)
@@ -42,7 +42,7 @@ class Tx_Extbase_Object_ObjectManager implements Tx_Extbase_Object_ObjectManager
         * Constructs a new Object Manager
         */
        public function __construct() {
-               $this->objectContainer = Tx_Extbase_Object_Container_Container::getContainer();
+               $this->objectContainer = t3lib_div::makeInstance('Tx_Extbase_Object_Container_Container'); // Singleton
        }
 
        /**
@@ -60,7 +60,8 @@ class Tx_Extbase_Object_ObjectManager implements Tx_Extbase_Object_ObjectManager
         */
        public function get($objectName) {
                $arguments = func_get_args();
-               return call_user_func_array(array($this->objectContainer, 'getInstance'), $arguments);
+               array_shift($arguments);
+               return $this->objectContainer->getInstance($objectName, $arguments);
        }
 
        /**
@@ -78,7 +79,8 @@ class Tx_Extbase_Object_ObjectManager implements Tx_Extbase_Object_ObjectManager
         */
        public function create($objectName) {
                $arguments = func_get_args();
-               $instance = call_user_func_array(array($this->objectContainer, 'getInstance'), $arguments);
+               array_shift($arguments);
+               $instance = $this->objectContainer->getInstance($objectName, $arguments);
 
                if ($instance instanceof t3lib_Singleton) {
                        throw new Tx_Extbase_Object_Exception_WrongScope('Object "' . $objectName . '" is of not of scope prototype, but only prototype is supported by create()', 1265203124);
index edb144c..f942e31 100644 (file)
@@ -104,15 +104,6 @@ class Tx_Extbase_Persistence_Repository implements Tx_Extbase_Persistence_Reposi
                } else {
                        $this->objectManager = $objectManager;
                }
-               $this->initializeObject();
-       }
-
-       /**
-        * Life cycle method. You can override this in your own repository
-        *
-        * @return void
-        */
-       public function initializeObject() {
        }
 
        /**
index 804bd9b..ba638cd 100644 (file)
@@ -37,7 +37,7 @@ class Tx_Extbase_Object_Container_ContainerTest extends Tx_Extbase_Tests_Unit_Ba
        private $container;
 
        public function setUp() {
-               $this->container = Tx_Extbase_Object_Container_Container::getContainer();
+               $this->container = new Tx_Extbase_Object_Container_Container();
 
        }
 
@@ -81,7 +81,17 @@ class Tx_Extbase_Object_Container_ContainerTest extends Tx_Extbase_Tests_Unit_Ba
         * @expectedException Tx_Extbase_Object_Exception
         */
        public function getInstanceThrowsExceptionWhenTryingToInstanciateASingletonWithConstructorParameters() {
-               $this->container->getInstance('t3lib_object_tests_amixed_array_singleton');
+               $this->container->getInstance('t3lib_object_tests_amixed_array_singleton', array('somevalue'));
+       }
+
+       /**
+        * @test
+        */
+       public function getInstanceReturnsInstanceOfAClassWithConstructorInjectionAndDefaultConstructorParameters() {
+               $object = $this->container->getInstance('t3lib_object_tests_amixed_array');
+               $this->assertType('t3lib_object_tests_b', $object->b);
+               $this->assertType('t3lib_object_tests_c', $object->c);
+               $this->assertEquals(array('some' => 'default'), $object->myvalue);
        }
 
        /**
@@ -90,7 +100,7 @@ class Tx_Extbase_Object_Container_ContainerTest extends Tx_Extbase_Tests_Unit_Ba
        public function getInstancePassesGivenParameterToTheNewObject() {
                $mockObject = $this->getMock('t3lib_object_tests_c');
 
-               $object = $this->container->getInstance('t3lib_object_tests_a', $mockObject);
+               $object = $this->container->getInstance('t3lib_object_tests_a', array($mockObject));
                $this->assertType('t3lib_object_tests_a', $object);
                $this->assertSame($mockObject, $object->c);
        }
@@ -119,9 +129,17 @@ class Tx_Extbase_Object_Container_ContainerTest extends Tx_Extbase_Tests_Unit_Ba
         * @test
         * @expectedException Tx_Extbase_Object_Exception
         */
-       public function getInstanceThrowsExceptionIfObjectContainsCyclicDependency() {
+       public function getInstanceThrowsExceptionIfObjectContainsCyclicDependencyAndIsNoSingleton() {
                $this->container->getInstance('t3lib_object_tests_cyclic1');
+       }
+
 
+       /**
+        * @test
+        * @expectedException Tx_Extbase_Object_Exception
+        */
+       public function getInstanceThrowsExceptionIfObjectContainsCyclicDependency() {
+               $this->container->getInstance('t3lib_object_tests_cyclic1');
        }
 
        /**
@@ -153,7 +171,7 @@ class Tx_Extbase_Object_Container_ContainerTest extends Tx_Extbase_Tests_Unit_Ba
        /**
         * @test
         */
-       public function test_canBuildCyclicDependenciesWithSetter() {
+       public function test_canBuildCyclicDependenciesOfSingletonsWithSetter() {
                $object = $this->container->getInstance('t3lib_object_tests_resolveablecyclic1');
                $this->assertType('t3lib_object_tests_resolveablecyclic1', $object);
                $this->assertType('t3lib_object_tests_resolveablecyclic1', $object->o2->o3->o1);
index fce57cf..8a05096 100644 (file)
@@ -44,10 +44,10 @@ class t3lib_object_tests_amixed_array_singleton implements t3lib_Singleton {
        public $b;
        public $c;
        public $myvalue;
-       public function __construct(t3lib_object_tests_b $b, t3lib_object_tests_c $c, array $myvalue=array('some' => 'default')) {
+       public function __construct(t3lib_object_tests_b $b, t3lib_object_tests_c $c, $someDefaultParameter = array('some' => 'default')) {
                $this->b = $b;
                $this->c = $c;
-               $this->myvalue = $myvalue;
+               $this->myvalue = $someDefaultParameter;
        }
 }
 
@@ -78,7 +78,7 @@ class t3lib_object_tests_c implements t3lib_Singleton {
 class t3lib_object_tests_b_child extends t3lib_object_tests_b {
 }
 
-interface t3lib_object_tests_someinterface {
+interface t3lib_object_tests_someinterface extends t3lib_Singleton {
 
 }
 
@@ -86,7 +86,7 @@ interface t3lib_object_tests_someinterface {
  * class which implements a Interface
  *
  */
-class t3lib_object_tests_someimplementation implements t3lib_object_tests_someinterface, t3lib_Singleton {
+class t3lib_object_tests_someimplementation implements t3lib_object_tests_someinterface {
 }
 
 /**
index a4fa1b5..6625930 100644 (file)
@@ -24,5 +24,13 @@ $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['cache_extb
        ),
 );
 
+// We need to set the default implementation for the Storage Backend
+// the code below is NO PUBLIC API! It's just to make sure that
+// Extbase works correctly in the backend if the page tree is empty or no
+// template is defined.
+$extbaseObjectContainer = t3lib_div::makeInstance('Tx_Extbase_Object_Container_Container'); // Singleton
+$extbaseObjectContainer->registerImplementation('Tx_Extbase_Persistence_Storage_BackendInterface', 'Tx_Extbase_Persistence_Storage_Typo3DbBackend');
+unset($extbaseObjectContainer);
+
 # $GLOBALS ['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'][] = 'EXT:extbase/Classes/Persistence/Hook/TCEMainValueObjectUpdater.php:tx_Extbase_Persistence_Hook_TCEMainValueObjectUpdater';
 ?>
\ No newline at end of file