OwnPay’s plugin system is built around a central event bus implemented inDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/own-pay/OwnPay-Documentation/llms.txt
Use this file to discover all available pages before exploring further.
OwnPay\Event\EventManager. Rather than a separate event-dispatch library, OwnPay unifies hooks and events under a single API with two hook types — actions (fire-and-forget notifications) and filters (pipeline mutation) — both managed by the same EventManager instance.
Understanding the difference between how you subscribe to events (in your plugin’s register() method) versus how the core fires them (using doAction and applyFilter) is the foundation of plugin development.
Hooks vs. Events: Key Distinction
Filter Hooks (Synchronous)
Synchronous pipeline. The core pauses, runs all registered listeners in priority order, collects the final return value, and continues. The calling code waits for the result. Use for modifying data: amounts, templates, context arrays, fee rules.
Action Hooks (Async Notifications)
Fire-and-forget notification. The core fires the action and continues immediately. Listener return values are discarded. Error isolation ensures one failing listener never blocks the next. Use for side effects: sending SMS, logging, dispatching jobs.
payment.amount.calculate filter directly delays checkout response time, whereas the same query inside a payment.transaction.completed action does not (though queueing it is still best practice).
Full EventManager API
TheEventManager instance is injected into PluginInterface::register() as the first argument. Never instantiate it directly in your plugin.
Subscribing to Events
addAction(string $hook, callable $callback, int $priority = 10): void
Register a listener on an action hook. Called by plugins in register().
$hook: The event name (e.g.,'payment.transaction.completed').$callback: Any PHP callable — closure,[$object, 'method'], or named function.$priority: Execution order. Lower = runs first. Default is10.
addFilter(string $hook, callable $callback, int $priority = 10): void
Register a listener on a filter hook. The callback receives the current value as its first argument and must return a value of the same type.
Firing Events (Core Only)
These methods are called by OwnPay core, not by plugins. They are documented here so you can understand what the core is doing when your listeners are invoked.doAction(string $hook, mixed ...$args): void
Fires an action hook. All registered listeners run; return values are discarded.
applyFilter(string $hook, mixed $value, mixed ...$args): mixed
Runs the value through all registered filter listeners. Returns the final transformed value. applyFilters() is an alias — both are identical.
Removing Listeners
Inspection and Debugging
Execution Model
Owner Attribution and Brand-Context Scoping
WhenPluginLoader calls PluginInterface::register(), it wraps the call with events->pushOwner($slug). Every addAction() or addFilter() call made during register() is attributed to the plugin’s slug.
At dispatch time, before invoking each listener, EventManager calls isOwnerActive($owner), which checks PluginRegistry::isPluginActive($owner, $brandId) against the current brand context. If the plugin is not active for the current brand, the listener is silently skipped:
Error Isolation
Every listener invocation is wrapped intry/catch(Throwable). If a plugin listener throws, EventManager:
- Logs the error:
[OwnPay] Hook error in "{hook}" (owner: {slug}): {message} in {file}:{line}. - Continues to the next listener without crashing.
db.query.before and the resulting SQL fails PluginSandbox::validateSql(), the RuntimeException is re-thrown and propagates to the caller. A plugin attempting to access core tables must be stopped immediately.
Re-entrancy Guard
EventManager includes a resolvingOwnerActive boolean guard to prevent infinite recursion if isOwnerActive() triggers another hook that re-triggers isOwnerActive().
Twig Hook Points
Twig templates use thehook() helper registered by TwigExtensions to execute action hooks and capture their output:
hook() helper calls ob_start(), fires doAction($hookName), captures the output buffer, passes it through TwigExtensions::sanitizeHookOutput(), and returns the sanitized HTML.
sanitizeHookOutput() strips: <script>, <iframe>, <object>, <embed>, <form>, <base>, <meta>, <link>, inline event handlers (onclick, onload, etc.), and javascript: URIs.
Event Groups Reference
payment.* — Payment Lifecycle
payment.transaction.created, payment.transaction.completed, payment.transaction.failed, payment.transaction.cancelled, payment.transaction.before_create, payment.amount.calculate, payment.fee.calculate, payment.intent.created, payment.intent.expired, refund.created, ledger.entry.created, dispute.opened, dispute.resolved.
customer.* — Customer Actions
customer.created, customer.updated, customer.deleted.
gateway.* — Gateway Operations
gateway.capture.before, gateway.capture.after, gateway.webhook.{slug}, gateway.manual.render, gateway.manual.verify.
webhook.* — Outbound Webhook Delivery
webhook.delivery.success, webhook.delivery.failed.
user.* / auth.* — User and Authentication Events
auth.login.before, auth.login.attempt, auth.login.success, auth.login.failed, auth.logout, auth.forgot_password.
system.* — System Events
system.boot, system.shutdown, system.request, system.response, system.middleware.pipeline, system.route.matched, system.routes.register, system.cron.before, system.cron.after.
Practical Patterns
Pattern: Outbound HTTP on Payment Completion
Pattern: Register a Custom Communication Channel
Pattern: Intercept and Modify Email Templates
Pattern: Identify Your Plugin During Execution
Guidelines and Constraints
| Rule | Detail |
|---|---|
Register in register() only | Hooks in boot() or the constructor bypass brand-context activation and are not attributed to your slug. |
| Return same type in filters | Returning null from a string filter corrupts downstream logic. Always return the correct type. |
| BCMath for financial values | payment.amount.calculate and payment.fee.calculate must use bcadd()/bcmul()/bcdiv(). Never cast to float. |
| Never post to the ledger from hooks | Do not call LedgerService::postEntries() inside a hook. Use it from services or job runners only. |
| Escape all echoed HTML | Action hooks that echo must escape user-sourced values with htmlspecialchars($value, ENT_QUOTES, 'UTF-8'). |
No eval() or OS commands | eval, exec, shell_exec, system, passthru, popen, proc_open, pcntl_exec, dl, assert, create_function are blocked by token scanning. |