Gallery:模块开发教程

来自站长百科
Firebrance讨论 | 贡献2008年11月27日 (四) 17:11的版本
跳转至: 导航、​ 搜索

一般注释

部分内容经许可取自: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)"链接是发生什么?

  1. 请求送达main.php,即G2主要的包装脚本。它会看到后置到"main.php"的URL部分,"?g2_view=members.MembersList",然后获知你想载入"members"模块并显示视图"MembersList"(一个视图就是一个动态G2页面)。
  2. "members"模块从modules/members被载入,并被告知显示"MembersList"视图。模块载入一个名为MembersList.inc 的文件,它会使用G2 API来联系数据库并准备所有所需信息(membername,id,email等)。
  3. 默认外观主题被载入并被指示显示一个模块页面。在编写模块时你无需为此担忧,只要注意外观主题可以在你的模块内容周围显示东西即可,比如头/尾等。
  4. 对应模板,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,因此这里先空着不管。

我们的RequiredCoreApiRequiredModuleApi版本必须符合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的所有实例修改为tutorial2Tutorial1修改为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系统呼叫在模板中的使用方式。所有Smarty呼叫都被包含在大括号内,有时会产生有趣的结果(当然还有可怕的错误),如果你尝试使用JavaScript,CSS或其他HTML相关使用大括号的古怪玩意的话。如果你确实想这么做的话,请将你的代码包括进{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中激活该模块。

带有动作的页面

前言

Now that you can create a view to display something, let's add an action to that page.

模块结构

We'll build from your tutorial2 module for this one.

templates/MyPage.tpl

In this file you've already got

    {if $MyPage.showAlbum}
      {$MyPage.album.title|markup}
    {/if}

Let's add a form to perform an action.

    {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}

This creates a form that will send data to a "controller" called tutorial2.MyPage. A view defines a page display, where a controller handles actions for that page. We define the controller also in MyPage.inc. The g->formVar calls add a prefix ("g2_") to the form fields. This helps avoid conflicts with other applications when G2 is embedded. You can see status messages above for success and error conditions. We'll explain how those are used below.

Note: If you get a ERROR_REQUEST_FORGED, you probably forgot to add {g->hiddenFormVars} to your <form> in the .tpl file.

MyPage.inc

We already have MyPageView defined. We'll need to add MyPageController, but first let's modify the view to initialize our form. Add the following after global $gallery; in function loadTemplate.

        if ($form['formName'] != 'MyPage') {
            $form['formName'] = 'MyPage';
            $form['char'] = '';
        }

When you first visit your view, $form is empty. So we initialize the form name and an empty value for "char".

Now to our controller. Add this definition just above the class MyPageView line.

class MyPageController extends GalleryController {

    /**
     * @see GalleryController::handleRequest
     */
    function handleRequest($form) {

The handleRequest function is called when an action is sent to the tutorial2.MyPage controller. All our form fields had names starting with "form[" so they have been loaded into the $form variable for us. Let's check the form input and perform the action.

        $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;
            }
        }

Several GalleryCoreApi calls here.. basically, if the requested action is "addChar" and a "char" value is given then we verify edit permission on the root album, lock it, load it, modify the title, save the change and release the lock. You can take a look at modules/core/classes/GalleryCoreApi.class or the apidoc to see the long list of things you can do with GalleryCoreApi.

If the "char" value was not a single character we set a value in $error. If the action went ok we set a value in $status. Look back at the tpl file to see where we check for these and show the appropriate message.

Next we need to tell G2 what to do now that our action is complete. We'll jump back to our view.

        $method = empty($error) ? 'redirect' : 'delegate';
        $result = array($method => array('view' => 'tutorial2.MyPage'),
                        'status' => $status, 'error' => $error);
        return array(null, $result);

On success we use a redirect back to our view and show the status message. On error we "delegate" back to our view. This means the $form data, even though not saved due to the error condition, is maintained so the user can fix the error and resubmit the form. Not a big deal in our form here with just one field, but this is handy if you filled in several things and just had an error in one.

Finally, close the handleRequest function and the controller class.

    }
}

测试

Login as a site admin (or other user with permission to edit the root album), browse to your view and try clicking the submit button with various inputs (too short, too long, just right). Click "reload" in your browser after a successful save to see that the status message appears only once.

模块Callback

These are values you can add in the setCallbacks call in module.inc. Setting a callback means that when the value specified is called, the corresponding function in your module class will be polled.

Example: $this->setCallbacks('getSiteAdminViews|getItemLinks'); (value is a | separated list).

For each entry here, define the function with that name in your module class (apidoc).

  • getSiteAdminViews: Insert links in site administration
  • getItemAdminViews: Insert links for items administration
  • getUserAdminViews: Insert links for user administration
  • getItemLinks: Insert links for items (Edit Photo, Add Comment, etc)
  • getSystemLinks: Links not associated with a particular item (Login, Site Admin, etc)
  • getItemSummaries: Insert summary content about items
  • registerEventListeners: Listen for events to add advanced functionality

与数据库的协作

In G2 most of the DB interaction is done through helper classes that are generated using a series of tools. This section will discuss the files that are needed, the tools that are needed and the procedures needed to get simple database operations working.

GNUmakefiles are not very important to understand, except that they are the method that all of the generated code is made. To run them you will need 'gmake' or 'make' and commandline PHP.

You will need the following GNUmakefile files. Copy them from another module (like comment) and make any directories that are needed. If our module were named dbExample, the directory structure would look like this:

dbExample/classes/GNUmakefile
dbExample/classes/GalleryStorage/GNUmakefile

There are two types of G2 tables:

  1. Entity tables.
    • Entity tables store objects like users, images or comments, and have a matching PHP file in the module's classes directory. As you might guess, an image would have different fields (path, name, description, etc.) than a comment (subject, commentorId, comment, etc.). Consequently, they are both entities even though their data reside in different tables.
  2. Map tables
    • Map tables contain any other data a module may need to store.

Entities are defined with a PHP class. See modules/comment/classes/GalleryComment.class as an example. Comments with @g2 tags define the database structure. Important parts of this file include:

  • Comment at the top with @g2 tags to define class-name, parent-class-name and version number for the table.
  • GalleryCoreApi::requireOnce call to load the parent class
  • class EntityName extends ParentClass
  • var definitions for each data field of the entity, each with a comment above it to define that field with @g2 tags.
  • The class must define get/set functions for all its data fields.

Maps are defined in classes/Maps.xml. See modules/customfield/classes/Maps.xml as an example. The structure of the table is defined in XML.

The DTDs for entity/map definitions are in lib/tools/dtd so you can see the available options for table/column definitions.

Once the entity and/or map definitions are ready, go to the command line, change into the classes directory and do make (or gmake). This will build the following files:

  • classes/GalleryStorage/schema.tpl and
  • classes/Maps.inc and/or
  • classes/Entities.inc

To use an entity you must register it (see function performFactoryRegistrations() in modules/comment/module.inc). You can then use GalleryCoreApi::newFactoryInstance to create an instance, $entity->create() to initialize it and $entity->save() to save it. GalleryCoreApi::loadEntitiesById loads entities.

Use these Core APIs to interact with maps:

  • GalleryCoreApi
addMapEntry
updateMapEntry
removeMapEntry
removeAllMapEntries

In Gallery 2.2+ you can use GalleryCoreApi::getMapEntry; prior to this you must use $gallery->search and provide a SQL query for your map.