Developing Generic Plugins for OJS 3.3, 3.4, and 3.5
Comprehensive step-by-step tutorial for building generic plugins in OJS. Includes complete boilerplate code, settings modal management, and locale string configuration.
Real Problem
Developers starting with OJS plugin development struggle with cryptic namespace structures, missing version.xml requirements, and breaking API changes between OJS 3.3 and 3.4.
Practical Solution
Follow this end-to-end coding tutorial: scaffold the plugin directory, author compliant version.xml descriptors, implement the main plugin class, create settings forms, and handle multi-language locale strings.
Step-by-Step Construction of a Production Generic Plugin
To demonstrate how to build an enterprise-grade generic plugin, we will create a complete working plugin named JournalNotifierPlugin that logs publication events and injects custom metadata badges into published articles.
Directory Structure
Navigate to plugins/generic/ and create the directory journalNotifier:
plugins/generic/journalNotifier/
├── index.php
├── JournalNotifierPlugin.inc.php
├── version.xml
├── locale/
│ └── en_US/
│ └── locale.xml
└── templates/
└── settings.tpl
1. The Entry Point: index.php
This file instantiates the main plugin class:
<?php
require_once('JournalNotifierPlugin.inc.php');
return new JournalNotifierPlugin();
2. The Manifest: version.xml
OJS uses version.xml to track database installation and compatibility:
<?xml version="1.0" encoding="UTF-8"?>
<version>
<application>journalNotifier</application>
<type>plugins.generic</type>
<release>1.0.0.0</release>
<date>2024-01-01</date>
<lazy-load>1</lazy-load>
<sitewide>0</sitewide>
</version>
3. The Main Plugin Class: JournalNotifierPlugin.inc.php
<?php
import('lib.pkp.classes.plugins.GenericPlugin');
class JournalNotifierPlugin extends GenericPlugin {
public function register($category, $path, $mainContextId = null) {
$success = parent::register($category, $path, $mainContextId);
if ($success && $this->getEnabled($mainContextId)) {
// Hook into the article publication event
HookRegistry::register('Publication::publish', [$this, 'onArticlePublished']);
}
return $success;
}
public function onArticlePublished($hookName, $args) {
$newPublication = $args[0];
$submissionId = $newPublication->getData('submissionId');
// Custom business logic: log or trigger external webhook
error_log("JournalNotifier: Submission {$submissionId} was successfully published!");
return false; // Allow other hooked plugins to continue execution
}
public function getDisplayName() {
return __('plugins.generic.journalNotifier.displayName');
}
public function getDescription() {
return __('plugins.generic.journalNotifier.description');
}
}
4. Localization: locale/en_US/locale.xml
<?xml version="1.0" encoding="UTF-8"?>
<locale name="en_US" full_name="U.S. English">
<message key="plugins.generic.journalNotifier.displayName">Journal Publication Notifier</message>
<message key="plugins.generic.journalNotifier.description">Automatically logs and notifies external services when articles are published.</message>
</locale>
5. Testing and Activation
Once uploaded, navigate to Settings > Website > Plugins > Generic Plugins. Locate "Journal Publication Notifier" and toggle the checkbox to enable it. Publish a test submission and monitor your server error log to observe your custom hook firing in real time.
Step-by-Step Action Plan
Scaffold Directory Structure
Create plugins/generic/journalNotifier/ with index.php, version.xml, JournalNotifierPlugin.inc.php, and locale files.
Write version.xml Descriptor
Define the application name, category type plugins.generic, release version 1.0.0.0, and lazy-load settings.
Implement Plugin Class & Registration
Extend GenericPlugin, define getDisplayName() and getDescription(), and register core hook listeners.
Build Administrative Settings Form
Implement getActions() and manage() methods to display a settings modal allowing editors to configure options.
Test Installation & Execution
Enable the plugin in Journal Settings > Plugins and verify hook execution in the application logs.
Questions People Usually Ask
Why does my new plugin not appear in the OJS Plugin list?
The most common causes are a syntax error in version.xml, mismatched class names between index.php and the main class file, or incorrect file permissions preventing PHP from reading the directory.
How do I handle multi-language translations in a custom plugin?
Place XML or PO translation files in locale/en_US/locale.xml (or locale/en/locale.po in OJS 3.4). Reference strings in templates using {translate key="plugins.generic.myPlugin.name"}.
What changed in plugin file naming in OJS 3.4?
In OJS 3.3, PHP class files required the .inc.php extension (e.g., MyPlugin.inc.php). In OJS 3.4+, standard PSR-4 naming and .php extensions are used (e.g., MyPlugin.php).
Build Enterprise Plugins with AcademiaTech
We develop custom, well-documented plugins adhering to strict PKP coding standards for university presses and scholarly societies.