Hook Architecture in OJS: Extending Core Functionality
Deep-dive technical guide into the PKP HookRegistry framework. Explains hook registration, callback parameters, return value conventions, and essential core hooks.
Real Problem
Developers wanting to alter editorial decisions, email templates, or article landing pages often assume they must fork OJS core code, leading to unmaintainable systems.
Practical Solution
Master the PKP HookRegistry facade: learn how to inspect available core hooks, register callbacks, alter input/output parameters by reference, and control execution propagation.
The Design Pattern: Event-Driven Interception
In enterprise software architecture, the Observer pattern allows components to listen for and respond to system events without tight coupling. In Open Journal Systems, this pattern is implemented through the static HookRegistry class.
Throughout the OJS lifecycle—from routing HTTP requests to saving submission records—core classes invoke:
HookRegistry::call('HookName', [&$param1, &$param2, ...]);
Any active plugin that registered a listener for 'HookName' will have its callback invoked sequentially, with the parameters passed directly into the callback function.
Critical Core Hooks in OJS
1. Template & Presentation Hooks
- TemplateManager::display: Fires before any Smarty template is rendered to the browser. Ideal for injecting custom CSS, JavaScript libraries, or global template variables.
- Templates::Article::Details::Reference: Fires during article landing page rendering, allowing custom citation formatting or external reference badge injection.
2. Submission Lifecycle Hooks
- Submission::submit: Fires immediately after an author completes manuscript submission.
- Publication::publish: Triggered when an editor clicks "Publish" on an issue or individual article.
- EditorDecisionHandler::recordDecision: Fires when an editor records an editorial decision (Accept, Revisions Required, Decline).
3. Email & Notification Hooks
- Mail::send: Fires prior to transmitting any outbound email, allowing custom footer injection, dynamic BCC addition, or routing through external API services.
Writing an Advanced Hook Callback
Consider a real-world scenario where a university press wants to automatically notify a Slack or Teams channel whenever an editor accepts a manuscript:
public function register($category, $path, $mainContextId = null) {
if (parent::register($category, $path, $mainContextId)) {
if ($this->getEnabled($mainContextId)) {
HookRegistry::register('EditorAction::recordDecision', [$this, 'onEditorialDecision']);
}
return true;
}
return false;
}
public function onEditorialDecision($hookName, $args) {
$submission = $args[0];
$decision = $args[1];
if ($decision['decision'] === SUBMISSION_EDITOR_DECISION_ACCEPT) {
$this->notifyEditorialChannel("Manuscript #{$submission->getId()} has been ACCEPTED!");
}
return false; // Continue normal OJS processing
}
By leveraging the hook architecture, the journal achieves complex automated integration while keeping the core OJS installation completely pure.
Step-by-Step Action Plan
Locate Target Core Hook
Search the PKP codebase for HookRegistry::call() statements at the exact point where customization is needed.
Register Callback Method
Use HookRegistry::register("HookName", [$this, "callbackMethod"]) in your plugin’s register() lifecycle.
Inspect Callback Arguments
Analyze the $args array passed to your callback to extract models, request objects, or output buffers.
Modify Objects by Reference
Update entity properties or inject additional template variables into the active context.
Control Propagation via Return Values
Return false to let other hooks execute, or return true to halt further hook processing and short-circuit core execution.
Questions People Usually Ask
What does returning true vs false from an OJS hook callback mean?
Returning false indicates that your callback executed normally and allows subsequent registered hooks to execute. Returning true signals that your callback handled the event completely and instructs OJS to halt further hook processing.
Can hooks be used to modify SQL queries executed by OJS?
Yes! Core DAO classes frequently invoke hooks before executing queries, allowing plugins to append WHERE clauses, change sort orders, or alter result sets.
How can I discover which hooks are firing on a specific page request?
You can register a wildcard or temporary debugging listener on HookRegistry::call() to log all executing hook names to the PHP error log during page generation.
Need Advanced Architectural Customization?
AcademiaTech architects clean, hook-driven extensions for complex university publishing requirements.