Updating Plugins For 2.0

This page is a draft.

The information on this page concerns an unreleased version of Omeka or is subject to change for some other reason. Stay away, if you know what's good for you.

Omeka 2.0 will include many changes that will affect how plugins are designed and how they accomplish various tasks. This is a running list, and will likely change greatly between now and the eventual release of 2.0 (no date is set for that yet).

Omeka an Archive?

While archivists can (and many do) use Omeka as a presentation layer to their digital holdings, Omeka is not archival management software. To underscore this fact, we've removed all mention of the word "archive" in the Omeka codebase and filesystem. This will require at least two additional steps when upgrading from earlier versions to 2.0:

  • Rename the archive/files/ directory to /archive/original/:
 $ mv /path/to/omeka/archive/files/ to /path/to/omeka/archive/original/
  • Rename the archive/ directory to files/:
 $ mv /path/to/omeka/archive/ /path/to/omeka/files/

Some changes to the API were necessary for this change. They include:

  • Rename File::$archive_filename to File::$filename
  • Rename "archive" to "original" (when an argument that refers to path to files/original/)
  • Rename Omeka_Filter_Filename::renameFileForArchive() to Omeka_Filter_Filename::renameFile()
  • Rename Omeka_View_Helper_Media::archive_image() to Omeka_View_Helper_Media::image_tag()
  • Rename Installer_Requirements::_checkArchiveStorageSetup() to Installer_Requirements::_checkFileStorageSetup()

Abstract Plugin Class

The Omeka_Plugin_Abstract class was introduced in Omeka 1.5 to assist plugin authors in setting up their plugin's hooks, filters, and options. In 2.0 this has been renamed to Omeka_Plugin_AbstractPlugin. We highly recommend using this class because plugin.php may be deprecated in future versions of Omeka in favor of loading the plugin directly from this class. Also starting in 2.0, Omeka does not require the plugin.php file if a valid plugin class exists following this pattern:

// In plugins/YourPlugin/YourPluginPlugin.php
class YourPluginPlugin extends Omeka_Plugin_AbstractPlugin

One change introduced in Omeka 2.0 is arbitrary hook and filter callback names. Before, all callback names followed a predetermined format. While this format remains an option, now a corresponding key in $_hooks and $_filters will be interpreted as the name of the callback method.

      'changeSomething' => 'display_setting_site_title', 
      'displayItemDublinCoreTitle' => array(
          'Dublin Core', 


Your controllers should now extend Omeka_Controller_AbstractActionController instead of Omeka_Controller_Action. This is for forward compatibility with Zend Framework 2.0. For example:

class YourController extends Omeka_Controller_AbstractActionController

modelClass and modelName

Controllers used to have a _modelClass property that refers to the corresponding name of the model used by the controller. That changes to use setDefaultModelName.

// 1.x
public function init() 
    $this->_modelClass = 'MyModel';
// 2.0
public function init() 

Db wrappers removed

getDb, getTable, and findById are replaced by equivalent calls to _helper->db->{methodName}.

$elementToRemove = $this->getDb()->getTable('Element')->find($elementId);


$elementToRemove = $this->_helper->db->getTable('Element')->find($elementId);



Your records should now extend Omeka_Record_AbstractRecord instead of Omeka_Record. This is for forward compatibility with Zend Framework 2.0. For example:

class YourRecord extends Omeka_Record_AbstractRecord

We've made some major changes to the record API. The following callbacks have been removed:

  • beforeSaveForm
  • afterSaveForm
  • beforeInsert
  • afterInsert
  • beforeUpdate
  • afterUpdate
  • beforeValidate
  • afterValidate

As such, the following plugin hooks have been removed, where * is "record" or a specific record name (e.g. "item", "collection"):

  • before_save_form_*
  • after_save_form_*
  • before_insert_*
  • after_insert_*
  • before_update_*
  • after_update_*
  • before_validate_*
  • after_validate_*

By removing these callbacks we give you full control over the timing of execution. Any logic that's currently in the SaveForm, Insert, Update callbacks should be moved to beforeSave() and afterSave(). The Any logic that's currently in the Validate callbacks should be moved to _validate(). For example:

// Note the order of execution.
public function afterSave($args)
    if ($args['insert']) {
        // Do something after record insert. Equivalent to afterInsert.
    } else {
        // Do something after record update. Equivalent to afterUpdate.
    // Do something after every record save.
    if ($args['post']) {
        // Do something with the POST data. Equivalent to afterSaveForm.

Note that the signature of the beforeSave() and afterSave() in Omeka_Record_AbstractRecord has changed to beforeSave($args) and afterSave($args), with no type specified for $args. To adhere to strict standards, existing beforeSave and afterSave methods should reflect that change.

Another change is that saveForm() has been merged into save(). Using save() to handle a form in your controller is done like this:

public function editAction()
    // Check if the form was submitted.
    if ($this->getRequest()->isPost()) {
        // Set the POST data to the record.
        // Save the record. Passing false prevents thrown exceptions.
        if ($record->save(false)) {
            $successMessage = $this->_getEditSuccessMessage($record);
            if ($successMessage) {
                $this->_helper->flashMessenger($successMessage, 'success');
        // Flash an error if the record does not validate.
        } else {

Models Directory

The models directory has been reorganized to only have models that extend Omeka_Record_AbstractRecord at the root level. Classes that extend Omeka_Db_Table were moved to the models/Table/ directory, and renamed from *Table to Table_*. This change is transparent when using Omeka_Db::getTable(), so you'll still pass the root name of the table, like so:

// This gets the Table_Item object.
$itemTable = $db->getTable('Item');

When writing plugins we recommend following this directory pattern in your models directory:

  • Classes that extend Omeka_Record_AbstractRecord should go directly in the models/ directory, as usual.
  • Classes that extend Omeka_Db_Table should go in the models/Table/ directory, and their class names should be Table_[record name].

For backwards compatibility we allow Omeka_Db_Table classes to follow their old pattern, but this behavior is deprecated.

Database Aliases in Table_* Classes

Previously, when using the getSelect() method, select statements could use a short alias for the database name, when adding clauses, for example , "e" in this query:

$select->where("e.id = ?", $exhibitId);

when querying the exhibits table. Those should no longer be used. Use the full name of the table instead (without the table prefix):

$select->where("exhibits.id = ?", $exhibitId);

User Record

The User record no longer depends on an Entity to store some data like name and email. The user's full name and email are directly part of the User object now.

There are no longer separate fields for first, last, and middle name, just one for full name.

Entity Record

The Entity, EntitiesRelations, and EntityRelationships records are gone, as is the Relatable mixin.

Plugins that were relying on or referring to entities or entity ids should usually directly refer to users instead.

Plugins that had records using Relatable should use the new Mixin_Owner mixin instead for handling record ownership.


The following mixins have been changed

Old Mixin New Mixin
Ownable Mixin_Owner
Taggable Mixin_Tag
ActsAsElementText Mixin_ElementText
PublicFeatured Mixin_PublicFeatured

The Mixin_Timestamp mixin has been added.

The Mixing_Search mixin has been added.

The Orderable mixin has been removed.


The Omeka_Acl object no longer exists. References to Omeka_Acl should be to Zend_Acl instead.

Omeka_Acl provided several non-standard methods which are gone along with it: loadRoleList(), loadResourceList(), and loadAllowList(). All of these methods were shortcuts for passing arrays to the Acl object.

Now, just directly make individual calls to addRole(), addResource(), and allow(). You no longer need to use loadResourceList() to define the privileges for each resource.



$acl->checkUserPermission('ExhibitBuilder_Exhibits', 'showNotPublic');

Use isAllowed() instead:

$acl->isAllowed(current_user(), 'ExhibitBuilder_Exhibits', 'showNotPublic');

Helper Functions

Many helper functions are removed or replaced. The table below lists previously used functions with their replacements

Function Replacements

Old Function New Function
random_featured_item() random_featured_items(1)
has_tags() metadata($record, 'has tags')
item_has_tags() metadata($item, 'has tags')
item_has_files() metadata($item, 'has files')
item_has_thumbnail() metadata($item, 'has thumbnail')
item_citation() metadata($item, 'citation')
n/a metadata($item, 'file count')
item_fullsize() item_image('fullsize')
item_thumbnail() item_image('thumbnail')
item_square_thumbnail() item_image('square_thumbnail')
setting() option()
display_js() head_js()
display_css() head_css()
css() css_src()
js() js_tag()
queue_js() queue_js_file()
queue_css() queue_css_file()
display_random_featured_collection() random_featured_collection()
recent_collections() get_recent_collections()
random_featured_collection() get_random_featured_collection()
get_latest_omeka_version() latest_omeka_version()
display_file() file_markup()
display_files() file_markup()
recent_files() get_recent_files()
show_file_metadata() all_element_texts()
_tag_attributes() tag_attributes()
simple_search() simple_search_form()
display_form_input_for_element() element_form()
display_element_set_form() element_set_form()
label_options() label_table_options()
display_search_filters() search_filters()
current_action_contexts() get_current_action_contexts()
__v() get_view()
display_files_for_item() files_for_item()
display_random_featured_item() random_featured_item()
display_random_featured_items() random_featured_items()
recent_items() get_recent_items()
random_featured_items() get_random_featured_items()
random_featured_item() get_random_featured_item()
show_item_metadata() all_element_texts()
link_to_advanced_search() link_to_item_search()
link_to_file_metadata() link_to_file_show()
link_to_next_item() link_to_next_item_show()
link_to_previous_item() link_to_previous_item_show()
link_to_browse_items() link_to_items_browse()
nls2p() text_to_paragraphs()
recent_tags() get_recent_tags()
item_tags_as_string() tag_string('item')
item_tags_as_cloud() tag_cloud('item')
has_permission() is_allowed()
uri() url()
abs_uri() absolute_url()
abs_item_uri() record_url($item, 'show', true)
record_uri() record_url()
item_uri() record_url($item)
current_uri() current_url()
is_current_uri() is_current_url()
items_output_uri() items_output_url()
file_display_uri() file_display_url()
public_uri() public_url()
admin_uri() admin_url()
set_theme_base_uri() set_theme_base_url()
revert_theme_base_uri() revert_theme_base_url
loop_records() loop()
loop_files() loop('files')
loop_collections() loop('collections')
loop_items() loop('items')
loop_item_types() loop('item_types')
loop_items_in_collection() loop('items')
loop_files_for_item() loop('files')
set_current_file() set_current_record('file', $file)
set_current_collection() set_current_record('collection', $collection)
set_current_item() set_current_record('item', $item)
get_current_item() get_current_record('item')
get_current_collection() get_current_record('collection')
get_current_file() get_current_record('file')
get_current_item_type() get_current_record('item_type')
set_current_item_type() set_current_record('item_type')
set_items_for_loop() set_loop_records('items', $items)
get_items_for_loop() get_loop_records('items')
has_items_for_loop() has_loop_records('items')
set_collections_for_loop() set_loop_records('collections', $items)
get_collections_for_loop() get_loop_records('collections')
has_collections_for_loop() has_loop_records('collections')
set_files_for_loop() set_loop_records('files', $items)
get_files_for_loop() get_loop_records('files')
has_files_for_loop() has_loop_records('files')
set_item_types_for_loop() set_loop_records('item_types', $itemTypes)
get_item_types_for_loop() get_loop_records('item_types')
has_item_types_for_loop() has_loop_records('item_types')
get_item_by_id() get_record_by_id('item', $id)
get_collection_by_id() get_record_by_id('collection', $id)
get_user_by_id() get_record_by_id('user', $id)
_select_from_table() get_table_options() with Zend's formSelect()
is_odd($num) n/a; use $num & 1
form_error() n/a; Use Zend Form Validations
collection_is_featured metadata('collection', 'featured')
collection_is_public metadata('collection', 'public')
button_to n/a; use Zend's form helpers or hand-write HTML buttons.
delete_button link_to($record, 'delete-confirm', 'Delete', ...)

Return Value

All functions that previously echoed output will now return the same output. For instance, instead of this:

<?php head(); ?>

you must echo the return value in your script:

<?php echo head(); ?>

Looping Records

All functions that used loop_records() have been consolidated into one function, loop(). This includes loop_items(), loop_files(), loop_collections(), etc. The basic structure of a loop has changed as well, so this:

<?php while ($item = loop_items()): ?>
<!-- do something -->
<?php endwhile; ?>

should now be written like this:

<?php foreach (loop('items') as $item): ?>
<!-- do something -->
<?php endforeach; ?>

Set/Get/Has Records for Loop

All functions that set, returned, or checked the existence of records for loop have been consolidated into three functions: set_loop_records(), get_loop_records(), and has_loop_records(). So, for example, this:

if (has_items_for_loop()) {
    $items = get_items_for_loop();

becomes this:

set_loop_records('items', $items);
if (has_loop_records('items')) {
    $items = get_loop_records('items');

Set/Get Current Record

All functions that set or returned the current record have been consolidated into two functions: set_current_record() and get_current_record(). So, for example, this:

$item = get_current_item();

becomes this:

set_current_record('item', $item);
$item = get_current_record('item');

Get Record by ID

All functions that got a record by its ID have been consolidated into one function: get_record_by_id(). So, for example, this:

$item = get_item_by_id($id);

becomes this:

$item = get_record_by_id('item', $id);

Form Functions

In addition to the above functions, the form functions like text(), label(), select(), radio(), etc. have been removed in Omeka 2.0. Instead, use the corresponding Zend Form Helpers. So, for example, this:

echo text(array('name'=>'title', 'class'=>'textinput', 'id'=>'title'), 'default title', 'Title');

becomes this:

echo $this->formLabel('title', 'Title');
echo $this->formText('title', 'default title', array('class'=>'textinput'));

Or, of course, it might be easier to just write the HTML yourself.


We no longer use Omeka_Context. Instead get the resource via Zend_Registry. So, for example,

$acl = Omeka_Context::getInstance()->acl;


$acl = Zend_Registry::get('bootstrap')->getResource('Acl');


We highly recommend that all processes that may run longer than a typical web process are sent to a job. The job will mediate the process, reducing the chance of timeout and memory usage errors that can happen even with the best written code. To run a job just write a class that contains the code to run, like so:

class YourJob extends Omeka_Job_AbstractJob
    public function perform()
        // code to run

You have two options on how to run the code: default and long-running. The default way is intended to run processes that, though are more processor-intensive than the typical web process, are usually not in danger of timing out. You can run these processes like so,


Your other option is intended for processes that will most likely result in a timeout error if run as a normal web script. Processes that import thousands of records or convert hundreds of images are examples of such processes. You can run these processes like so,


It's important to note that nothing that uses the job system should assume or require synchronicity with the web process. If your process has to be synchronous, it shouldn't be a job.

Phased Loading

In previous versions, long running processes were fired directly through a background process via ProcessDispatcher::startProcess(), which loaded resources (e.g. Db, Option, Pluginbroker) in phases. Phased loading is now removed in favor of loading resources when needed.

When using the background process adapter for your jobs (typically used for long running jobs), the following resources are pre-loaded for you: Autoloader, Config, Db, Options, Pluginbroker, Plugins, Jobs, Storage, Mail. If you need other resources, load them like so in your job:


Hooks and Filters

New Hooks

  • navigation_form
  • search_sql
  • public_append_to_home

Renamed Hooks

primary and secondary renamed content and sidebar for item and form show pages

after_upload_file no after_ingest_file

Removed Hooks

insert, update, save_form, and validate hooks have been removed (this supercedes what might be listed below)

In previous versions of Omeka, hook and filter callbacks accepted any number of arguments that could be useful during implementation. Now, in 2.0, we've consolidated these arguments into one array. Because of this change, writing your callbacks will be easier and self-documenting:

// Add a hook.
add_plugin_hook('after_save_item', 'my_hook_callback');
// Define the hook callback. Note the single $args array instead of
// the list of function arguments used in previous versions.
function my_hook_callback($args)
    // Extract the arguments only as needed.
    $item = $args['record'];
    // Do something.
// Add a filter.
add_filter(array('Display', 'Item', 'Dublin Core', 'Title'),
// Define the filter callback. Note the single $args array instead 
// of the list of function arguments used in previous versions.
function my_filter_callback($title, $args)
    // Extract the arguments only as needed.
    $itemRecord = $args['record'];
    $titleElementTextRecord = $args['element_text'];
    // Filter the value and return it...
    return $title;

The following hooks have been modified to accommodate the above changes. Hook names are followed by their available arguments (accessed via the $args keys).

Hooks that previously received a request object should now obtain that object within the callback itself like so:

    $request = Zend_Controller_Front::getInstance()->getRequest();

  • admin_append_to_users_form: form, user
  • general_settings_form: form
  • browse_tags: tags, for
  • *_browse_sql: select, params
  • define_routes: router
  • html_purifier_form_submission: purifier
  • browse_*: records
  • show_*: record
  • after_upload_file: file, item
  • after_ingest_file: file, item
  • admin_theme_footer
  • admin_theme_header
  • public_theme_footer
  • public_theme_header
  • public_theme_body
  • public_theme_page_header
  • public_theme_page_content
  • before_upload_files: item
  • add_*_tag: record, added
  • remove_*_tag: record, removed
  • make_*_public: record
  • make_*_not_public: record
  • make_*_featured: record
  • make_*_not_featured: record
  • items_batch_edit_custom: item, custom
  • admin_append_to_collections_form: collection
  • admin_append_to_collections_browse_primary: collections
  • admin_append_to_collections_show_primary: collection
  • admin_append_to_items_form_files: item
  • append_to_item_form: item
  • admin_append_to_items_browse_primary: items
  • admin_append_to_items_batch_edit_form
  • admin_append_to_items_show_secondary: item
  • admin_append_to_items_show_secondary: item
  • admin_append_to_items_form_tags: item
  • admin_append_to_items_form_collection: item
  • admin_append_to_files_form: file
  • admin_append_to_files_show_primary: file
  • admin_append_to_files_show_secondary: file
  • admin_append_to_item_types_form: item_type
  • admin_append_to_item_types_browse_primary: item_types
  • admin_append_to_item_types_show_primary: item_type
  • config: post
  • define_acl: acl
  • upgrade: old_version, new_version
  • install: plugin_id
  • before_save_form_record: record, post
  • after_save_form_record: record, post
  • before_insert_record: record
  • before_update_record: record
  • before_save_record: record
  • after_insert_record: record
  • after_update_record: record
  • after_save_record: record
  • before_delete_record: record
  • after_delete_record: record
  • before_save_form_*: record, post
  • after_save_form_*: record, post
  • before_insert_*: record
  • before_update_*: record
  • before_save_*: record
  • after_insert_*: record
  • after_update_*: record
  • after_save_*: record
  • before_delete_*: record
  • after_delete_*: record

The following filters have been modified to accommodate the above changes. Filter names are followed by their available arguments (accessed via the $args keys):

  • login_adapter: login_form
  • items_batch_edit_error: metadata, custom, item_ids
  • storage_path: filename, type
  • item_citation: item
  • display_search_filters: request_array
  • array(Display, Record): record, element_text
  • array(Form, Record): input_name_stem, value, record, element
  • element_form_display_html_flag: element
  • display_file: file, callback, options, wrapper_attributes
  • array(Save, Record): record, element
  • array(Flatten, Record): post_array, element
  • array(Validate, Record): text, record, element
  • theme_options: theme_name
  • admin_items_form_tabs: item
  • define_action_contexts: controller, context_switcher

New Filters

  • <model>_browse_params
  • record_metadata_elements
  • public_navigation_admin_bar
  • search_record_types
  • admin_navigation_global
  • admin_dashboard_stats
  • admin_dashboard_panels
  • item_search_filters

Changed Filters

Old Filter New Filter
display_setting_* display_option_*
array('Form' ....) array('ElementInput' . . . )

Removed Filters

  • admin_navigation_items_browse
  • admin_navigation_tags


Omeka 2.0 allows any record to be full-text searchable, not just items, but also files, collections, exhibits, etc.

Individual record indexing and bulk-indexing will only work on record types that have been registered via the new "search_record_types" filter:

// Tell Omeka where your search_record_types callback is. 
add_filter('search_record_types', 'your_search_record_types_callback');
// Accept an array and return an array.
function your_search_record_types_callback(array $searchableRecordTypes)
    // Register the name of your record class. The key should be the name 
    // of the record class; the value should be the human readable and 
    // internationalized version of the record type.
    $searchableRecordTypes['YourRecord'] = __('Your Record');
    return $searchableRecordTypes;

Follow this template to make your record searchable:

class YourRecord extends Omeka_Record_AbstractRecord
    // Add the search mixin during _initializeMixins() and after any mixins
    // that can add search text, such as Mixin_ElementText. Doing this
    // tells Omeka that you want this record to be searchable.
    protected function _initializeMixins()
        // Add the search mixin.
        $this->_mixins[] = new Mixin_Search($this);
    // Use the afterSave() hook to set the record's search text data.
    protected function afterSave($args)
        // A record's search text is public by default, but there are times
        // when this is not desired, e.g. when an item is marked as
        // private. Make a check to see if the record is public or private.
        if ($private) {
            // Setting the search text to private makes it invisible to
            // most users.
        // Set the record's title. This will be used to identify the record
        // in the search results.
        // Set the record's search text. Records that implement the
        // Mixin_ElementText mixin during _initializeMixins() will
        // automatically have all element texts added. Note that you
        // can add multiple search texts, which simply appends them.
    // The search results need a route to the record show page, so build 
    // a routing array here. You can also assemble the URL yourself using 
    // the URL view helper and return the entire URL as a string.
    // The abstract Omeka_Record_AbstractRecord class has this method,
    // so you only need to override it if the route to your record does 
    // something unusual
    public function getRecordUrl($action)
        if ('your-show-action' == $action) {
            return array(
                'module' => 'your-module', 
                'controller' => 'your-controller', 
                'action' => $action,
                'id' => $this->id, 
        return parent::getRecordUrl($action);

Indexing Your Records

Indexing means to collect, parse, and store data to facilitate fast and accurate searches. Omeka will index individual records as they are saved, but there are circumstances when your records are not indexed; for instance, when updating from an earlier version of Omeka.

In 2.0 we've added a feature that lets you index all your records at one time. Just go to the new search settings page on the settings tab and press the index button. This is done using a background process so you may continue administering your site while it indexes. The process may take a while to complete.

Customizing Search Type

Omeka now comes with three search query types: full text, boolean, and exact match. Full text and boolean use MySQL's native full text engine, while exact match searches for all strings identical to the query.

Plugin authors may customize the type of search by implementing the search_query_types filter. For example, if you want to implement a "ends with" query type that searches for records that contain at least one word that ends with a string:

// Tell Omeka where your search_query_types callback is. 
add_filter('search_query_types', 'your_search_query_types_callback');
// Accept an array and return an array.
function your_search_query_types_callback($queryTypes)
    // Register the name of your custom query type. The key should be the
    // type's GET query value; the values should be the human readable and
    // internationalized version of the query type.
    $queryTypes['ends_with'] = __('Ends with');
    return $queryTypes;

Then you must modify the search SQL, like so:

add_plugin_hook('search_sql', 'your_search_sql_callback');
function your_search_sql_callback($args)
    $params = $args['params'];
    if ('ends_with' == $params['query_type']) {
        $select = $args['select'];
        // Make sure to reset the existing WHERE clause.
        $select->where('`text` REGEXP ?', $params['query'] . '[[:>:]]');

Remember that you're searching against an aggregate of all texts associated with a record, not structured data about the record.

Plugin Hooks

The functions that theme developers should use to fire plugin hooks have been removed. Instead of, for example,

echo plugin_append_to_collections_show()




Previously, plugins had to add setUp code to make sure that their hooks and filters were reloaded on each test run. This is no longer required. Simply using the Plugin test helper will correctly re-load the hooks and filters every time.

As a side effect, plugin.php can no longer contain declarative code (declaring functions, classes, etc.). This was already an informal rule, with many plugins separating into plugin.php and functions.php files, but now the plugin loader assumes plugins are written this way. Using the Omeka_Plugin_AbstractPlugin class in a separate file will naturally cause this separation (and even allows you to omit plugin.php entirely).

Development Environment

The .htaccess file has added a

setEnv APPLICATION_ENV development

directive. Make sure that you update your .htaccess file.

Admin Views

Example structure of a fieldset within a form:

 <div class="field">
   <div id="administrator_email-label" class="two columns alpha"><label for="administrator_email" class="required">Administrator Email</label></div>
   <div class="inputs five columns omega"><input type="text" name="administrator_email" id="administrator_email" value="knguye27@gmu.edu <mailto:value=%22knguye27@gmu.edu>"></div>

Follows these guides: