此文是一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()方法: