Use ObjectFactory to construct ContentHandlers

Changed
 - ContentHandlerFactory with legacy support
 - ContentHandlerFactoryTests
Added
 - MediaWikiIntegrationNoDbTestCase for test without preparing DB
New
 - tests

Bug: T243560
Change-Id: I693dda56af55bd03e48d62a2f1ade42f65a8fac9
This commit is contained in:
ArtBaltai 2020-01-18 23:25:04 +03:00
parent e79b72beeb
commit 272e941b7f
6 changed files with 409 additions and 93 deletions

View file

@ -173,7 +173,10 @@ return [
'ContentHandlerFactory' => function ( MediaWikiServices $services ) : IContentHandlerFactory {
$contentHandlerConfig = $services->getMainConfig()->get( 'ContentHandlers' );
return new ContentHandlerFactory( $contentHandlerConfig );
return new ContentHandlerFactory(
$contentHandlerConfig,
$services->getObjectFactory()
);
},
'ContentLanguage' => function ( MediaWikiServices $services ) : Language {

View file

@ -5,8 +5,11 @@ namespace MediaWiki\Content;
use ContentHandler;
use FatalError;
use Hooks;
use InvalidArgumentException;
use MWException;
use MWUnknownContentModelException;
use UnexpectedValueException;
use Wikimedia\ObjectFactory;
final class ContentHandlerFactory implements IContentHandlerFactory {
@ -20,15 +23,22 @@ final class ContentHandlerFactory implements IContentHandlerFactory {
*/
private $handlersByModel = [];
/**
* @var ObjectFactory
*/
private $objectFactory;
/**
* ContentHandlerFactory constructor.
*
* @param string[]|callable[] $handlerSpecs ClassName for resolve or Callable resolver
* @param ObjectFactory $objectFactory
*
* @see \$wgContentHandlers
*/
public function __construct( array $handlerSpecs ) {
public function __construct( array $handlerSpecs, ObjectFactory $objectFactory ) {
$this->handlerSpecs = $handlerSpecs;
$this->objectFactory = $objectFactory;
}
/**
@ -77,10 +87,18 @@ final class ContentHandlerFactory implements IContentHandlerFactory {
* @throws FatalError
*/
public function getContentModels(): array {
$models = array_keys( $this->handlerSpecs );
Hooks::run( self::HOOK_NAME_GET_CONTENT_MODELS, [ &$models ] );
$modelsFromHook = [];
Hooks::run( self::HOOK_NAME_GET_CONTENT_MODELS, [ &$modelsFromHook ] );
$models = array_merge( // auto-registered from config and MediaServiceWiki or manual
array_keys( $this->handlerSpecs ),
return $models;
// incorrect registered and called: without HOOK_NAME_GET_CONTENT_MODELS
array_keys( $this->handlersByModel ),
// correct registered: as HOOK_NAME_GET_CONTENT_MODELS
$modelsFromHook );
return array_unique( $models );
}
/**
@ -106,21 +124,6 @@ final class ContentHandlerFactory implements IContentHandlerFactory {
return in_array( $modelID, $this->getContentModels(), true );
}
/**
* Register ContentHandler for ModelID
*
* @param string $modelID
* @param ContentHandler $contentHandler
*/
private function registerForModelID( string $modelID, ContentHandler $contentHandler ): void {
wfDebugLog(
__METHOD__,
"Registered handler for {$modelID}: " . get_class( $contentHandler )
. ( !empty( $this->handlersByModel[$modelID] ) ? ' (replace old)' : null )
);
$this->handlersByModel[$modelID] = $contentHandler;
}
/**
* Create ContentHandler for ModelID
*
@ -162,7 +165,7 @@ final class ContentHandlerFactory implements IContentHandlerFactory {
if ( !$contentHandler instanceof ContentHandler ) {
throw new MWException(
"ContentHandler for model {$modelID} must supply a ContentHandler instance, "
. get_class( $contentHandler ) . 'given.'
. get_class( $contentHandler ) . 'given.'
);
}
}
@ -178,16 +181,28 @@ final class ContentHandlerFactory implements IContentHandlerFactory {
private function createContentHandlerFromHandlerSpec(
string $modelID, $handlerSpec
): ContentHandler {
$contentHandler = null;
if ( is_string( $handlerSpec ) ) {
$contentHandler = new $handlerSpec( $modelID );
} elseif ( is_callable( $handlerSpec ) ) {
$contentHandler = call_user_func( $handlerSpec, $modelID );
} else {
throw new MWException( "Wrong Argument HandlerSpec for ModelID: {$modelID}." );
try {
/**
* @var ContentHandler $contentHandler
*/
$contentHandler = $this->objectFactory->createObject( $handlerSpec,
[
'assertClass' => ContentHandler::class,
'allowCallable' => true,
'allowClassName' => true,
'extraArgs' => [ $modelID ],
] );
}
catch ( InvalidArgumentException $e ) {
//legacy support
throw new MWException( "Wrong Argument HandlerSpec for ModelID: {$modelID}. " .
"Error: {$e->getMessage()}" );
}
catch ( UnexpectedValueException $e ) {
//legacy support
throw new MWException( "Wrong HandlerSpec class for ModelID: {$modelID}. " .
"Error: {$e->getMessage()}" );
}
$this->validateContentHandler( $modelID, $contentHandler );
return $contentHandler;

View file

@ -119,6 +119,7 @@ $wgAutoloadClasses += [
"$testDir/phpunit/mocks/content/DummySerializeErrorContentHandler.php",
'ContentHandlerTest' => "$testDir/phpunit/includes/content/ContentHandlerTest.php",
'JavaScriptContentTest' => "$testDir/phpunit/includes/content/JavaScriptContentTest.php",
'ObjectFactoryMakeContentHandlerWithSpecsToTest' => "$testDir/phpunit/includes/content/ObjectFactoryMakeContentHandlerWithSpecsToTest.php",
'TextContentTest' => "$testDir/phpunit/includes/content/TextContentTest.php",
'WikitextContentTest' => "$testDir/phpunit/includes/content/WikitextContentTest.php",

View file

@ -0,0 +1,122 @@
<?php
use MediaWiki\MediaWikiServices;
/**
* @group ContentHandlerFactory
*/
class ObjectFactoryMakeContentHandlerWithSpecsToTest extends MediaWikiTestCase {
private $createObjectOptions = [
'assertClass' => ContentHandler::class,
'allowCallable' => true,
'allowClassName' => true,
];
protected function setUp(): void {
parent::setUp();
$this->setMwGlobals( [
'wgContentHandlers' => [],
] );
}
/**
* @covers \Wikimedia\ObjectFactory::createObject
*
* @param array $handlerSpecs
*
* @dataProvider provideHandlerSpecs
*/
public function testObjectFactoryCreateObject_callWithProvider_same(
array $handlerSpecs
): void {
$objectFactory = MediaWikiServices::getInstance()->getObjectFactory();
foreach ( $handlerSpecs as $modelID => $handlerSpec ) {
$this->assertInstanceOf( DummyContentHandlerForTesting::class,
$objectFactory->createObject( $handlerSpec,
$this->createObjectOptions + [ 'extraArgs' => [ $modelID ] ] ) );
}
}
public function provideHandlerSpecs() {
return [
'typical list' => [
[
'ExistClassName' => DummyContentHandlerForTesting::class,
'ExistCallbackWithExistClassName' => function ( $modelID ) {
return new DummyContentHandlerForTesting( $modelID );
},
],
DummyContentHandlerForTesting::class,
],
];
}
/**
* @covers \Wikimedia\ObjectFactory::createObject
*
* @dataProvider provideHandlerSpecsWithMWException
*
* @param array $handlerSpecs
* @param string $exceptionName
*/
public function testCreateContentHandlerForModelID_callWithProvider_throwsException(
array $handlerSpecs,
string $exceptionName
) {
$objectFactory = MediaWikiServices::getInstance()->getObjectFactory();
foreach ( $handlerSpecs as $modelID => $handlerSpec ) {
try {
$objectFactory->createObject( $handlerSpec,
$this->createObjectOptions + [ 'extraArgs' => [ $modelID ] ] );
$this->assertTrue( false );
}
catch ( \Throwable $exception ) {
$this->assertInstanceOf( $exceptionName,
$exception,
"For test with: '$modelID'" );
}
}
}
public function provideHandlerSpecsWithMWException() {
return [
'UnexpectedValueException with wrong specs result' => [
[
'ExistCallbackWithWrongType' => function () {
return true;
},
'ExistCallbackWithNull' => function () {
return null;
},
'ExistCallbackWithEmptyString' => function () {
return '';
},
'WrongClassName' => self::class,
],
UnexpectedValueException::class,
],
'ObjectFactory with wrong specs' => [
[
'WrongType' => true,
'NullType' => null,
'WrongClassInstanceName' => $this,
'WrongClassNameNotExist' => 'ClassNameNotExist',
'EmptyString' => '',
],
InvalidArgumentException::class,
],
'Error expected' => [
[
'ExistCallbackWithNotExistClassName' => function () {
return \ClassNameNotExist();
},
],
Error::class,
],
];
}
}

View file

@ -11,15 +11,6 @@ class RegistrationContentHandlerFactoryToMediaWikiServicesTest extends MediaWiki
parent::setUp();
$this->setMwGlobals( [
'wgExtraNamespaces' => [
12312 => 'Dummy',
12313 => 'Dummy_talk',
],
// The below tests assume that namespaces not mentioned here (Help, User, MediaWiki, ..)
// default to CONTENT_MODEL_WIKITEXT.
'wgNamespaceContentModels' => [
12312 => 'testing',
],
'wgContentHandlers' => [
CONTENT_MODEL_WIKITEXT => WikitextContentHandler::class,
CONTENT_MODEL_JAVASCRIPT => JavaScriptContentHandler::class,
@ -33,13 +24,11 @@ class RegistrationContentHandlerFactoryToMediaWikiServicesTest extends MediaWiki
],
] );
// Reset LinkCache
MediaWikiServices::getInstance()->resetServiceForTesting( 'LinkCache' );
MediaWikiServices::getInstance()->resetServiceForTesting( 'ContentHandlerFactory' );
}
protected function tearDown(): void {
// Reset LinkCache
MediaWikiServices::getInstance()->resetServiceForTesting( 'LinkCache' );
MediaWikiServices::getInstance()->resetServiceForTesting( 'ContentHandlerFactory' );
parent::tearDown();
}

View file

@ -1,18 +1,23 @@
<?php
use MediaWiki\Content\ContentHandlerFactory;
use Wikimedia\ObjectFactory;
/**
* @group ContentHandlerFactory
*/
class ContentHandlerFactoryTest extends MediaWikiUnitTestCase {
public function provideHandlerSpecs() {
return [
'typical ' => [
'typical list' => [
[
'ExistClassName' => DummyContentHandlerForTesting::class,
'ExistCallbackWithExistClassName' => function ( $modelID ) {
return new DummyContentHandlerForTesting( $modelID );
},
],
DummyContentHandlerForTesting::class,
],
];
}
@ -22,23 +27,81 @@ class ContentHandlerFactoryTest extends MediaWikiUnitTestCase {
* @covers \MediaWiki\Content\ContentHandlerFactory::createForModelID
* @covers \MediaWiki\Content\ContentHandlerFactory::createContentHandlerFromHandlerSpec
* @covers \MediaWiki\Content\ContentHandlerFactory::validateContentHandler
* @covers \MediaWiki\Content\ContentHandlerFactory::__construct
*
* @param array $handlerSpecs
* @param string $contentHandlerClass
*
* @todo test ContentHandlerFactory::createContentHandlerFromHook
*
* @throws MWException
* @throws MWUnknownContentModelException
* @dataProvider provideHandlerSpecs $handlerSpecs
*/
public function testGetContentHandler_callWithProvider_same( array $handlerSpecs ) {
$registry = new ContentHandlerFactory( $handlerSpecs );
public function testGetContentHandler_callWithProvider_same(
array $handlerSpecs, string $contentHandlerClass
): void {
$contentHandlerExpected = new $contentHandlerClass( 'dummy' );
$objectFactory = $this->createMockObjectFactory();
$factory = new ContentHandlerFactory( $handlerSpecs, $objectFactory );
$i = 0;
foreach ( $handlerSpecs as $modelID => $handlerSpec ) {
$contentHandler = $registry->getContentHandler( $modelID );
$this->assertInstanceOf( DummyContentHandlerForTesting::class, $contentHandler );
$this->assertSame( $modelID, $contentHandler->getModelID() );
$objectFactory
->expects( $this->at( $i++ ) )
->method( 'createObject' )
->with( $handlerSpec,
[
'assertClass' => ContentHandler::class,
'allowCallable' => true,
'allowClassName' => true,
'extraArgs' => [ $modelID ],
] )
->willReturn( $contentHandlerExpected );
}
foreach ( $handlerSpecs as $modelID => $handlerSpec ) {
$this->assertSame( $contentHandlerExpected, $factory->getContentHandler( $modelID ) );
}
}
public function provideHandlerSpecsWithMWException() {
/**
* @covers \MediaWiki\Content\ContentHandlerFactory::getContentHandler
* @covers \MediaWiki\Content\ContentHandlerFactory::createForModelID
* @covers \MediaWiki\Content\ContentHandlerFactory::createContentHandlerFromHook
* @covers \MediaWiki\Content\ContentHandlerFactory::validateContentHandler
*
* @param array $handlerSpecs
* @param string $contentHandlerClass
*
* @throws MWException
* @throws MWUnknownContentModelException
* @dataProvider provideHandlerSpecs $handlerSpecs
*/
public function testGetContentHandler_hookWithProvider_same(
array $handlerSpecs,
string $contentHandlerClass
): void {
$contentHandlerExpected = new $contentHandlerClass( 'dummy' );
$factory = new ContentHandlerFactory( [], $this->createMockObjectFactory() );
foreach ( $handlerSpecs as $modelID => $handlerSpec ) {
$this->assertFalse( $factory->isDefinedModel( $modelID ) );
$contentHandler = null;
$this->setTemporaryHook( 'ContentHandlerForModelID',
function ( $handlerSpec, &$contentHandler ) use (
$contentHandlerExpected
) {
$contentHandler = $contentHandlerExpected;
return true;
} );
$contentHandler = $factory->getContentHandler( $modelID );
$this->assertSame( $contentHandlerExpected, $contentHandler, $modelID );
$this->assertTrue( $factory->isDefinedModel( $modelID ), $modelID );
}
}
public function provideHandlerSpecsWithMWException(): array {
return [
'MWException expected' => [
[
@ -80,21 +143,27 @@ class ContentHandlerFactoryTest extends MediaWikiUnitTestCase {
* @dataProvider provideHandlerSpecsWithMWException
*
* @param array $handlerSpecs
* @param string $exceptionName
* @param string $exceptionClassName
*/
public function testCreateContentHandlerForModelID_callWithProvider_throwsException(
array $handlerSpecs, string $exceptionName
) {
$registry = new ContentHandlerFactory( $handlerSpecs );
array $handlerSpecs,
string $exceptionClassName
): void {
/**
* @var Exception $exceptionExpected
*/
$objectFactory = $this->createMockObjectFactory();
$objectFactory->method( 'createObject' )
->willThrowException( $this->createMock( $exceptionClassName ) );
$factory = new ContentHandlerFactory( $handlerSpecs, $objectFactory );
foreach ( $handlerSpecs as $modelID => $handlerSpec ) {
try {
$registry->getContentHandler( $modelID );
$factory->getContentHandler( $modelID );
$this->assertTrue( false );
}
catch ( \Throwable $exception ) {
$this->assertInstanceOf( $exceptionName, $exception,
"$modelID get: " . get_class( $exception ) . " but expect: $exceptionName" );
$this->assertInstanceOf( $exceptionClassName, $exception );
}
}
}
@ -107,10 +176,12 @@ class ContentHandlerFactoryTest extends MediaWikiUnitTestCase {
public function testCreateContentHandlerForModelID_callNotExist_throwMWUCMException() {
$this->expectException( MWUnknownContentModelException::class );
( new ContentHandlerFactory( [] ) )->getContentHandler( 'ModelNameNotExist' );
( new ContentHandlerFactory( [], $this->createMockObjectFactory() ) )
->getContentHandler( 'ModelNameNotExist' );
}
/**
* @covers \MediaWiki\Content\ContentHandlerFactory::defineContentHandler
* @covers \MediaWiki\Content\ContentHandlerFactory::getContentHandler
* @covers \MediaWiki\Content\ContentHandlerFactory::createForModelID
* @covers \MediaWiki\Content\ContentHandlerFactory::createContentHandlerFromHandlerSpec
@ -118,48 +189,133 @@ class ContentHandlerFactoryTest extends MediaWikiUnitTestCase {
* @covers \MediaWiki\Content\ContentHandlerFactory::isDefinedModel
*/
public function testDefineContentHandler_flow_throwsException() {
$registry = new ContentHandlerFactory( [] );
$this->assertFalse( $registry->isDefinedModel( 'define test' ) );
$objectFactory = $this->createMockObjectFactory();
$objectFactory
->method( 'createObject' )
->willReturn( $this->createMock( DummyContentHandlerForTesting::class ) );
$factory = new ContentHandlerFactory( [], $objectFactory );
$this->assertFalse( $factory->isDefinedModel( 'define test' ) );
$registry->defineContentHandler( 'define test', DummyContentHandlerForTesting::class );
$this->assertTrue( $registry->isDefinedModel( 'define test' ) );
$factory->defineContentHandler( 'define test', DummyContentHandlerForTesting::class );
$this->assertTrue( $factory->isDefinedModel( 'define test' ) );
$this->assertInstanceOf(
DummyContentHandlerForTesting::class,
$registry->getContentHandler( 'define test' )
$factory->getContentHandler( 'define test' )
);
}
/**
* @covers \MediaWiki\Content\ContentHandlerFactory::getContentModels
*
* @dataProvider provideValidDummySpecList
*
* @param string $name1
* @param string $name2
* @param string $name3
* @param string $name4
* @throws FatalError
* @throws MWException
*/
public function testGetContentModels_flow_same() {
$registry = new ContentHandlerFactory( [
'mock name 1' => DummyContentHandlerForTesting::class,
'mock name 0' => DummyContentHandlerForTesting::class,
] );
public function testGetContentModels_flow_same(
string $name1, string $name2, string $name3, string $name4
): void {
$factory = new ContentHandlerFactory( [
$name1 => DummyContentHandlerForTesting::class,
$name2 => DummyContentHandlerForTesting::class,
], $this->createMockObjectFactory() );
$this->assertArrayEquals(
[ $name1, $name2, ],
$factory->getContentModels() );
$this->assertArrayEquals( [
'mock name 1',
'mock name 0',
], $registry->getContentModels() );
$factory->defineContentHandler(
$name3,
function () {
}
);
$registry->defineContentHandler( 'some new name', function () {
} );
$this->assertArrayEquals(
[ $name1, $name2, $name3, ],
$factory->getContentModels()
);
$this->assertArrayEquals( [
'mock name 1',
'mock name 0',
'some new name',
], $registry->getContentModels() );
$this->setTemporaryHook( 'GetContentModels',
function ( &$models ) use ( $name4 ) {
$models[] = $name4;
} );
$this->assertArrayEquals(
[ $name1, $name2, $name3, $name4, ],
$factory->getContentModels()
);
}
/**
* @covers \MediaWiki\Content\ContentHandlerFactory::isDefinedModel
* @covers \MediaWiki\Content\ContentHandlerFactory::createContentHandlerFromHook
* @dataProvider provideValidDummySpecList
*
* @param string $name1
* @param string $name2
* @param string $name3
* @param string $name4
* @throws MWException
*/
public function testIsDefinedModel_flow_same(
string $name1, string $name2, string $name3, string $name4
): void {
$factory = new ContentHandlerFactory( [
$name1 => DummyContentHandlerForTesting::class,
$name2 => DummyContentHandlerForTesting::class,
], $this->createMockObjectFactory() );
$this->assertTrue( $factory->isDefinedModel( $name1 ) );
$this->assertTrue( $factory->isDefinedModel( $name2 ) );
$this->assertFalse( $factory->isDefinedModel( $name3 ) );
$this->assertFalse( $factory->isDefinedModel( $name4 ) );
$this->assertFalse( $factory->isDefinedModel( 'not exist name' ) );
$factory->defineContentHandler(
$name3,
function () {
}
);
$this->assertTrue( $factory->isDefinedModel( $name1 ) );
$this->assertTrue( $factory->isDefinedModel( $name2 ) );
$this->assertTrue( $factory->isDefinedModel( $name3 ) );
$this->assertFalse( $factory->isDefinedModel( $name4 ) );
$this->assertFalse( $factory->isDefinedModel( 'not exist name' ) );
$this->setTemporaryHook(
'GetContentModels',
function ( &$models ) use ( $name4 ) {
$models[] = $name4;
} );
$this->assertTrue( $factory->isDefinedModel( $name1 ) );
$this->assertTrue( $factory->isDefinedModel( $name2 ) );
$this->assertTrue( $factory->isDefinedModel( $name3 ) );
$this->assertTrue( $factory->isDefinedModel( $name4 ) );
$this->assertFalse( $factory->isDefinedModel( 'not exist name' ) );
}
public function provideValidDummySpecList() {
return [
'1-0-3' => [
'mock name 1',
'mock name 0',
'mock name 3',
'mock name 4',
],
];
}
/**
* @covers \MediaWiki\Content\ContentHandlerFactory::getContentModels
*/
public function testGetContentModels_empty_empty() {
$registry = new ContentHandlerFactory( [] );
$factory = new ContentHandlerFactory( [], $this->createMockObjectFactory() );
$this->assertArrayEquals( [], $registry->getContentModels() );
$this->assertArrayEquals( [], $factory->getContentModels() );
}
/**
@ -167,28 +323,58 @@ class ContentHandlerFactoryTest extends MediaWikiUnitTestCase {
* @covers \MediaWiki\Content\ContentHandlerFactory::defineContentHandler
*/
public function testGetAllContentFormats_flow_same() {
$registry = new ContentHandlerFactory( [
$contentHandler1 = $this->createMock( DummyContentHandlerForTesting::class );
$contentHandler1->method( 'getSupportedFormats' )->willReturn( [ 'format 1' ] );
$contentHandler2 = $this->createMock( DummyContentHandlerForTesting::class );
$contentHandler2->method( 'getSupportedFormats' )->willReturn( [ 'format 0' ] );
$contentHandler3 = $this->createMock( DummyContentHandlerForTesting::class );
$contentHandler3->method( 'getSupportedFormats' )->willReturn( [ 'format 3' ] );
$objectFactory = $this->createMockObjectFactory();
$objectFactory->expects( $this->at( 0 ) )
->method( 'createObject' )
->willReturn( $contentHandler1 );
$objectFactory->expects( $this->at( 1 ) )
->method( 'createObject' )
->willReturn( $contentHandler2 );
$objectFactory->expects( $this->at( 2 ) )
->method( 'createObject' )
->willReturn( $contentHandler3 );
$factory = new ContentHandlerFactory( [
'mock name 1' => function () {
return new DummyContentHandlerForTesting( 'mock 1', [ 'format 1' ] );
//return new DummyContentHandlerForTesting( 'mock 1', [ 'format 1' ] );
},
'mock name 2' => function () {
return new DummyContentHandlerForTesting( 'mock 0', [ 'format 0' ] );
//return new DummyContentHandlerForTesting( 'mock 0', [ 'format 0' ] );
},
] );
], $objectFactory );
$this->assertArrayEquals( [
'format 1',
'format 0',
], $registry->getAllContentFormats() );
],
$factory->getAllContentFormats() );
$registry->defineContentHandler( 'some new name', function () {
return new DummyContentHandlerForTesting( 'mock defined', [ 'format defined' ] );
} );
$factory->defineContentHandler( 'some new name',
function () {
//return new DummyContentHandlerForTesting( 'mock defined', [ 'format defined' ] );
} );
$this->assertArrayEquals( [
'format 1',
'format 0',
'format defined',
], $registry->getAllContentFormats() );
'format 3',
],
$factory->getAllContentFormats() );
}
/**
* @return ObjectFactory|\PHPUnit\Framework\MockObject\MockObject
*/
private function createMockObjectFactory(): ObjectFactory {
return $this->createMock( ObjectFactory::class );
}
}