Allow injecting a message localizer into Status

The coupling of Status with the global request context for
Message object creation is a common pain point in unit tests and
in no-session code. As a short term solution (until Status is
properly deprecated) allow injecting a different localizer.

Also refactor the code a bit to get rid if the explicit need for
the context language (which is already implicit in the localizer).

Change-Id: I82a2e4a83743546a934fb938b94e877a2471a3d2
This commit is contained in:
Gergő Tisza 2019-10-30 02:51:03 -07:00
parent 8074fc63e8
commit dd01c6dd64
No known key found for this signature in database
GPG key ID: C34FEC97E6257F96
3 changed files with 97 additions and 42 deletions

View file

@ -20,8 +20,6 @@
* @file
*/
use MediaWiki\MediaWikiServices;
/**
* Generic operation result class
* Has warning/error list, boolean status and arbitrary value
@ -43,6 +41,9 @@ class Status extends StatusValue {
/** @var callable|false */
public $cleanCallback = false;
/** @var MessageLocalizer|null */
protected $messageLocalizer;
/**
* Succinct helper method to wrap a StatusValue
*
@ -77,7 +78,7 @@ class Status extends StatusValue {
* @return mixed
* @throws RuntimeException
*/
function __get( $name ) {
public function __get( $name ) {
if ( $name === 'ok' ) {
return $this->isOK();
}
@ -96,7 +97,7 @@ class Status extends StatusValue {
* @param mixed $value
* @throws RuntimeException
*/
function __set( $name, $value ) {
public function __set( $name, $value ) {
if ( $name === 'ok' ) {
$this->setOK( $value );
} elseif ( !property_exists( $this, $name ) ) {
@ -107,6 +108,20 @@ class Status extends StatusValue {
}
}
/**
* Makes this Status object use the given localizer instead of the global one.
* If it is an IContextSource or a ResourceLoaderContext, it will also be used to
* determine the interface language.
* @note This setting does not survive serialization. That's usually for the best
* (there's no guarantee we'll still have the same localization settings after
* unserialization); it is the caller's responsibility to set the localizer again
* if needed.
* @param MessageLocalizer $messageLocalizer
*/
public function setMessageLocalizer( MessageLocalizer $messageLocalizer ) {
$this->messageLocalizer = $messageLocalizer;
}
/**
* Splits this Status object into two new Status objects, one which contains only
* the error messages, and one that contains the warnings, only. The returned array is
@ -117,10 +132,17 @@ class Status extends StatusValue {
* ]
*
* @return Status[]
* @suppress PhanUndeclaredProperty Status vs StatusValue
*/
public function splitByErrorType() {
list( $errorsOnlyStatus, $warningsOnlyStatus ) = parent::splitByErrorType();
// phan/phan#2133?
'@phan-var Status $errorsOnlyStatus';
'@phan-var Status $warningsOnlyStatus';
if ( $this->messageLocalizer ) {
$errorsOnlyStatus->setMessageLocalizer( $this->messageLocalizer );
$warningsOnlyStatus->setMessageLocalizer( $this->messageLocalizer );
}
$errorsOnlyStatus->cleanCallback =
$warningsOnlyStatus->cleanCallback = $this->cleanCallback;
@ -151,33 +173,16 @@ class Status extends StatusValue {
return $cleanParams;
}
/**
* @param string|Language|null|StubUserLang $lang Language to use for processing
* messages, or null to default to the user language.
* @return Language|StubUserLang
*/
protected function languageFromParam( $lang ) {
if ( $lang === null ) {
return RequestContext::getMain()->getLanguage();
}
if ( $lang instanceof Language || $lang instanceof StubUserLang ) {
return $lang;
}
return MediaWikiServices::getInstance()->getLanguageFactory()->getLanguage( $lang );
}
/**
* Get the error list as a wikitext formatted list
*
* @param string|bool $shortContext A short enclosing context message name, to
* be used when there is a single error
* @param string|bool $longContext A long enclosing context message name, for a list
* @param string|Language|null|StubUserLang $lang Language to use for processing messages
* @param string|Language|StubUserLang|null $lang Language to use for processing messages
* @return string
*/
public function getWikiText( $shortContext = false, $longContext = false, $lang = null ) {
$lang = $this->languageFromParam( $lang );
$rawErrors = $this->getErrors();
if ( count( $rawErrors ) === 0 ) {
if ( $this->isOK() ) {
@ -192,9 +197,9 @@ class Status extends StatusValue {
if ( count( $rawErrors ) === 1 ) {
$s = $this->getErrorMessage( $rawErrors[0], $lang )->plain();
if ( $shortContext ) {
$s = wfMessage( $shortContext, $s )->inLanguage( $lang )->plain();
$s = $this->msgInLang( $shortContext, $lang, $s )->plain();
} elseif ( $longContext ) {
$s = wfMessage( $longContext, "* $s\n" )->inLanguage( $lang )->plain();
$s = $this->msgInLang( $longContext, $lang, "* $s\n" )->plain();
}
} else {
$errors = $this->getErrorMessageArray( $rawErrors, $lang );
@ -203,9 +208,9 @@ class Status extends StatusValue {
}
$s = '* ' . implode( "\n* ", $errors ) . "\n";
if ( $longContext ) {
$s = wfMessage( $longContext, $s )->inLanguage( $lang )->plain();
$s = $this->msgInLang( $longContext, $lang, $s )->plain();
} elseif ( $shortContext ) {
$s = wfMessage( $shortContext, "\n$s\n" )->inLanguage( $lang )->plain();
$s = $this->msgInLang( $shortContext, $lang, "\n$s\n" )->plain();
}
}
return $s;
@ -228,12 +233,10 @@ class Status extends StatusValue {
*
* @param string|string[]|bool $shortContext A message name or an array of message names.
* @param string|string[]|bool $longContext A message name or an array of message names.
* @param string|Language|null $lang Language to use for processing messages
* @param string|Language|StubUserLang|null $lang Language to use for processing messages
* @return Message
*/
public function getMessage( $shortContext = false, $longContext = false, $lang = null ) {
$lang = $this->languageFromParam( $lang );
$rawErrors = $this->getErrors();
if ( count( $rawErrors ) === 0 ) {
if ( $this->isOK() ) {
@ -248,11 +251,11 @@ class Status extends StatusValue {
if ( count( $rawErrors ) === 1 ) {
$s = $this->getErrorMessage( $rawErrors[0], $lang );
if ( $shortContext ) {
$s = wfMessage( $shortContext, $s )->inLanguage( $lang );
$s = $this->msgInLang( $shortContext, $lang, $s );
} elseif ( $longContext ) {
$wrapper = new RawMessage( "* \$1\n" );
$wrapper->params( $s )->parse();
$s = wfMessage( $longContext, $wrapper )->inLanguage( $lang );
$s = $this->msgInLang( $longContext, $lang, $wrapper );
}
} else {
$msgs = $this->getErrorMessageArray( $rawErrors, $lang );
@ -262,11 +265,11 @@ class Status extends StatusValue {
$s->params( $msgs )->parse();
if ( $longContext ) {
$s = wfMessage( $longContext, $s )->inLanguage( $lang );
$s = $this->msgInLang( $longContext, $lang, $s );
} elseif ( $shortContext ) {
$wrapper = new RawMessage( "\n\$1\n", [ $s ] );
$wrapper->parse();
$s = wfMessage( $shortContext, $wrapper )->inLanguage( $lang );
$s = $this->msgInLang( $shortContext, $lang, $wrapper );
}
}
@ -288,20 +291,22 @@ class Status extends StatusValue {
if ( isset( $error['message'] ) && $error['message'] instanceof Message ) {
$msg = $error['message'];
} elseif ( isset( $error['message'] ) && isset( $error['params'] ) ) {
$msg = wfMessage( $error['message'],
$msg = $this->msg( $error['message'],
array_map( 'wfEscapeWikiText', $this->cleanParams( $error['params'] ) ) );
} else {
$msgName = array_shift( $error );
$msg = wfMessage( $msgName,
$msg = $this->msg( $msgName,
array_map( 'wfEscapeWikiText', $this->cleanParams( $error ) ) );
}
} elseif ( is_string( $error ) ) {
$msg = wfMessage( $error );
$msg = $this->msg( $error );
} else {
throw new UnexpectedValueException( 'Got ' . get_class( $error ) . ' for key.' );
}
$msg->inLanguage( $this->languageFromParam( $lang ) );
if ( $lang ) {
$msg->inLanguage( $lang );
}
return $msg;
}
@ -314,7 +319,6 @@ class Status extends StatusValue {
* @return string
*/
public function getHTML( $shortContext = false, $longContext = false, $lang = null ) {
$lang = $this->languageFromParam( $lang );
$text = $this->getWikiText( $shortContext, $longContext, $lang );
$out = MessageCache::singleton()->parse( $text, null, true, true, $lang );
return $out instanceof ParserOutput
@ -329,7 +333,6 @@ class Status extends StatusValue {
* @return Message[]
*/
protected function getErrorMessageArray( $errors, $lang = null ) {
$lang = $this->languageFromParam( $lang );
return array_map( function ( $e ) use ( $lang ) {
return $this->getErrorMessage( $e, $lang );
}, $errors );
@ -389,11 +392,13 @@ class Status extends StatusValue {
/**
* Don't save the callback when serializing, because Closures can't be
* serialized and we're going to clear it in __wakeup anyway.
* Don't save the localizer, because it can be pretty much anything. Restoring it is
* the caller's responsibility (otherwise it will just fall back to the global request context).
* @return array
*/
function __sleep() {
$keys = array_keys( get_object_vars( $this ) );
return array_diff( $keys, [ 'cleanCallback' ] );
return array_diff( $keys, [ 'cleanCallback', 'messageLocalizer' ] );
}
/**
@ -401,5 +406,33 @@ class Status extends StatusValue {
*/
function __wakeup() {
$this->cleanCallback = false;
$this->messageLocalizer = null;
}
/**
* @param string|MessageSpecifier $key
* @param string|string[] ...$params
* @return Message
*/
private function msg( $key, ...$params ) {
if ( $this->messageLocalizer ) {
return $this->messageLocalizer->msg( $key, ...$params );
} else {
return wfMessage( $key, ...$params );
}
}
/**
* @param string|MessageSpecifier $key
* @param string|Language|StubUserLang|null $lang
* @param string|string[] ...$params
* @return Message
*/
private function msgInLang( $key, $lang, ...$params ) {
$msg = $this->msg( $key, ...$params );
if ( $lang ) {
$msg->inLanguage( $lang );
}
return $msg;
}
}

View file

@ -728,7 +728,7 @@ class Message implements MessageSpecifier, Serializable {
* turned off.
*
* @since 1.17
* @param Language|string $lang Language code or Language object.
* @param Language|StubUserLang|string $lang Language code or Language object.
* @return Message $this
* @throws MWException
*/

View file

@ -739,4 +739,26 @@ class StatusTest extends MediaWikiLangTestCase {
$this->assertTrue( $sw->isOK() );
}
/**
* @covers Status::setMessageLocalizer
*/
public function testSetContext() {
$status = Status::newFatal( 'foo' );
$status->fatal( 'bar' );
$messageLocalizer = $this->getMockBuilder( MessageLocalizer::class )
->setMethods( [ 'msg' ] )
->getMockForAbstractClass();
$messageLocalizer->expects( $this->atLeastOnce() )
->method( 'msg' )
->willReturnCallback( function ( $key ) {
return new RawMessage( $key );
} );
/** @var MessageLocalizer $messageLocalizer */
$status->setMessageLocalizer( $messageLocalizer );
$status->getWikiText();
$status->getWikiText( null, null, 'en' );
$status->getWikiText( 'wrap-short', 'wrap-long' );
}
}