Adamw has submitted this change and it was merged.

Change subject: Add minimal dependency injection, including tests
......................................................................


Add minimal dependency injection, including tests

Not re-inventing the wheel, just a spoke or two. If a DI library is
approved for use on WMF production, that should be used instead.

Change-Id: Ia24d55c43f47b82513a46032434bf81fcde2e18c
---
M Campaigns.php
A includes/setup/Setup.php
A tests/phpunit/setup/SetupTest.php
3 files changed, 429 insertions(+), 0 deletions(-)

Approvals:
  Adamw: Verified; Looks good to me, approved
  jenkins-bot: Checked



diff --git a/Campaigns.php b/Campaigns.php
index 57e9851..bffe0ec 100644
--- a/Campaigns.php
+++ b/Campaigns.php
@@ -29,6 +29,7 @@
 // Autoload
 
 $wgAutoloadClasses['Campaigns\Hooks'] = $dir . '/Campaigns.hooks.php';
+$wgAutoloadClasses['Campaigns\Setup\Setup'] = $dir . 
'/includes/setup/Setup.php';
 
 
 // Hooks
diff --git a/includes/setup/Setup.php b/includes/setup/Setup.php
new file mode 100644
index 0000000..377d85e
--- /dev/null
+++ b/includes/setup/Setup.php
@@ -0,0 +1,196 @@
+<?php
+
+namespace Campaigns\Setup;
+
+use \MWException;
+use \ReflectionClass;
+
+/**
+ * Helper for managing general setup.
+ *
+ * This class provides minimal, lightweight dependency injection.
+ *
+ * You can register a type and the concrete class you wish to instantiate for
+ * that type. When you call get() with the name of the type, you'll get an
+ * instance of the concrete class you registered.
+ *
+ * If the class to instantiate has parameters in its constructor, they should 
be
+ * type-hinted, and concrete classes to instantiate should be registered for
+ * those types.
+ *
+ * Only the 'singleton' scope is available. For types in this scope, only one
+ * instance will be created.
+ *
+ * More features may be added later on if needed.
+ *
+ * TODO: If an external DI library is approved for use on production WMF sites,
+ * it should be used here instead.
+ */
+class Setup {
+
+       protected static $instance = null;
+
+       private $registrations = array();
+       private $singletons = array();
+
+       /**
+        * Get a global Setup instance
+        *
+        * @return Setup
+        */
+       public static function getInstance() {
+
+               if ( is_null( static::$instance ) ){
+                       static::$instance = new static();
+               }
+
+               return static::$instance;
+       }
+
+       /**
+        * Clear the global Setup instance (useful for setting up tests)
+        */
+       public static function clearInstance() {
+               static::$instance = null;
+       }
+
+       /**
+        * Register a type, an implementation class, and a scope
+        * (for now, must be 'singleton').
+        *
+        * @param string $typeName
+        * @param string $implClassName
+        * @param string $scope For now, must be 'singleton'
+        * @throws MWException
+        */
+       public function register( $typeName, $implClassName, $scope ) {
+
+               // Check this type hasn't already been registered
+               if ( isset( $this->registrations[$typeName] ) ) {
+                       throw new MWException( 'Attempted to register type ' . 
$typeName .
+                               ', but it\'s already registered.' );
+               }
+
+               // Check that the registration isn't for an unsupported scope
+               if ( $scope !== 'singleton' ) {
+                       throw new MWException( 'Attempted to register type ' . 
$typeName .
+                               ' for a scope other than singleton.' );
+               }
+
+               $reflClass = new ReflectionClass( $implClassName );
+
+               // Check that the implementation class is a subclass of the type
+               if ( !$reflClass->isSubClassOf( $typeName ) ) {
+                       throw new MWException( $implClassName . ' is not a 
subclass of ' .
+                               $typeName );
+               }
+
+               // If the implementation class has a constructor, check that 
all the
+               // constructor params are type-hinted
+               if ( $reflClass->hasMethod( '__construct' ) ) {
+                       $constructor = $reflClass->getMethod( '__construct' );
+                       $reflParams = $constructor->getParameters();
+
+                       // Cycle through constructor params
+                       foreach ( $reflParams as $reflParam ) {
+
+                               // Raise an exception if there's no type hint
+                               if ( is_null( $reflParam->getClass() ) ) {
+
+                                       throw new MWException( 'Attempted to 
register class ' .
+                                               $implClassName . ' with no type 
hint for the ' .
+                                               'constructor parameter ' . 
$reflParam->getName() );
+                               }
+                       }
+               }
+
+               // All OK, let's register
+               $this->registrations[$typeName] =
+                       new Registration( $typeName, $implClassName, $scope );
+       }
+
+       /**
+        * Get an object of the class registered for the type $typeName.
+        *
+        * @param string $typeName
+        * @return mixed An object of the class registered for $typeName
+        */
+       public function get( $typeName ) {
+
+               // Check that we have a registration for this type
+               if ( !isset( $this->registrations[$typeName] ) ) {
+                       throw new MWException( 'No concrete class registered 
for ' .
+                               $typeName . '.' );
+               }
+
+               $registration = $this->registrations[$typeName];
+
+               // Only 'singleton' scope is supported so far; this is where we 
could
+               // implement other scopes, though
+               switch ( $registration->scope ) {
+                       case 'singleton':
+
+                               // If an instance has already been created, 
return that
+                               if ( isset( $this->singletons[$typeName] ) ) {
+                                       return $this->singletons[$typeName];
+                               }
+
+                               // No previously created instance? Create one, 
store it, then
+                               // return it
+                               $obj = $this->instantiate( $registration );
+                               $this->singletons[$typeName] = $obj;
+                               return $obj;
+
+                       default:
+                               throw new MWException( 'Unavailable scope set 
for ' .
+                                       $typeName . '.' );
+               }
+       }
+
+       /**
+        * Instantiate a new object as per $registration.
+        *
+        * @param Registration $registration
+        * @return mixed An object of the class defined in $registration
+        */
+       private function instantiate( Registration $registration ) {
+
+               $reflClass = new ReflectionClass( $registration->implClassName 
);
+
+               // If there's no constructor, we can just instantiate and leave
+               if ( !$reflClass->hasMethod( '__construct' ) ) {
+                       return $reflClass->newInstance();
+               }
+
+               // Get the parameters declared on the constructor
+               $constructor = $reflClass->getMethod( '__construct' );
+               $reflParams = $constructor->getParameters();
+
+               // Create an array of the parameters to pass in to the 
constructor by
+               // checking the type hints for each one, and getting the 
corresponding
+               // instances
+               $params = array();
+
+               foreach ( $reflParams as $reflParam ) {
+                       $params[] = $this->get( 
$reflParam->getClass()->getName() );
+               }
+
+               // Instantiate
+               return $reflClass->newInstanceArgs( $params );
+       }
+}
+
+/**
+ * Wrapper for info about a registration; not used elsewhere.
+ */
+class Registration {
+       public $typeName;
+       public $implClassName;
+       public $scope;
+
+       public function __construct( $typeName, $implClassName, $scope ) {
+               $this->typeName = $typeName;
+               $this->implClassName = $implClassName;
+               $this->scope = $scope;
+       }
+}
\ No newline at end of file
diff --git a/tests/phpunit/setup/SetupTest.php 
b/tests/phpunit/setup/SetupTest.php
new file mode 100644
index 0000000..e052ce1
--- /dev/null
+++ b/tests/phpunit/setup/SetupTest.php
@@ -0,0 +1,232 @@
+<?php
+
+namespace Campaigns\PHPUnit\Setup;
+
+use \Campaigns\Setup\Setup;
+
+class SetupTest extends \MediaWikiTestCase {
+
+       /**
+        * Provides a Setup instance for test
+        *
+        * @return Setup
+        */
+       public function setupProvider() {
+
+               $setup = new Setup();
+
+               $setup->register(
+                       'Campaigns\PHPUnit\Setup\IClassWithNoConstructor',
+                       'Campaigns\PHPUnit\Setup\ClassWithNoConstructor',
+                       'singleton'
+               );
+
+               $setup->register(
+                       'Campaigns\PHPUnit\Setup\IClassWithAConstructorParam',
+                       'Campaigns\PHPUnit\Setup\ClassWithAConstructorParam',
+                       'singleton'
+               );
+
+               return array( array( $setup ) );
+       }
+
+       /**
+        * @dataProvider setupProvider
+        */
+       public function
+               testGetProvidesObjectOfCorrectClassForClassWithNoConstructor( 
Setup $setup ) {
+
+               $obj = $setup->get( 
'Campaigns\PHPUnit\Setup\IClassWithNoConstructor' );
+
+               $implClassName = 
'Campaigns\PHPUnit\Setup\ClassWithNoConstructor';
+               $this->assertInstanceOf( $implClassName, $obj );
+       }
+
+       /**
+        * @dataProvider setupProvider
+        */
+       public function
+               
testGetProvidesObjectOfCorrectClassForClassWithAConstructorParam(
+               $setup ) {
+
+               $obj = $setup->get(
+                       'Campaigns\PHPUnit\Setup\IClassWithAConstructorParam' );
+
+               $implClassName = 
'Campaigns\PHPUnit\Setup\ClassWithAConstructorParam';
+               $this->assertInstanceOf( $implClassName, $obj );
+       }
+
+       /**
+        * @dataProvider setupProvider
+        */
+       public function
+               
testObjectOfClassWithAConstructorParamReceivesObjectOfCorrectClassInConstructor(
+               $setup ) {
+
+               $obj1 = $setup->get(
+                       'Campaigns\PHPUnit\Setup\IClassWithAConstructorParam' );
+
+               $obj2 = $obj1->getValueSentInConstructor();
+
+               $obj2ImplClassName = 
'Campaigns\PHPUnit\Setup\ClassWithNoConstructor';
+               $this->assertInstanceOf( $obj2ImplClassName, $obj2 );
+       }
+
+       /**
+        * @dataProvider setupProvider
+        */
+       public function testInSingletonScopeGetAlwaysProvidesTheSameInstance(
+               $setup ) {
+
+               $obj1 = $setup->get(
+                       'Campaigns\PHPUnit\Setup\IClassWithNoConstructor' );
+
+               $obj2 = $setup->get(
+                       'Campaigns\PHPUnit\Setup\IClassWithNoConstructor' );
+
+               $this->assertTrue( $obj1 === $obj2 );
+       }
+
+       /**
+        * @dataProvider setupProvider
+        * @expectedException \MWException
+        * @expectedExceptionMessage No concrete class registered for
+        */
+       public function 
testExceptionThrownWhenObjectOfUnregisteredTypeRequested(
+               $setup ) {
+
+               $setup->get( 'IUnregisteredType' );
+       }
+
+       /**
+        * @expectedException \MWException
+        * @expectedExceptionMessage No concrete class registered for
+        */
+       public function
+               
testExceptionThrownWhenObjectOfClassWithConstructorParamOfUnregisteredTypeRequested()
 {
+
+               $setup = new Setup();
+
+               $setup->register(
+                       
'Campaigns\PHPUnit\Setup\IClassWithAConstructorParamOfUnregisteredType',
+                       
'Campaigns\PHPUnit\Setup\ClassWithAConstructorParamOfUnregisteredType',
+                       'singleton'
+               );
+
+               $setup->get(
+                       
'Campaigns\PHPUnit\Setup\IClassWithAConstructorParamOfUnregisteredType' );
+       }
+
+       /**
+        * @dataProvider setupProvider
+        * @expectedException \MWException
+        * @expectedExceptionMessage is not a subclass of
+        */
+       public function
+               
testExceptionThrownWhenImplementationClassThatIsntASubclassOfTypeIsRegistered(
+               $setup ) {
+
+               $setup->register(
+                       'Campaigns\PHPUnit\Setup\ISomeOtherType',
+                       'Campaigns\PHPUnit\Setup\ClassWithNoConstructor',
+                       'singleton'
+               );
+       }
+
+       /**
+        * @dataProvider setupProvider
+        * @expectedException \MWException
+        * @expectedExceptionMessage already registered
+        */
+       public function testExceptionThrownWhenSameTypeIsRegisteredTwice( 
$setup ) {
+               $setup->register(
+                       'Campaigns\PHPUnit\Setup\IClassWithNoConstructor',
+                       'Campaigns\PHPUnit\Setup\ClassWithNoConstructor',
+                       'singleton'
+               );
+       }
+
+       /**
+        * @expectedException \MWException
+        * @expectedExceptionMessage for a scope
+        */
+       public function
+               
testExceptionThrownWhenRegistrationAttemptedWithUnsupportedScope() {
+
+               $setup = new Setup();
+
+               $setup->register(
+                       'Campaigns\PHPUnit\Setup\IClassWithNoConstructor',
+                       'Campaigns\PHPUnit\Setup\ClassWithNoConstructor',
+                       'unsupportedscope'
+               );
+       }
+
+       /**
+        * @dataProvider setupProvider
+        * @expectedException \MWException
+        * @expectedExceptionMessage with no type hint
+        */
+       public function
+               
testExceptionThrownWhenClassWithConstructorParamWithNoTypeHintRegistered(
+               $setup ) {
+
+               $setup->register(
+                       
'Campaigns\PHPUnit\Setup\IClassWithAConstructorParamWithNoTypeHint',
+                       
'Campaigns\PHPUnit\Setup\ClassWithAConstructorParamWithNoTypeHint',
+                       'singleton'
+               );
+       }
+
+       public function testStaticClearInstanceClearsGlobalInstance() {
+               $instance1 = Setup::getInstance();
+               Setup::clearInstance();
+               $instance2 = Setup::getInstance();
+
+               $this->assertFalse( $instance1 === $instance2 );
+       }
+}
+
+interface IClassWithNoConstructor { }
+
+class ClassWithNoConstructor implements IClassWithNoConstructor{ }
+
+class AnotherClassWithNoConstructor implements IClassWithNoConstructor{ }
+
+interface IClassWithAConstructorParam {
+
+       public function getValueSentInConstructor();
+}
+
+class ClassWithAConstructorParam implements IClassWithAConstructorParam {
+
+       var $value;
+
+       public function __construct( IClassWithNoConstructor $value ) {
+               $this->value = $value;
+       }
+
+       public function getValueSentInConstructor() {
+               return $this->value;
+       }
+}
+
+interface IUnregisteredType { }
+
+interface IClassWithAConstructorParamOfUnregisteredType { }
+
+class ClassWithAConstructorParamOfUnregisteredType
+       implements IClassWithAConstructorParamOfUnregisteredType {
+
+       public function __construct( IUnregisteredType $value ) { }
+}
+
+interface ISomeOtherType { }
+
+interface IClassWithAConstructorParamWithNoTypeHint { }
+
+class ClassWithAConstructorParamWithNoTypeHint
+       implements IClassWithAConstructorParamWithNoTypeHint {
+
+       public function __construct( $value ) { }
+}
\ No newline at end of file

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

Gerrit-MessageType: merged
Gerrit-Change-Id: Ia24d55c43f47b82513a46032434bf81fcde2e18c
Gerrit-PatchSet: 9
Gerrit-Project: mediawiki/extensions/Campaigns
Gerrit-Branch: wip/editorcampaigns
Gerrit-Owner: AndyRussG <[email protected]>
Gerrit-Reviewer: Adamw <[email protected]>
Gerrit-Reviewer: AndyRussG <[email protected]>
Gerrit-Reviewer: Ragesoss <[email protected]>
Gerrit-Reviewer: Swalling <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

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

Reply via email to