PHP 面向對象及Mediawiki 框架分析(一)

此文是一JAVA哥大神寫的,雖然他不懂PHP。我這人PHP半桶水,面向對象更是半桶水都沒有,此文本來是爲了讓我理解MediaWiki的運行機制的,愣是用他的JAVA的面向對象知識,對Mediawiki程序源碼進行了一個總體剖析,膜拜!此文涉及諸多設計模式方面的知識,想搞MediaWiki的人,仍是蠻有參考價值的。
本文除了原初要解決如何在第三方系統調用mediawiki 的圖片文件資源外,也探尋了 Mediawiki 的GUI(Graphical User Interface)模式等。因爲Mediawiki 的大部分頁面是以SpecialPage 爲引導,因此我以SpecialPages 爲切入點進入分析Mediawiki 的架構。php

SpecialPages 是全部 special pages 的父類。有關SpecialPage 父類的內在結構,有幾個重要部分分解:
一、SpecialPage 父類中有mContext 上下文接口,供SpecialPage 的相關子類調用。接口含有getOutput()\getConfig()等方法。html

這裏首先來分析下SpecialPage 子類與分類的繼承關係。以及子類的實例化。
繼承關係:編程

衆多的SpeicalPage 的子類,系統使用工廠模式管理SpecialPage 以及其餘父類的衆多子類的實例( Page ) , 關於怎麼建立
SpecialPageFactory 實例,這裏你能夠看源代碼,咱們重點來講下工廠模式,以SpecialListFiles 爲例:設計模式

SpecialPageFactory 代碼:
/Specialpage/SpecialPageFactory.php
private static $list = array(
// Media reports and uploads
'Listfiles' => 'SpecialListFiles',
'Filepath' => 'SpecialFilepath',
'MIMEsearch' => 'MIMEsearchPage',
'FileDuplicateSearch' => 'FileDuplicateSearchPage',
'Upload' => 'SpecialUpload',
'UploadStash' => 'SpecialUploadStash',
'ListDuplicatedFiles' => 'ListDuplicatedFilesPage',

再看獲取'Listfiles' => 'SpecialListFiles'頁面的getPage()方法:服務器

/Specialpage/SpecialPageFactory.php
/**
* Find the object with a given name and return it (or NULL)
*
* @param string $name Special page name, may be localised and/or
an alias
* @return SpecialPage|null SpecialPage object or null if the
page doesn't exist
*/
public static function getPage( $name ) {
list( $realName, /*...*/ ) = self::resolveAlias( $name );
if ( property_exists( self::getList(), $realName ) ) {
$rec = self::getList()->$realName;
if ( is_string( $rec ) ) {
$className = $rec;
return new $className;
} elseif ( is_array( $rec ) ) {
// @deprecated, officially since 1.18, unofficially
since forever
wfDebug( "Array syntax for \$wgSpecialPages is
deprecated, " .
"define a subclass of SpecialPage instead." );
$className = array_shift( $rec );
self::getList()->$realName =
MWFunction::newObj( $className, $rec );
}
return self::getList()->$realName;
} else {
return null;
}
}

關鍵指令:
self::getList()->$realName = MWFunction::newObj( $className, $rec );
請看UML圖:架構

小問題:此刻,建立的SpecialListFiles 對象由誰持有?此處不表。
咱們再來看SpecialListFiles 與SpecialPage。
上下文mContext 實例對象是如何建立的?在父類SpecialPage 中mContext 是一個接口的Reference(Java/C++這麼說),請看圖:框架

在建立SpeicalFileLists 對象的過程當中,父類完成了上下文接口的實現(RequestContext),建立過程:
代碼:ide

/Specialpage/SpecialPage.php
/**
* Gets the context(這當中省略 that)this SpecialPage is executed
in
*
* @return IContextSource|RequestContext
* @since 1.18
*/
public function getContext() {
if ( $this->mContext instanceof IContextSource ) {
return $this->mContext;
} else {
wfDebug( __METHOD__ . " called and \$mContext is null. " .
"Return RequestContext::getMain(); for sanity\n" );
return RequestContext::getMain();
}
}

  請看圖:ui

在面向對象編程中,既然RequestContext 是類,不是對象,爲何還能夠調用其getMain()方法?解釋,getMain()方法是一個static靜態方法。在getMain()方法中:
代碼:this

/includes/context/RequestContext.php
/** Static methods **/
/**
* Get the RequestContext object associated with the main request
*
* @return RequestContext
*/
public static function getMain() {
static $instance = null;
if ( $instance === null ) {
$instance = new self;
}
return $instance;
}

有關SpeicalPage 的相關子類如何向服務器發送請求:這裏的過程其實是將RequestContext 對象的Reference 交給getContext()方法,實際上就是SpecialPage 的mContext。如此,透過上下文mContext Reference 調用getRequest():
getRequest()代碼:

/includes/context/RequestContext.php
/**
* Get the WebRequest object
*
* @return WebRequest
*/
public function getRequest() {
if ( $this->request === null ) {
global $wgRequest; # fallback to $wg till we can improve this
$this->request = $wgRequest;
}
return $this->request;
}

以SpecialListFile 爲例,談下顯示圖像清單的程序。
在SpecialListFile 的圖像顯示界面,mediawiki 又交給了一個pager 的實現類ImageListPager 完成。SpecialListFile 建立一個ImageListPager 對象,完成相關的list 表格和圖像的Thumb(拇指)圖像的顯示。
請看建立ImageListPager 對象的UML 圖示:

ImageListPager 對象處理圖像數據方法是formatValue()。
formatValue()代碼:

return $this->msg( 'listfiles-latestversion-' .
$value );
default:
throw new MWException( "Unknown field '$field'" );
}
}</code>
Thumb pic 對象的操做(transform 及 toHtml)指令代碼:
<code>includes/specials/SpecialListfiles.php
case 'thumb':
$opt = array( 'time' =>
$this->mCurrentRow->img_timestamp );
$file =
RepoGroup::singleton()->getLocalRepo()->findFile( $value, $opt );
// If statement for paranoia
if ( $file ) {
$thumb = $file->transform( array( 'width' => 180,
'height' => 360 ) );
return $thumb->toHtml( array( 'desc-link' =>
true ) );
} else {
return htmlspecialchars( $value );
}

獲取文件(也多是尋找到現有存在/…/…/的文件):
$file = RepoGroup::singleton()->getLocalRepo()->findFile( $value,$opt );

findFile 方法返回true/false,findFile()代碼:

includes/filerepo/RepoGroup.php
/**
* Search repositories for an image.
* You can also use wfFindFile() to do this.
*
* @param $title Title|string Title object or string
* @param array $options Associative array of options:
* time: requested time for an archived image, or false for
the
* current version. An image object will be returned
which was
* created at the specified time.
* ignoreRedirect: If true, do not follow file redirects
* private: If true, return restricted (deleted) files if the
current
* user is allowed to view them. Otherwise, such files
will not
* be found.
* bypassCache: If true, do not use the process-local cache of
File objects
* @return File|bool False if title is not found
*/
function findFile( $title, $options = array() ) {
if ( !is_array( $options ) ) {
// MW 1.15 compat
$options = array( 'time' => $options );
}
if ( !$this->reposInitialised ) {
$this->initialiseRepos();
}
$title = File::normalizeTitle( $title );
if ( !$title ) {
return false;
}
# Check the cache
if ( empty( $options['ignoreRedirect'] )
&& empty( $options['private'] )
&& empty( $options['bypassCache'] )
) {
$time = isset( $options['time'] ) ? $options['time'] : '';
$dbkey = $title->getDBkey();
if ( $this->cache->has( $dbkey, $time, 60 ) ) {
return $this->cache->get( $dbkey, $time );
}
$useCache = true;
} else {
$useCache = false;
}
# Check the local repo
$image = $this->localRepo->findFile( $title, $options );
# Check the foreign repos
if ( !$image ) {
foreach ( $this->foreignRepos as $repo ) {
$image = $repo->findFile( $title, $options );
if ( $image ) {
break;
}
}
}
$image = $image ? $image : false; // type sanity
# Cache file existence or non-existence
if ( $useCache && ( !$image || $image->isCacheable() ) ) {
$this->cache->set( $dbkey, $time, $image );
}
return $image;
}

Image 對象thumb 輸出:
File 類的圖像處理方法 transform():

/includes/filerepo/file/File.php
/**
* Transform a media file
*
* @param array $params an associative array of handler-specific
parameters.
* Typical keys are width, height and page.
* @param int $flags A bitfield, may contain self::RENDER_NOW to force
rendering
* @return MediaTransformOutput|bool False on failure
*/
function transform( $params, $flags = 0 ) {
global $wgUseSquid, $wgIgnoreImageErrors, $wgThumbnailEpoch;
wfProfileIn( __METHOD__ );
do {
if ( !$this->canRender() ) {
$thumb = $this->iconThumb();
break; // not a bitmap or renderable image, don't try
}
// Get the descriptionUrl to embed it as comment into the
thumbnail. Bug 19791.
$descriptionUrl = $this->getDescriptionUrl();
if ( $descriptionUrl ) {
$params['descriptionUrl'] =
wfExpandUrl( $descriptionUrl, PROTO_CANONICAL );
}
$handler = $this->getHandler();
$script = $this->getTransformScript();
if ( $script && !( $flags & self::RENDER_NOW ) ) {
// Use a script to transform on client request, if possible
$thumb = $handler->getScriptedTransform( $this, $script,
$params );
if ( $thumb ) {
break;
}
}
$normalisedParams = $params;
$handler->normaliseParams( $this, $normalisedParams );
$thumbName = $this->thumbName( $normalisedParams );
$thumbUrl = $this->getThumbUrl( $thumbName );
$thumbPath = $this->getThumbPath( $thumbName ); // final thumb
path
if ( $this->repo ) {
// Defer rendering if a 404 handler is set up...
if ( $this->repo->canTransformVia404() && !( $flags &
self::RENDER_NOW ) ) {
wfDebug( __METHOD__ . " transformation deferred.\n" );
// XXX: Pass in the storage path even though we are not
rendering anything
// and the path is supposed to be an FS path. This is
due to getScalerType()
// getting called on the path and clobbering
$thumb->getUrl() if it's false.
$thumb = $handler->getTransform( $this, $thumbPath,
$thumbUrl, $params );
break;
}
// Clean up broken thumbnails as needed
$this->migrateThumbFile( $thumbName );
// Check if an up-to-date thumbnail already exists...
wfDebug( __METHOD__ . ": Doing stat for $thumbPath\n" );
if ( !( $flags & self::RENDER_FORCE ) &&
$this->repo->fileExists( $thumbPath ) ) {
$timestamp =
$this->repo->getFileTimestamp( $thumbPath );
if ( $timestamp !== false && $timestamp >=
$wgThumbnailEpoch ) {
// XXX: Pass in the storage path even though we are
not rendering anything
// and the path is supposed to be an FS path. This
is due to getScalerType()
// getting called on the path and clobbering
$thumb->getUrl() if it's false.
$thumb = $handler->getTransform( $this,
$thumbPath, $thumbUrl, $params );
$thumb->setStoragePath( $thumbPath );
break;
}
} elseif ( $flags & self::RENDER_FORCE ) {
wfDebug( __METHOD__ . " forcing rendering per flag
File::RENDER_FORCE\n" );
}
}
// If the backend is ready-only, don't keep generating
thumbnails
// only to return transformation errors, just return the error
now.
if ( $this->repo->getReadOnlyReason() !== false ) {
$thumb = $this->transformErrorOutput( $thumbPath,
$thumbUrl, $params, $flags );
break;
}
// Create a temp FS file with the same extension and the
thumbnail
$thumbExt = FileBackend::extensionFromPath( $thumbPath );
$tmpFile = TempFSFile::factory( 'transform_', $thumbExt );
if ( !$tmpFile ) {
$thumb = $this->transformErrorOutput( $thumbPath,
$thumbUrl, $params, $flags );
break;
}
$tmpThumbPath = $tmpFile->getPath(); // path of 0-byte temp
file
// Actually render the thumbnail...
wfProfileIn( __METHOD__ . '-doTransform' );
$thumb = $handler->doTransform( $this, $tmpThumbPath,
$thumbUrl, $params );
wfProfileOut( __METHOD__ . '-doTransform' );
$tmpFile->bind( $thumb ); // keep alive with $thumb
if ( !$thumb ) { // bad params?
$thumb = null;
} elseif ( $thumb->isError() ) { // transform error
$this->lastError = $thumb->toText();
// Ignore errors if requested
if ( $wgIgnoreImageErrors && !( $flags &
self::RENDER_NOW ) ) {
$thumb = $handler->getTransform( $this,
$tmpThumbPath, $thumbUrl, $params );
}
} elseif ( $this->repo && $thumb->hasFile()
&& !$thumb->fileIsSource() ) {
// Copy the thumbnail from the file system into storage...
$disposition = $this->getThumbDisposition( $thumbName );
$status = $this->repo->quickImport( $tmpThumbPath,
$thumbPath, $disposition );
if ( $status->isOK() ) {
$thumb->setStoragePath( $thumbPath );
} else {
$thumb = $this->transformErrorOutput( $thumbPath,
$thumbUrl, $params, $flags );
}
// Give extensions a chance to do something with this
thumbnail...
wfRunHooks( 'FileTransformed', array( $this, $thumb,
$tmpThumbPath, $thumbPath ) );
}
// Purge. Useful in the event of Core -> Squid connection failure
or squid
// purge collisions from elsewhere during failure. Don't keep
triggering for
// "thumbs" which have the main image URL though (bug 13776)
if ( $wgUseSquid ) {
if ( !$thumb || $thumb->isError() || $thumb->getUrl() !=
$this->getURL() ) {
SquidUpdate::purge( array( $thumbUrl ) );
}
}
} while ( false );
wfProfileOut( __METHOD__ );
return is_object( $thumb ) ? $thumb : false;
}

代碼中的紅字部分便是真是的圖片地址。
獲取地址的getThumbPath()、getThumbRel()、getRel()方法:

/includes/filerepo/file/File.php
/**
* Get the path of the thumbnail directory, or a particular file if
$suffix is specified
*
* @param bool|string $suffix If not false, the name of a thumbnail
file
* @return string
*/
function getThumbPath( $suffix = false ) {
$this->assertRepoDefined();
return $this->repo->getZonePath( 'thumb' ) . '/' .
$this->getThumbRel( $suffix );
}
/**
* Get the path, relative to the thumbnail zone root, of the
* thumbnail directory or a particular file if $suffix is specified
*
* @param bool|string $suffix if not false, the name of a thumbnail
file
* @return string
*/
function getThumbRel( $suffix = false ) {
$path = $this->getRel();
if ( $suffix !== false ) {
$path .= '/' . $suffix;
}
return $path;
}
/**
* Get the path of the file relative to the public zone root.
* This function is overriden in OldLocalFile to be like
getArchiveRel().
*
* @return string
*/
function getRel() {
return $this->getHashPath() . $this->getName();
}
/**
* Get the filename hash component of the directory including trailing
slash,
* e.g. f/fa/
* If the repository is not hashed, returns an empty string.
*
* @return string
*/
function getHashPath() {
if ( !isset( $this->hashPath ) ) {
$this->assertRepoDefined();
$this->hashPath =
$this->repo->getHashPath( $this->getName() );
}
return $this->hashPath;
}

這樣就有html 內容結果了:
return $thumb->toHtml( array( 'desc-link' => true ) );
至此,Thumb 拇指圖片的數據檢索,生成完畢。如何調用,也就明瞭。
有關單獨的圖片顯示或者插入圖片的顯示過程,可待補充。
根據如下內容的框架屬性,我把如下內容都歸屬於mediawiki 的GUI部分。SpecialListFile 的html 界面顯示輸出:

/includes/specials/SpecialListfiles.php
public function execute( $par ) {
$this->setHeaders();
$this->outputHeader();
if ( $this->including() ) {
$userName = $par;
$search = '';
$showAll = false;
} else {
$userName = $this->getRequest()->getText( 'user', $par );
$search = $this->getRequest()->getText( 'ilsearch', '' );
$showAll = $this->getRequest()->getBool( 'ilshowall',
false );
}
$pager = new ImageListPager(
$this->getContext(),
$userName,
$search,
$this->including(),
$showAll
);
if ( $this->including() ) {
$html = $pager->getBody();
//這是顯示部分。
} else {
$form = $pager->getForm();
$body = $pager->getBody();
$nav = $pager->getNavigationBar();
$html = "$form<br />\n$body<br />\n$nav";
}
$this->getOutput()->addHTML( $html );
}</code></pre>
說明SpecialListFile 是由其父類SpecialPage 的終極方法run() 調用。
在execute(),這一段代碼是顯示圖片列表指令:<pre><code>/includes/specials/SpecialListfiles.php
if ( $this->including() ) {
$html = $pager->getBody();
//這是顯示部分。
}

關係如圖:

execute 中,另外一個關鍵指令是:
/includes/specials/SpecialListfiles.php
$this->getOutput()->addHTML( $html );
爲何會有一個getOutput 方法,而有關getOutput()方法,背後的架構是什麼?透過OutputPage 開篇註釋語:Preparation for the final page rendering.這讓我想起來Android 的GUI 機制。也讓咱們明白了mediawiki 中的GUI,請看下UML 圖:

addHTML()代碼:

/includes/OutputPage.php
/**
* Append $text to the body HTML
*
* @param string $text HTML
*/
public function addHTML( $text ) {
$this->mBodytext .= $text;
}

MediaWiki的GUI架構的核心就是,透過OutputPage 的output()方法打印出結果:
Output()方法的代碼:

/includes/OutputPage.php
$response = $this->getRequest()->response();
if ( $this->mArticleBodyOnly ) {
echo $this->mBodytext;
}

看到這裏,咱們要問output方法由誰調用?哈哈哈,在查遍全部代碼發現,output方法是由MediaWiki類(在wiki.php)中,在run()方法中執行mediawiki類的私有方法main()時,被執行。

includes/Wiki.php
private function main() {
if(…)
$this->context->setTitle( $title );
$output = $this->context->getOutput();
// Since we only do this redir to change proto, always
send a vary header
$output->addVaryHeader( 'X-Forwarded-Proto' );
$output->redirect( $redirUrl );
$output->output();
wfProfileOut( __METHOD__ );
…
// Output everything!
$this->context->getOutput()->output();
wfProfileOut( __METHOD__ );
}

而對於MediaWiki則是在訪問index.php時,即被建立實例。有關輸出的指令,請如下UML圖例:

調用output()方法:

相關文章
相關標籤/搜索