Gallery2:Module Development Tutorial - Gallery Codex
Personal tools

Gallery2:Module Development Tutorial

From Gallery Codex

General Notes

Parts taken with permission from: http://txtdump.com/g2dev/

The code here has been updated for Gallery 2.2.

Module Creator Script

This tutorial will guide you to create all module files from scratch. You can also create the files for a new module by going to the command line and running php lib/tools/creator/create-module.php from your gallery2 directory.

Note: You must have a G2 developer package or install from svn/nightly snapshot to get lib/tools/creator.

Developing for G2: an Introduction

Preface

When first developing for G2 you can spend a lot of time trying to figure out the basics. In this tutorial we will start with the basics, a module that somebody with no PHP experience whatsoever can code, and hope to move on to bigger and better things.

The G2 Philosophy and Implementation

G2 has been developed to achieve almost complete separation of code and design as well as a pluggable module system. What does this mean to you? By using G2's API (Application Programming Interface) you can add new functionality to G2 without modifying any of the core code.

The first impulse I felt when I wanted to modify G2 was to find the code in question and edit it to fit my needs. While this is a very fast short-term solution, this sort of thinking would create major headaches in the future especially once you have to upgrade the core codebase. A much more efficient way to implement a modification or a new feature is to code a module for it. It may take you slightly more time to get started, but you will pat yourself on the back later when you realise that 'maintaining' your ten custom modules is as easy as changing a few lines to match with the new API versions.

How Does G2 Work?

Before we get started, you need at least some understanding of how G2 works. For example, what happens when the user clicks the "Member List" link on the sidebar?

  1. The request goes to main.php, the main G2 wrapper script. It looks at the part of the URL appended to "main.php" that says "?g2_view=members.MembersList" and realizes that you want to load the "members" module and display the view, "MembersList" (a view is analogous to a dynamic G2 page).
  2. The "members" module is loaded, from modules/members and is told to display the "MembersList" view. The module loads up a file called MembersList.inc, which in turn uses the G2 API to contact the database and prepare all of the information required (membername, id, email, etc.).
  3. The default theme is loaded and instructed to display a module page. You don't need to worry about this when coding a module, just be aware the theme can display stuff around your module content like a header/footer, etc.
  4. The corresponding template, modules/members/templates/MembersList.tpl, is loaded and the data is passed to it. The template generates a table with the data and the transaction is complete.

As you can see, there are several distinct parts in the process, each of which serve their own purpose. With very few exceptions, you do not include complex code into the templates or templating data into the PHP code. Once you understand this key concept, developing for G2 becomes a whole lot easier.

You've made it this far. Great! Let's continue on to creating a module for G2. It will be easy, I promise.

Module Directory Structure

modules/$modulename/module.inc

This is the entry point to every module. It defines what the module is and will do. This is the only file every module must have. The rest may or may not be used depending on what the module does.

modules/$modulename/$viewname.inc

Pages are called "views". Each view is given its own .inc file. The name of the file is the same as the view. If the view can perform any actions (i.e., it has buttons that do something) then this file will also have a "controller" class which handles those actions.

modules/$modulename/templates/$viewname.tpl

The actual HTML for each view is in a template file with the same name as the view.

modules/$modulename/templates/blocks/blocks.inc

A PHP file that tells each theme what blocks are available and what template file to load for each one. This file is not required for blocks to function, but it makes them available for themes to use in settings for "sidebar blocks", "photo blocks", etc.

modules/$modulename/templates/blocks/*.tpl

Contains the template code for blocks specified from blocks.inc.

modules/$modulename/Callbacks.inc

This file is where callbacks are loaded from for each module. A module that defines blocks may use this file to load data for the blocks.

modules/$modulename/Preloads.inc

This file is where preloads are loaded from for each module. A module that defines blocks may use this file to specify css or javascript needed for the blocks.

modules/$modulename/classes/*Helper.class

*Helper.class files are optional. They are used to organize the code for things that are used in more than one view or simply to break up large code into manageable chunks.

modules/$modulename/classes/*Interface.class

Interfaces are a way for multiple modules to share a common method for doing similar tasks. For example, the Search module implements a search interface that other modules build off of to add search capabilities to their module.

modules/$modulename/classes/Maps.xml

Database table schemes are stored in an XML file for portability. Gallery does the work to make it compatible with various database engines. This is needed only if the module stores data in the database.

modules/$modulename/test/phpunit/*Test.class

These are unit tests for a module. Visit lib/tools/phpunit/index.php in a Gallery install to run unit tests. These are automated tests to verify the module works as expected. They take time to write, but are very valuable to ensure Gallery core changes or your own module changes won't break the module.

modules/$modulename/po/*.po

These are translations of text in your module to other languages.

modules/$modulename/locale/*

These are the compiled translations actually used by Gallery.

Coding a Dummy Module

Preface

Now that you have a basic grasp of the G2 system, let's dive in and code a basic module.

Note: Make sure that PHP shows you syntax errors and other problems. See: PHP Settings for Gallery developers, especially the note about display_errors' in your gallery2/config.php.

Module Structure

All modules in G2 must have their own directory (folder) under modules/ as well as a module.inc file within that directory. Go ahead and create both using your preferred method. Name the directory tutorial1.

Tip: in *nix systems, the touch command is useful for creating empty files.

Great! You now have the following:

modules/tutorial1/           (directory)
modules/tutorial1/module.inc (empty file)

module.inc

module.inc is the very 'core' of your module. It tells G2 things like the module's name, description, and version, as well as a whole myriad of other nifty stuff. So let's fill it in.

First, we tell the world that this is a PHP script. Easy enough, just start with:

<?php

Now, let's paste in the standard G2 boilerplate.

/*
 * Gallery - a web based photo album viewer and editor
 * Copyright (C) 2000-2006 Bharat Mediratta
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or (at
 * your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA  02110-1301, USA.
 */

Then, we want to fill in some administrative data. You don't have to change any of this. This data is not used by our module for anything, but G2 uses it for management/documentation purposes.

/**
 * Tutorial1 module
 *
 * My first module!
 *
 * @package Tutorial1
 * @author Your Name <you@email.com>
 * @version $Revision$ $Date: 2006/01/02 07:58:13 $
 */

Next, we tell G2 that our tutorial1 module is extending GalleryModule.

class Tutorial1Module extends GalleryModule {

Now we need to fill in the required data for the module. We do this in the module's Tutorial1Module function.

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

The first three fields above are fairly self-descriptive. The setId value must match the directory name for your module. The $gallery->i18n line allows text fields to be translated into other languages. The setVersion parameter is simply a version number you want to use for your module. We'll use 0.9.0 to start. setGroup refers to the group the module will be placed under the the Site Admin pages. We don't need to set any callbacks just yet, so we leave that blank.

Our RequiredCoreApi and RequiredModuleApi versions must match G2's provided versions, or things will break. Want to know your G2's latest API version? Check one of the G2 Team's modules in your install and use the numbers from there. Here, we're using the CoreAPI v 7.10 and ModuleAPI v 3.2.

Note: Gallery 2.3 modules will have an additional line in the constructor:

        $this->_templateVersion = 1;
Gallery 2.3 themes can override module tpl files. Increment this number whenever releasing a new version of the module that includes changes in any tpl file that are incompatible with previous versions. Theme overrides will only be used when the template version matches. Note that this code in the module constructor does not use $this->setTemplateVersion(1) in case someone installs the module on Gallery 2.2.x or older where this method does not exist. A PHP error in the module constructor would prevent Gallery's API version checking.

We're done! Let's close it up with:

}
?>

The entire module.inc code is available here.

Installing/Activating your Module

Congratulations! You've now coded your first G2 module! But so far, your module does not do anything. It's hard to use something with no functionality, but provided everything went smoothly you'll be able to login to your Site Admin interface, click Plugins, scroll down to the Extra Data section, and then install and activate (and deactivate and uninstall) your module. Fun, isn't it?

You've just created a fully compatible, standards-compliant, wonderful, amazing, brilliant module for G2. You'll find that all of your future modules will follow the same structure. Give yourself a pat on the back and continue to the next section where you'll learn to create a module that starts using the templating engine.

A Module that Displays Static Pages

Preface

Now that you know how to create a proper G2 module let's go one step further and create a module that uses the G2 templating engine to show static pages.

Module Structure

To begin, create a modules/tutorial2 directory. We'll continue on where you left off with the last module. Copy the old module.inc into the new directory but make sure to change all instances of tutorial1 to tutorial2 and Tutorial1 to Tutorial2.

Also, let's create the following files and directories. You can leave the files empty for now.

modules/tutorial2/MyPage.inc
modules/tutorial2/templates/
modules/tutorial2/templates/MyPage.tpl

module.inc

Great news, we don't need to make any changes to module.inc! Just the existence of the MyPage.inc file allows G2 to use it. Let's continue and open the MyPage.inc file.

MyPage.inc

In G2, it is not possible to access templates directly. Rather, you must use a "view" that will prepare all the necessary data and then load the template itself. MyPage.inc is such a view.

Let's start by pasting the regular G2 boilerplate.

<?php
/*
 * Gallery - a web based photo album viewer and editor
 * Copyright (C) 2000-2006 Bharat Mediratta
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or (at
 * your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA  02110-1301, USA.
 */

Fill in the other standard data.

/**
 * This view shows a static page.
 *
 * @package Tutorial2
 * @subpackage UserInterface
 * @author Your Name <you@email.com>
 * @version $Revision$ $Date: 2006/01/05 07:00:00 $
 */

Phew! The structure from module.inc may seem familiar to you so you might be able to guess what's coming. We want to extend the GalleryView class with your own class.

class MyPageView extends GalleryView {

The class name must match the name of your *.inc file with "View" added on the end. Now, our module doesn't have to do any data manipulation so we only need two functions. The main one is loadTemplate.

    /**
     * @see GalleryView::loadTemplate
     */
    function loadTemplate(&$template, &$form) {
        global $gallery;

In loadTemplate function we tell G2 everything we want to do before loading our template.

For our page, we'll load the top album of the Gallery so we can display some information about it.

        list ($ret, $albumId) = GalleryCoreApi::getDefaultAlbumId();
        if ($ret) {
            return array($ret, null);
        }

        list ($ret, $album) = GalleryCoreApi::loadEntitiesById($albumId);
        if ($ret) {
            return array($ret, null);
        }
Note: in Gallery 2.1.x return array($ret, null); would be return array($ret->wrap(__FILE__, __LINE__), null);

Take note of the structure of these calls, as it is fairly important. It is repeated throughout a lot of the G2 code. Also note that we've loaded the root album, but have not done anything with it just yet. Let's make that data available to our template.

        $template->setVariable('MyPage',
            array('album' => (array)$album, 'showAlbum' => true));

Any data we need to display our page should be given to $template->setVariable. Whenever an "entity" is added to the template data it should be converted to an array as shown above.

Let's continue by loading the template.

        return array(null,
                     array('body' => 'modules/tutorial2/templates/MyPage.tpl'));
    }

If we've gone this far, the operation has been a success. Therefore, we tell G2 that we've succeeded and load the appropriate template file.

Ever notice those nice back to ____ navigation links on the sidebar? Let's incorporate this feature into our module. We do this by creating another function.

    /**
     * @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'));
    }

Feel free to change "My Page" to whatever your heart desires. Finally, we close the class and the PHP script.

}
?>

Phew! While it looks like a lot of code for such a simple task, most of the code can be reused over and over in your subsequent modules. You'll find that once you know how to code a basic module you can simply keep building on your existing knowledge to easily code bigger and bigger modules.

Let's continue with our last file, the template.

templates/MyPage.tpl

As you should remember, the last call in the loadTemplate() function is the loading of the template. G2 uses the Smarty templating engine which provides excellent code and design separation. 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$
 * Read this before changing templates!  http://codex.galleryproject.org/Gallery2:Editing_Templates
 *}
<div class="gbBlock gcBackground1">
  <h2> {g->text text="Look, it's my first page! And it's got a title."} </h2>
</div>

This creates the nice breadcrumb navigation at the top. Note how G2 system calls are still used in the templates, but not extensively. All Smarty calls are enveloped in the curly brackets which sometimes results in funny results (and horrible errors) if you try to use JavaScript, CSS, or any other HTML-related oddity that uses curly brackets. If you want to do so, just wrap your code with {literal}..{/literal} tags to tell Smarty that you don't want it to interpret it.

It is also good form to use {g->text text="your text here"} to output text, but this is not required. Personally, I recommend you do it regardless as it makes it possible to translate your module into other languages.

Let's fill in the rest of the template.

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

I hope that the template is fairly self explanatory. As mentioned earlier, most of the things you can do with HTML you can also achieve with the templating engine. Play around with the template until you think you have it looking the way you want it. The most advanced portion of this file is the bit above starting with {if. This is Smarty syntax to conditionally display some output. Since we set "showAlbum" to true in our template data, you should see the title of your root album in the page. See the Template reference for the compendium of what you can do with Smarty.

Accessing your Page

We have not made any links in G2 to our page, so how do we access it? Luckily, G2 (the standalone version, at least) has a standard method of accessing modules and their views:

http://www.example.com/main.php?g2_view=tutorial2.MyPage

You should be able to see your new static page wrapped nicely with the G2 theme and CSS. If you get an access error check you activated the module in Site Admin / Plugins.

A Page with Actions

Preface

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

Module Structure

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.

    }
}

Test it out

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.

Module Callbacks

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

Working With the Database

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). If you are developing on windows you will need to download cygwin and make sure you get make during installation and then add your php directory to cygwin's PATH (eg export PATH=$PATH:/cygdrive/c/php) from the cygwin command line. You can access your c drive from that path as well in order to move around to start the make process. Running make 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.