Gallery:模块开发教程
一般注释[ ]
部分内容经许可取自:http://txtdump.com/g2dev/
此处的代码针对Gallery2.2已经过更新。
模块创建脚本(Module Creator Script)[ ]
该教程将帮助你从零开始创建所有的模块文件。你还可通过命令行运行gallery2目录的php lib/tools/creator/create-module.php来新建模块。
- 注: 你必须有G2开发者整合包或从svn/nightly snapshot进行安装以获得lib/tools/creator。
G2开发:介绍[ ]
前言[ ]
初次开发G2时,你可以尝试多花点时间来理解基本知识。此教程中我们会从最基本的开始,没用过PHP的人都可以学会编写模块代码,并且能做大做好。
G2的“理”和“实”[ ]
G2几乎实现了代码和设计之间的完全分离,以及一套可插模块的系统。那么这对你意味着什么呢?通过使用G2的API(应用程序编程接口)你可以向G2添加新的功能而无需对核心代码做任何修改。
在我想修改G2时的第一种冲动就是找到有问题的代码,然后将其修改为我需要的形式。但是这只是一个短期的权宜之计,在将来,尤其是你需要升级核心代码库时,麻烦就来了。实现某种修改或新特点的更有效途径就是编写模块。开始可能会多花费你一些时间,但之后你就会意识到维护好比10个模块,就如同修改几行语句以符合新版本的API那样轻松。
G2如何工作?[ ]
在开始之前,你至少需要明白G2的工作方式。比如,当点击边栏中的"成员列表(Member List)"链接是发生什么?
- 请求送达main.php,即G2主要的包装脚本。它会看到后置到"main.php"的URL部分,"?g2_view=members.MembersList",然后获知你想载入"members"模块并显示视图"MembersList"(一个视图就是一个动态G2页面)。
- "members"模块从modules/members被载入,并被告知显示"MembersList"视图。模块载入一个名为MembersList.inc 的文件,它会使用G2 API来联系数据库并准备所有所需信息(membername,id,email等)。
- 默认外观主题被载入并被指示显示一个模块页面。在编写模块时你无需为此担忧,只要注意外观主题可以在你的模块内容周围显示东西即可,比如头/尾等。
- 对应模板,modules/members/templates/MembersList.tpl,被载入而数据则被传送至此。该模板使用生成一个表格,事项处理就完成了。
如你所见,在此过程中有若干相异部分并各司其职。除了个别例外情况,不将复杂代码包括进模板,或将模板数据置入PHP代码中。一旦你理解此重要概念,G2的开发就变得更为简单了。
你已经看了这么多啦!真不简单!接下来我们来为G2创建模块。我打包票,绝对不难。
模块目录结构[ ]
modules/$modulename/module.inc
- 此为各模块所具有的入口点。它定义模块的身份以及职能。此为所有模块所必须具有的文件,当然也是唯一必须的文件。其余文件则根据模块职能不同而有所取舍。
modules/$modulename/$viewname.inc
- 页面被称为"视图(view)"。各视图都具有各自的.inc文件。该文件的文件名与视图相同。如果视图可以执行任何动作的话(即可通过按钮完成一些行为),那么该文件还会具有一个"控制器(controller)"类别,用以处理这些动作。
modules/$modulename/templates/$viewname.tpl
- 各视图的实际HTML位于一个模板文件中,该文件具有与视图相同的文件名。
modules/$modulename/templates/blocks/blocks.inc
- 此为一PHP文件,它告知各外观主题哪些为可用区块以及针对各区块所需载入的模板文件。该文件并非区块运行所必需的,但能使得这些区块可被外观主题用于"边栏区块(sidebar blocks)"及"相片区块(photo blocks)"等的设定之中。
modules/$modulename/templates/blocks/*.tpl
- 包含自blocks.inc 所定义区块的模板代码。
modules/$modulename/Callbacks.inc
- 该文件为各模块callback加载所用。定义区块的模块可使用此文件加载区块数据。
modules/$modulename/Preloads.inc
- 该文件为各模块preload加载所用。定义区块的模块可使用此文件指定区块所需的css或javascript。
modules/$modulename/classes/*Helper.class
- *Helper.class文件为可选的。它们被用来组织用于多个视图内容的代码,或是将大块代码分解成易于管理的小块。
modules/$modulename/classes/*Interface.class
- 接口针对多个模块共享某种常见方法以完成相似任务之用。比如,Search模块实现的search接口,其他模块可添加搜索(search)功能。
modules/$modulename/classes/Maps.xml
- 数据库表方案被保存在XML文件中以保证其可移植性。Gallery则会让它与各类数据库引擎兼容协作。仅当模块在数据库中保存数据的情况下才会用到它。
modules/$modulename/test/phpunit/*Test.class
- 这些是模块的单元测试。访问Gallery中的lib/tools/phpunit/index.php来运行单元测试。这些自动化的测试旨在确认模块能按预期运行。它们的编写需要花费时间,但能保证Gallery核心更改或你自己对模块作出的修改不会对模块造成破坏。
modules/$modulename/po/*.po
- 这些是模块中文本在其他语言中的翻译。
modules/$modulename/locale/*
- 这些为Gallery实际所使用的经编译的翻译。
哑模块编码[ ]
前言[ ]
现在你应该基本了解了G2系统,那么我们就开始深入下去吧,比如编写一个基础模块。
- 注: 确保PHP能显示语法错误和其他问题。参见:针对Gallery开发者的PHP设定,尤其注意一下gallery2/config.php中的display_errors。
模块结构[ ]
G2中的所有模块在modules/目录下都必须具有自己的目录,并在该目录下含有一个module.inc文件。用你习惯的方法创建它们。将目录命名为tutorial1。
提示:在*nix系统中。touch命令可用来创建空文件。
就是这样!现在你就有这些了:
modules/tutorial1/ (目录) modules/tutorial1/module.inc (空文件)
module.inc[ ]
module.inc是模块最'核心'的部分。它告知G2诸如模块名称,描述及版本一类的信息,以及其他庞杂的细枝末节。.
首先我们表明此为一PHP脚本。很简单,如此开头即可:
<?php
现在,将其粘贴到G2标准样版文件中。
/* * Gallery – 基于web的相片相册查看器和编辑器 * Copyright (C) 2000-2006 Bharat Mediratta * * 该程序为免费的,你可以对其做修改 * 受GNU General Public License条款约束 * 由Free Software Foundation发布;许可的第二版,或者说是 * 以后的版本。 * * 我们希望该应用程序能有用处,但 * 不打包票;也不能保证其 * 适销性或针对特殊目的的适用性。更多信息请参见GNU * General Public License。 * * 在获取此程序的同时,你应当也收到了 * GNU General Public License;如果没有的话,请联系Free Software * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. */
接下来我们要填入一些管理数据。你不需要进行任何修改。该数据不会为模块所用,但G2会将其用于管理/文档处理。
/** * Tutorial1 module * * My first module! * * @package Tutorial1 * @author Your Name <you@email.com> * @version $Revision$ $Date: 2006/01/02 07:58:13 $ */
接下来,我们告知G2我们的tutorial1模块将扩展GalleryModule。
class Tutorial1Module extends GalleryModule {
现在我们需要填入该模块所需的数据。这将在模块的Tutorial1Module函数中完成。
function Tutorial1Module() { global $gallery; $this->setId('tutorial1'); $this->setName($gallery->i18n('Tutorial Module 1')); $this->setDescription($gallery->i18n('My first module.')); $this->setVersion('0.9.0'); $this->setGroup('data', $gallery->i18n('Extra Data')); $this->setCallbacks(''); $this->setRequiredCoreApi(array(7, 10)); $this->setRequiredModuleApi(array(3, 2)); }
前三行是自描述的。setId值必须符合模块的目录名。$gallery->i18n行允许文本字段被翻译成其他语言。setVersion字段则只是用于模块的版本号,我们使用0.9.0。setGroup指的是站点管理页面中模块将被放入的组。我们还不需要设定callback,因此这里先空着不管。
我们的RequiredCoreApi和RequiredModuleApi版本必须符合G2所提供的版本,不然的话就会造成破坏。你想知道G2最新的API版本?检查G2 Team的某个模块并使用它的版本号。这里我们会使用CoreAPI v 7.10和ModuleAPI v 3.2。
注: Gallery 2.3模块会在构建式里多出一行:
$this->_templateVersion = 1;
- Gallery 2.3的外观主题可以覆盖模块的tpl文件。当模块新版本发布时请注意增加此数字,因为其在tpl文件中的修改无法与旧版本兼容。外观主题覆盖仅当模板版本相符时才能使用。注意,如果某人安装了Gallery2.2.x或更早版本的模块,而此方法不存在时,模块构建式中的此代码就不会使用$this->setTemplateVersion(1)。模块构建式中的PHP错误将会妨碍Gallery API的版本检查。
完成了,使用如下内容做结束:
} ?>
整个module.inc代码可在此找到。
模块的安装和激活[ ]
祝贺你,你已经编写完成了第一个G2模块!但目前你的模块还无法实际使用。如果不启用功能的话,就没有任何用处;所以,如果没有问题的话,你可以登入站点管理界面,点击插件(Plugins),下拉到额外数据(Extra Data)部分,然后安装并激活(在此还可以进行禁用和卸载)你的模块。很有意思,是不?
你刚刚创建完成了一个完全兼容且完美的G2模块。你会发现将来所制作的模块结构都是如此。好吧,是时候鼓励一下自己了,接下来的部分将教会你创建使用模板引擎的模块。
显示静态页面的模块[ ]
前言[ ]
现在你知道如何创建一个G2模块了,那么就让我们更深一步,创建使用模板引擎的模块以显示静态页面。
模块结构[ ]
首先创建一个modules/tutorial2目录。我们将从之前的模块继续。将旧的module.inc复制到新目录中,但请确保将tutorial1的所有实例修改为tutorial2,Tutorial1修改为Tutorial2。
另外,创建如下的文件和目录,目前这些文件都可以为空。
modules/tutorial2/MyPage.inc modules/tutorial2/templates/ modules/tutorial2/templates/MyPage.tpl
module.inc[ ]
我们不需要对module.inc做任何修改。MyPage.inc文件存在就允许G2使用它。继续并打开MyPage.inc文件。
MyPage.inc[ ]
在G2中是无法直接访问模板的。你必须使用一个"视图(view)",它将会准备所有必须的数据然后载入模板。MyPage.inc就是这样一种视图。
现在开始粘贴一般的G2样板文件。
<?php /* * Gallery – 基于web的相片相册查看器和编辑器 * Copyright (C) 2000-2006 Bharat Mediratta * * 该程序为免费的,你可以对其做修改 * 受GNU General Public License条款约束 * 由Free Software Foundation发布;许可的第二版,或者说是 * 以后的版本。 * * 我们希望该应用程序能有用处,但 * 不打包票;也不能保证其 * 适销性或针对特殊目的的适用性。更多信息请参见GNU * General Public License。 * * 在获取此程序的同时,你应当也收到了 * GNU General Public License;如果没有的话,请联系Free Software * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. */
填入其他标准数据。
/** * 该视图显示一个静态页面。 * * @package Tutorial2 * @subpackage UserInterface * @author Your Name <you@email.com> * @version $Revision$ $Date: 2006/01/05 07:00:00 $ */
你应该对module.inc的结构不陌生吧,那你就应该知道接下来要做的了。我们希望使用你自己的类别对GalleryView类别进扩展。
class MyPageView extends GalleryView {
类别名称必须与你的*.inc文件名称相同,并以"View"结尾。现在,我们的模块还不需要做任何数据操作,我们只需要2个函数。其中主要的一个就是loadTemplate。
/** * @see GalleryView::loadTemplate */ function loadTemplate(&$template, &$form) { global $gallery;
在loadTemplate函数中,我们告知G2在载入模板前所需做的所有事情。
对于我们的页面,将载入Gallery顶层相册,所以我们就可以显示有关它的一些信息了。
list ($ret, $albumId) = GalleryCoreApi::getDefaultAlbumId(); if ($ret) { return array($ret, null); } list ($ret, $album) = GalleryCoreApi::loadEntitiesById($albumId); if ($ret) { return array($ret, null); }
- 注: 在Gallery 2.1.x中,return array($ret, null);应为return array($ret->wrap(__FILE__, __LINE__), null);
将这些呼叫的结构记录下来,这相当重要,因为在很多G2代码中都要重复使用它。另外注意我们已加载了根相册,但还没有对其做任何操作。现在将这些数据用于我们的模板。
$template->setVariable('MyPage', array('album' => (array)$album, 'showAlbum' => true));
我们用以显示页面的数据应当被给予$template->setVariable。无论何时模板数据被添加一个"实体",它都应当被转换为上面所示的数组。
现在让我们继续,来加载模板。
return array(null, array('body' => 'modules/tutorial2/templates/MyPage.tpl')); }
如果我们能走到这一步,那么所作的这些都成功了。所以,我们告知G2成功了,并载入合适的模板文件。
你是否边栏上的注意到back to ____导航链接了?让我们将此特点整入我们的模块中吧。要达到此目的,我们要创建另一个函数。
/** * @see GalleryView::getViewDescription() */ function getViewDescription() { list ($ret, $module) = GalleryCoreApi::loadPlugin('module', 'tutorial2'); if ($ret) { return array($ret, null); } return array(null, $module->translate('My Page')); }
你可以将"My Page"改为你中意的明朝。最后,我们结束该类别以及PHP脚本。
} ?>
这么简单的一个任务看起来却用到了这么多代码,但这些代码可以在你今后的模块编写中反复使用的。你会发现,一旦掌握了基本模块的编码手段,就能轻松如意地编写更大更丰富的模块了。
现在是最后一个文件了,模板。
templates/MyPage.tpl[ ]
你应该能记得,loadTemplate()函数中的最后一个呼叫是模板的加载。G2使用Smarty模板引擎,这能提供很棒的代码与设计间的分离。 You'll find that you can use standard HTML in the template files, which makes them much easier to edit.
Let's get started.
{* * $Revision$ * 如果你想对此文件进行自定义的话,请勿直接编辑它,因为在将来升级时 * 会将其覆盖掉。请将其复制到一个名为"local"的新建文件夹中再进行编辑 * Gallery会首先查找此文件,如果存在的话就会先使用它。 *} <div class="gbBlock gcBackground1"> <h2> {g->text text="Look, it's my first page! And it's got a title."} </h2> </div>
这将在顶部创建美观的breadcrumb导航。注意G2系统呼叫在模板中的使用方式。如果你尝试使用JavaScript,CSS或其他HTML相关的,使用大括号的古怪玩意的话,那么所有的Smarty呼叫都被包含在大括号内,有时会产生有趣的结果(当然还有可怕的错误)。如果你确实想这么做的话,请将你的代码包括进{literal}..{/literal}标签中以告知Smarty你不想其被翻译。
另外使用{g->text text="在此输入你的文本"}来输出文本也是个好作法,但这不是必须的。就个人而言,我建议你不这么做,因为这将有可能会使你的模块被翻译成其他语言。
现在来填写模板的剩余部分。
<div class="gbBlock"> <p class="giDescription"> {g->text text="Hey, cool! I can write stuff here."} </p> </div> <div class="gbBlock"> <p class="giDescription"> {g->text text="Look, when I do this G2 makes me a nice little divisor."} <br/> {if $MyPage.showAlbum} {$MyPage.album.title|markup} {/if} </p> </div>
我希望此模板足以进行自我解释。正如前面提到的,大部分可以使用HTML解决的问题同样可以使用模板引擎来完成。慢慢把玩此模板,直到达到你满意的效果。该文件最高级的部分就是以{if开头那行之上的一点点。这是Smarty使用的语法,从而能够根据情况显示输出。由于我们在模板数据中将"showAlbum"设定为true,因此你应当能在页面中看到根相册的标题。参见 Tpl相关参考中对Smarty功能的简述。
页面的访问[ ]
我们还没有在G2中对页面做任何连接,那么该如何进行访问呢?走运的是,G2(至少需要独立版本的G2)具有的一个标准方法可以访问模块和它们的视图:
http://www.example.com/main.php?g2_view=tutorial2.MyPage
你应当能看到新的静态页面被完美地包裹在G2外观主题和CSS之中。如果你得到了某个访问错误检查,你可以在'Site Admin / Plugins中激活该模块。
带有动作的页面[ ]
前言[ ]
现在你可以创建一个视图来显示内容了,向该页面添加一个动作吧。
模块结构[ ]
我们会从tutorial2模块进行建立。
templates/MyPage.tpl[ ]
在此文件中你已有了
{if $MyPage.showAlbum} {$MyPage.album.title|markup} {/if}
现在添加一个表单来执行动作。
{if $MyPage.showAlbum} {if isset($status.saved)} <div class="giSuccess"> {g->text text="Saved!"} </div> {/if} {$MyPage.album.title|markup} <br/> <form method="POST" action="{g->url}"> {g->hiddenFormVars} <input type="hidden" name="{g->formVar var="controller"}" value="tutorial2.MyPage"/> <input type="hidden" name="{g->formVar var="form[formName]"} value="{$form.formName}"/> <input type="text" size="2" name="{g->formVar var="form[char]"}" value="{$form.char}"/> {if isset($form.error.char)} <div class="giError"> {g->text text="Enter a single character"} </div> {/if} <input type="submit" class="inputTypeSubmit" name="{g->formVar var="form[action][addChar]"}" value="{g->text text="Add to title"}"/> </form> {/if}
这会将数据发送给名为tutorial2.MyPage 的"控制器"。一个视图定义一个页面显示,而控制器则会为该页面处理相关动作。我们也在MyPage.inc中定义控制器。g->formVar呼叫会向表单字段添加一个前缀("g2_")。这将会避免G2在被嵌入情况下与其他应用程序发生冲突。成功或出现错误情况时,你会在上面看到状态消息。我们会在后面解释使用方法。
- 注:如果你得到了ERROR_REQUEST_FORGED错误,那么很可能你忘记向.tpl文件中的<form>添加{g->hiddenFormVars}了。
MyPage.inc[ ]
我们已经定义了MyPageView。我们需要添加MyPageController,但让我们先对视图做修改以初始化表单。在loadTemplate 函数中的global $gallery;之后添加如下内容。
if ($form['formName'] != 'MyPage') { $form['formName'] = 'MyPage'; $form['char'] = ''; }
当你首次访问视图时,$form是为空的。因此我们要为"char"初始化表单名称以及一个空值。
现在来到我们的控制器,在class MyPageView那一行的上面添加此定义。
class MyPageController extends GalleryController { /** * @see GalleryController::handleRequest */ function handleRequest($form) {
handleRequest函数在某动作被发送到tutorial2.MyPage控制器时被呼叫。所有表单字段名称开头为"form[",这样它们就会被载入$form变量。现在让我们来检查表单输入并执行动作。
$status = $error = array(); if (isset($form['action']['addChar'])) { if (strlen($form['char']) != 1) { $error[] = 'form[error][char]'; } else { list ($ret, $albumId) = GalleryCoreApi::getDefaultAlbumId(); if ($ret) { return array($ret, null); } $ret = GalleryCoreApi::assertHasItemPermission($albumId, 'core.edit'); if ($ret) { return array($ret, null); } list ($ret, $lockId) = GalleryCoreApi::acquireWriteLock($albumId); if ($ret) { return array($ret, null); } list ($ret, $album) = GalleryCoreApi::loadEntitiesById($albumId); if ($ret) { GalleryCoreApi::releaseLocks($lockId); return array($ret, null); } $album->setTitle($album->getTitle() . $form['char']); $ret = $album->save(); if ($ret) { GalleryCoreApi::releaseLocks($lockId); return array($ret, null); } $ret = GalleryCoreApi::releaseLocks($lockId); if ($ret) { return array($ret, null); } $status['saved'] = 1; } }
这里有一些GalleryCoreApi呼叫,如果被请求动作为"addChar"并给定了一个"char"值,我们就能确认对根相册权限进行编辑,对其lock,载入,修改标题,保存修改并解除lock。你可以看看modules/core/classes/GalleryCoreApi.class或 apidoc来了解GalleryCoreApi的实际功用。
如果"char"值不是单个字符,我们就在$error中设定一个值。如果动作执行正确,我们就会在$status中设定一个值。现在回过头来看看tpl文件,来明确检查的位置并显示合适的消息。
接下来我们需要告知G2动作完成时所需要做的。我们会跳转回视图中。
$method = empty($error) ? 'redirect' : 'delegate'; $result = array($method => array('view' => 'tutorial2.MyPage'), 'status' => $status, 'error' => $error); return array(null, $result);
成功后通过重新导向转回我们的视图并显示状态消息。在发生错误时,我们会"委派"回视图。这意味着$form数据即使由于错误而没有被保存也会被留下,这样用户就可以修复错误并重新提交表单了。我们的例子只有一个字段,所以并不显著,但如果你填入了若干项目而仅有一个出错时,这就很好用了。
最后结束handleRequest函数及控制器类别。
} }
测试[ ]
以管理员身份登入(或其他可编辑根相册的用户),浏览你的视图,尝试使用各类输入点击提交按钮。成功保存后,点击浏览器中的"reload"会看到状态消息仅出现一次。
模块Callback[ ]
这些为可添加到module.inc 中setCallbacks呼叫的值。设定callback意味着,当某个指定值被呼叫时,模块类别中的对应函数就会被轮询。
举例:$this->setCallbacks('getSiteAdminViews|getItemLinks'); (value为一个以|分隔的列表)。
为这些入口定义函数,使用模块类别中的名称(apidoc)。
- getSiteAdminViews:在站点管理者插入链接
- getItemAdminViews:插入项目管理链接
- getUserAdminViews:插入用户管理链接
- getItemLinks:插入项目链接(编辑相片(Edit Photo)添加评论(Add Comment)等)
- getSystemLinks:不与特殊项目关联的链接(登入(Login)站点管理(Site Admin)等)
- getItemSummaries:插入项目相关的摘要内容
- registerEventListeners:侦听事件以添加高级功能
与数据库的协作[ ]
在G2中,大部分的BD交互是通过使用一系列工具所生成的helper类别完成的。此部分将讨论所需的文件,工具以及流程,以使简单数据库操作起效。
GNUmakefiles并不一定要理解,不过它们是所有生成的代码的创建方法。要允许它们的话,你需要'gmake'或'make'以及commandline PHP。
你会需要下面的GNUmakefile文件。将它们从另一个模块复制出来(比如comment模块)并建立所需的所有目录。如果我们的模块被命名为dbExample,目录结构就应如下:
dbExample/classes/GNUmakefile dbExample/classes/GalleryStorage/GNUmakefile
有两类G2表:
- 实体表(Entity table)
- 实体表存储用户,图片或评论之类的对象,并在模块类别目录下具有一个相符的PHP文件。正如你所猜到的,一张图片具有的字段(路径,名称,描述等)与一个评论所具有的(主题,commentorId,评论等)不同。所以,就算它们的数据位于不同的表中,它们都一样是实体。
- 映射表(Map table)
- 映射表含有模块可能需要存储的其他所有数据。
实体是在PHP类别中被定义的。modules/comment/classes/GalleryComment.class就是一个例子。带有@g2标记的批注定义数据库结构。该文件的重要部分包括:
- 顶部带有@g2标记的批注定义类别名称,父族类别名称以及表的版本号。
- GalleryCoreApi::requireOnce呼叫载入父族类别。
- class EntityName扩展ParentClass
- var 定义实体各数据字段,其上各有一个批注,使用@g2标记定义该字段。
- 类别必须为其所有的数据字段定义get/set函数。
映射是在classes/Maps.xml中被定义的。modules/customfield/classes/Maps.xml就是一个例子。表结构是在XML中定义的。
实体/映射定义的DTD位于lib/tools/dtd中,因此你可以看到表/栏定义的可以选项。
一旦准备好了实体和/或映射定义,打开命令行,转到类别目录并进行make(或gmake)。如此会建立如下文件:
- classes/GalleryStorage/schema.tpl 和
- classes/Maps.inc 和/或
- classes/Entities.inc
要使用实体的话,你必须进行注册(参见modules/comment/module.inc中的函数performFactoryRegistrations())。然后你可以使用GalleryCoreApi::newFactoryInstance创建一个实例,$entity->create()用来初始化,而$entity->save()则用来保存。GalleryCoreApi::loadEntitiesById载入实体。
使用如下Core API来与映射进行交互:
- GalleryCoreApi
在Gallery 2.2+中你可以使用GalleryCoreApi::getMapEntry;在此之前你必须使用$gallery->search并为映射提供一个SQL查询。