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