Mwjames has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/80139


Change subject: Extend DI framework to support SCOPE_SINGLETON
......................................................................

Extend DI framework to support SCOPE_SINGLETON

Most DI frameworks allow to specify if an object is be instantiated as
prototype or singleton.

* prototypical scope (default) where each new injection returns a new instance
* singleton scope will return the same instance for lifetime of a request

If not otherwise stated, all objects are create with SCOPE_PROTOTYPE.

## Example

$this->registerObject( 'Foo', function ( return new Foo() ) { ... }, 
self::SCOPE_SINGLETON )
$this->registerObject( 'Foo', new Foo(), self::SCOPE_SINGLETON )

Change-Id: I918b93511f2956aad918924932a185cdf95e2834
---
M includes/dic/BaseDependencyContainer.php
M includes/dic/README.mediawiki
M includes/dic/SharedDependencyContainer.php
M includes/dic/SimpleDependencyBuilder.php
M tests/phpunit/includes/dic/SimpleDependencyBuilderTest.php
5 files changed, 187 insertions(+), 11 deletions(-)


  git pull 
ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/SemanticMediaWiki 
refs/changes/39/80139/1

diff --git a/includes/dic/BaseDependencyContainer.php 
b/includes/dic/BaseDependencyContainer.php
index 3ac6c2c..4ddeb8f 100644
--- a/includes/dic/BaseDependencyContainer.php
+++ b/includes/dic/BaseDependencyContainer.php
@@ -21,6 +21,12 @@
  */
 abstract class BaseDependencyContainer extends ObjectStorage implements 
DependencyContainer {
 
+       /** New instance for each request */
+       const SCOPE_PROTOTYPE = 0;
+
+       /** Same instance for the lifetime of a request */
+       const SCOPE_SINGLETON = 1;
+
        /**
         * @see ObjectStorage::contains
         *
@@ -101,7 +107,7 @@
         * @param mixed $value
         */
        public function __set( $objectName, $objectSignature ) {
-               $this->set( $objectName, $objectSignature );
+               $this->registerObject( $objectName, $objectSignature );
        }
 
        /**
@@ -125,8 +131,8 @@
         * @param string $objectName
         * @param mixed $signature
         */
-       public function registerObject( $objectName, $objectSignature ) {
-               $this->set( $objectName, $objectSignature );
+       public function registerObject( $objectName, $objectSignature, 
$objectScope = self::SCOPE_PROTOTYPE ) {
+               $this->set( $objectName, array( $objectSignature, $objectScope 
) );
        }
 
 }
diff --git a/includes/dic/README.mediawiki b/includes/dic/README.mediawiki
index a737b44..3006b91 100644
--- a/includes/dic/README.mediawiki
+++ b/includes/dic/README.mediawiki
@@ -32,8 +32,23 @@
 |-
 | prototype (default)
 | Each injection or call of the newObject() method returns a new instance
+|-
+| singleton
+| Singleton scope will return the same instance for the lifetime of a request
 |}
 
+<pre>
+$container = new EmptyDependencyContainer();
+
+// SCOPE_PROTOTYPE (default)
+$container->registerObject( 'Foo', function ( return new Foo() ) { ... } )
+$container->registerObject( 'Foo', new Foo() )
+
+// SCOPE_SINGLETON
+$container->registerObject( 'Foo', function ( return new Foo() ) { ... }, 
$container::SCOPE_SINGLETON )
+$container->registerObject( 'Foo', new Foo(), $container::SCOPE_SINGLETON )
+</pre>
+
 == DependencyContainer ==
 * DependencyObject an interface that specifies a method to register a 
dependency object
 * DependencyContainer an interface that specifies methods to retrieve and 
store object definitions
diff --git a/includes/dic/SharedDependencyContainer.php 
b/includes/dic/SharedDependencyContainer.php
index 2bf6798..f911d29 100644
--- a/includes/dic/SharedDependencyContainer.php
+++ b/includes/dic/SharedDependencyContainer.php
@@ -37,15 +37,15 @@
 
                $this->registerObject( 'Settings', function () {
                        return Settings::newFromGlobals();
-               } );
+               }, self::SCOPE_SINGLETON );
 
                $this->registerObject( 'Store', function ( DependencyBuilder 
$builder ) {
                        return StoreFactory::getStore( $builder->newObject( 
'Settings' )->get( 'smwgDefaultStore' ) );
-               } );
+               }, self::SCOPE_SINGLETON );
 
                $this->registerObject( 'CacheHandler', function ( 
DependencyBuilder $builder ) {
                        return CacheHandler::newFromId( $builder->newObject( 
'Settings' )->get( 'smwgCacheType' ) );
-               } );
+               }, self::SCOPE_SINGLETON );
 
                $this->registerObject( 'ParserData', function ( 
DependencyBuilder $builder ) {
                        return new ParserData(
diff --git a/includes/dic/SimpleDependencyBuilder.php 
b/includes/dic/SimpleDependencyBuilder.php
index 6e38729..ed98c0c 100644
--- a/includes/dic/SimpleDependencyBuilder.php
+++ b/includes/dic/SimpleDependencyBuilder.php
@@ -221,17 +221,70 @@
         */
        protected function build( $objectName ) {
 
+               $dependencyContainer = $this->dependencyContainer;
+
                if ( !is_string( $objectName ) ) {
                        throw new InvalidArgumentException( 'Argument is not a 
string' );
                }
 
-               if ( !$this->dependencyContainer->has( $objectName ) ) {
+               if ( !$dependencyContainer->has( $objectName ) ) {
                        throw new OutOfBoundsException( "{$objectName} is not 
registered" );
                }
 
-               $object = $this->dependencyContainer->get( $objectName );
+               list( $object, $scope ) = $dependencyContainer->get( 
$objectName );
+
+               if ( $scope === $dependencyContainer::SCOPE_SINGLETON ) {
+                       $object = $this->fetchSingleton( $objectName, $object );
+               }
 
                return is_callable( $object ) ? $object( $this ) : $object;
        }
 
+
+       /**
+        * Fetch singleton object
+        *
+        * @note Internally all singleton objects are preceded with 'sing_' in 
the
+        * object storage
+        *
+        * @since  1.9
+        *
+        * @param string $objectName
+        * @param mixed $objectDefinition
+        *
+        * @return mixed
+        */
+       private function fetchSingleton( $objectName, $objectDefinition ) {
+
+               $objectName = 'sing_' . $objectName;
+
+               if ( !$this->dependencyContainer->has( $objectName ) ) {
+                       $this->dependencyContainer->set( $objectName, 
$this->buildSingelton( $objectDefinition ) );
+               }
+
+               $singleton = $this->dependencyContainer->get( $objectName );
+
+               return $singleton( $this );
+       }
+
+       /**
+        * Build singleton instance
+        *
+        * Keep the context within the closure as static so any repeated call to
+        * this closure object will find the static context instead
+        *
+        * @param mixed $objectDefinition
+        */
+       private function buildSingelton( $objectDefinition ) {
+
+               // Resolve the object once and use the result for static 
injection
+               $object = is_callable( $objectDefinition ) ? $objectDefinition( 
$this ) : $objectDefinition;
+
+               return function() use ( $object ) {
+                       static $singleton;
+                       return $singleton = $singleton === null ? $object : 
$singleton;
+               };
+
+       }
+
 }
diff --git a/tests/phpunit/includes/dic/SimpleDependencyBuilderTest.php 
b/tests/phpunit/includes/dic/SimpleDependencyBuilderTest.php
index 17195b3..4522deb 100644
--- a/tests/phpunit/includes/dic/SimpleDependencyBuilderTest.php
+++ b/tests/phpunit/includes/dic/SimpleDependencyBuilderTest.php
@@ -96,13 +96,13 @@
                $instance = $this->newInstance();
 
                // Register container
-               $container = $this->newDependencyContainer( array( 'Test' => 
'123' ) );
+               $container = $this->newDependencyContainer( array( 'Test' => 
array( '123', 0 ) ) );
                $instance->registerContainer( $container );
 
                $this->assertEquals( '123', $instance->newObject( 'Test' ) );
 
                // Register additional container
-               $container = $this->newDependencyContainer( array( 'Test2' => 
9001 ) );
+               $container = $this->newDependencyContainer( array( 'Test2' => 
array( 9001, 1 ) ) );
                $instance->registerContainer( $container );
 
                // Verifies that both objects are avilable
@@ -111,7 +111,7 @@
 
                // Register another container containing the same identifier but
                // with a different definition
-               $container = $this->newDependencyContainer( array( 'Test2' => 
1009 ) );
+               $container = $this->newDependencyContainer( array( 'Test2' => 
array( 1009, 0 ) ) );
                $instance->registerContainer( $container );
                $this->assertEquals( 1009, $instance->newObject( 'Test2' ) );
 
@@ -227,6 +227,72 @@
        }
 
        /**
+        * @test SimpleDependencyBuilder::newObject
+        * @dataProvider scopeDataProvider
+        *
+        * @since 1.9
+        */
+       public function testScope( $setup, $expected ) {
+
+               $instance  = $this->newInstance();
+               $container = $instance->getContainer();
+               $reflector = $this->newReflector( get_class( $container ) );
+               $scope     = $reflector->getConstant( $setup['scope'] );
+               $title     = $this->newTitle( NS_MAIN, 'Lila' );
+
+               // Lazy loading or deferred instantiation
+               $instance->getContainer()->registerObject( 'Test3', function ( 
DependencyBuilder $builder ) {
+                       return DIWikiPage::newFromTitle( $builder->getArgument( 
'Title' ) );
+               }, $scope );
+
+               $newInstance = $instance->newObject( 'Test3', array( $title ) );
+               $this->assertEquals( $title, $newInstance->getTitle() );
+               $this->assertEquals( $expected, $newInstance === 
$instance->newObject( 'Test3', array( $title ) ) );
+
+               // Eager loading, means that the object is created during 
initialization and not
+               // during execution which is forcing the object to be created 
instantly and therefore
+               // is the same indifferent from the chosen scope
+               $instance->getContainer()->registerObject( 'Title', 
$this->newTitle(), $scope );
+
+               $newInstance = $instance->newObject( 'Title' );
+               $this->assertTrue( $newInstance === $instance->newObject( 
'Title' ) );
+
+       }
+
+       /**
+        * @test SimpleDependencyBuilder::newObject
+        * @dataProvider setGetScopeDataProvider
+        *
+        * @since 1.9
+        */
+       public function testSetGetMagicWordScope( $setup, $expected ) {
+
+               $instance  = $this->newInstance();
+               $container = $instance->getContainer();
+               $reflector = $this->newReflector( get_class( $container ) );
+               $scope     = $reflector->getConstant( $setup['scope'] );
+
+               // __set/__get itself is alwasy of type SCOPE_PROTOTYPE
+               $instance->getContainer()->title1234 = function() { return 
$this->newTitle(); };
+               $this->assertTrue( $instance->title1234 !== 
$instance->newObject( 'title1234' ) );
+
+               // Override previous object
+               $instance->getContainer()->title1234 = function() { return 
$this->newTitle(); };
+               $instance->getContainer()->registerObject( 'title12345', 
function( $builder ) {
+                       return $builder->title1234;
+               }, $scope );
+
+               $this->assertEquals( $expected,
+                       $instance->title12345 === $instance->newObject( 
'title12345' )
+               );
+
+               $this->assertEquals( $expected,
+                       $instance->title12345->getText() === 
$instance->newObject( 'title12345' )->getText()
+               );
+
+       }
+
+       /**
         * @test SimpleDependencyBuilder::getArgument
         *
         * @since 1.9
@@ -274,4 +340,40 @@
 
        }
 
+       /**
+        * @return array
+        */
+       public function scopeDataProvider() {
+
+               $provider = array();
+
+               // __set/__get embbeded in a SCOPE_SINGLETON call which makes 
the
+               // __set/__get object indirect available through the SINGLETON 
as it is only
+               // executed once during intialization
+               $provider[] = array( array( 'scope' => 'SCOPE_SINGLETON' ), 
true );
+
+               // Invers behaviour to the previous assert
+               $provider[] = array( array( 'scope' => 'SCOPE_PROTOTYPE' ), 
false );
+
+               return $provider;
+       }
+
+       /**
+        * @return array
+        */
+       public function setGetScopeDataProvider() {
+
+               $provider = array();
+
+               // __set/__get embbeded in a SCOPE_SINGLETON call which makes 
the
+               // __set/__get object indirect available through the SINGLETON 
as it is only
+               // executed once during intialization
+               $provider[] = array( array( 'scope' => 'SCOPE_SINGLETON' ), 
true );
+
+               // Invers behaviour to the previous assert
+               $provider[] = array( array( 'scope' => 'SCOPE_PROTOTYPE' ), 
false );
+
+               return $provider;
+       }
+
 }

-- 
To view, visit https://gerrit.wikimedia.org/r/80139
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I918b93511f2956aad918924932a185cdf95e2834
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/SemanticMediaWiki
Gerrit-Branch: master
Gerrit-Owner: Mwjames <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to