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