Appearance
Developer Events Reference
NXP Easy Forms dispatches Joomla events at every meaningful lifecycle point so plugins can observe and modify behaviour without forking the component. This document is the canonical surface. Every event listed here is part of the public contract; events that ship in the codebase but aren't listed here are internal-only and may change without notice.
How to listen
Standard Joomla plugin pattern. Implement Joomla\Event\SubscriberInterface or hook directly via JEventDispatcher::register / Joomla's SubscriberInterface::getSubscribedEvents. Example skeleton:
php
namespace MyVendor\Plugin\System\NxpHooks\Extension;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Event\Event;
use Joomla\Event\SubscriberInterface;
final class NxpHooks extends CMSPlugin implements SubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
'onNxpEasyFormsBeforeEmailSent' => 'addBcc',
'onNxpEasyFormsAfterSubmission' => 'logToCrm',
];
}
public function addBcc(Event $event): void
{
$message = $event['message'];
$message['recipients'][] = 'audit@example.com';
$event['message'] = $message;
}
public function logToCrm(Event $event): void
{
// Read-only; informational events.
}
}Event types
- Filter: listener can mutate the payload. Read the value via
$event['key'], mutate locally, write back via$event['key'] = $newValue. The component re-reads the event immediately after dispatch and uses the mutated value. - Informational: listener observes only. Mutations are ignored (or undefined behaviour). Use these for audit logging, metrics, outbound notifications.
Stability guarantees
- Event names are stable across minor releases.
- Payload keys may be added in minor releases. Listeners must not assume the payload is the exact set of keys documented today.
- Existing payload keys, types, and semantics are stable across minor releases. Breaking changes are reserved for major versions and called out in the changelog.
- Filter events specify exactly which keys the component reads back after dispatch; mutating other keys is a no-op.
Submission lifecycle (site-side)
Events fired during a submission's pass through SubmissionService::handle(), in execution order.
onNxpEasyFormsBeforeSubmission
Type: Informational
Fires: SubmissionService.php:204, after CSRF/honeypot/min-time checks pass and after onNxpEasyFormsFilterSubmissionRequest.
Payload:
php
[
'formId' => int, // form being submitted
'form' => array<string,mixed>, // full form definition
'request' => array<string,mixed>, // raw incoming request data
'context' => array<string,mixed>, // ip_address, user_agent, files, ...
]Use cases: audit logging, request-counting, early metrics.
onNxpEasyFormsFilterSubmissionRequest
Type: Filter
Fires: SubmissionService.php:531, before validation, after CSRF.
Payload:
php
[
'value' => array<string,mixed>, // mutable: the request data
'formId' => int,
'form' => array<string,mixed>,
'context' => array<string,mixed>,
]Read back: $event['value'] (the mutated request data).
Use cases: rewrite incoming values before validation (translate field aliases, normalize phone numbers, drop legacy keys).
onNxpEasyFormsFilterMinSubmissionTime
Type: Filter (numeric)
Fires: SubmissionService.php:497, when computing the minimum elapsed-time-since-render bot-deterrent threshold.
Payload:
php
[
'value' => int, // mutable: minimum seconds (default 2)
'formId' => int,
'context' => array<string,mixed>,
]Read back: $event['value'] (the new threshold in seconds).
onNxpEasyFormsFilterRecaptchaScore
Type: Filter (numeric)
Fires: CaptchaService.php:139, after Google returns the v3 score.
Payload:
php
[
'value' => float, // mutable: minimum acceptable score 0.0–1.0
'formId' => int,
]Use cases: lower the threshold for low-risk forms or raise it for high-spam pages.
onNxpEasyFormsFilterMaxUploadSize
Type: Filter (numeric)
Fires: FileValidator.php:339, per file being validated.
Payload:
php
[
'value' => int, // mutable: max bytes
'field' => array<string,mixed>, // the field config
]onNxpEasyFormsFilterAllowedFileTypes
Type: Filter (array)
Fires: FileValidator.php:349, per file being validated.
Payload:
php
[
'value' => array<string,string>, // mutable: list of allowed MIME types
'field' => array<string,mixed>,
]Note: the validator strips dangerous MIME types (executable, script) from the result regardless of what the listener returns. A warning is logged when a listener tries to allow a forbidden type.
onNxpEasyFormsFilterMaxImageDimension
Type: Filter (numeric)
Fires: FileValidator.php:412, only for image/* MIME types.
Payload:
php
[
'value' => int, // mutable: max width OR height in pixels (default 4096)
'field' => array<string,mixed>,
]onNxpEasyFormsFilterSanitizedSubmission
Type: Filter
Fires: SubmissionService.php:564, after field validation + sanitisation, before persistence.
Payload:
php
[
'value' => array<string,mixed>, // mutable: sanitised submission values
'formId' => int,
'form' => array<string,mixed>,
'context' => array<string,mixed>,
'field_meta' => array<int,array<string,mixed>>,
]Use cases: post-validation enrichment (geo-resolve IP into city, hash a value, etc.).
onNxpEasyFormsBeforeIntegrationDispatch (1.3+)
Type: Informational
Fires: SubmissionService.php, once per enabled integration before the queue/sync routing decision is acted upon. Also fires once for the standalone webhook (integrationId = 'webhook') when enabled.
Payload:
php
[
'integrationId' => string, // 'webhook' | 'make' | 'mailchimp' | 'hubspot' | 'salesforce' | 'brevo' | ...
'settings' => array<string,mixed>,
'form' => array<string,mixed>,
'payload' => array<string,mixed>,
'context' => array<string,mixed>,
'field_meta' => array<int,array<string,mixed>>,
'mode' => string, // 'queued' | 'sync'
]Use cases: count outbound dispatch attempts, gate per-tenant throttling, observe before any side effect.
onNxpEasyFormsAfterIntegrationDispatch (1.3+)
Type: Informational
Fires: Two paths, same shape:
- Sync path:
SubmissionService.php, afterdispatcher->dispatch()returns or throws (joomla_article, user_registration, user_login, salesforce, teams). - Queued path:
IntegrationQueue.php::dispatchOne(), after the queued job's actual dispatch attempt completes (success, transient failure, or terminal drop).
Payload:
php
[
'integrationId' => string,
'settings' => array<string,mixed>,
'form' => array<string,mixed>,
'payload' => array<string,mixed>,
'context' => array<string,mixed>,
'field_meta' => array<int,array<string,mixed>>,
'mode' => string, // 'sync' | 'queued'
'result' => string, // 'dispatched' | 'failed' | 'failed_will_retry' | 'requeued'
'error' => ?string, // exception message when result != 'dispatched'
]Result values:
dispatched: dispatcher returned without throwing.failed: terminal failure, queue ran out of retry attempts and dropped the job. Only fires on the queued path.failed_will_retry: transient failure, queue rescheduled the job with backoff. Only fires on the queued path.requeued: sync attempt threw, dispatcher catch-block enqueued for later retry. Only fires on the sync path.
Use cases: per-integration success/failure metrics, alerting on terminal failures, audit logging.
Not fired for: integrations whose dispatcher isn't registered with IntegrationManager. Those still emit the legacy onNxpEasyFormsIntegrationDispatch instead (see below).
onNxpEasyFormsIntegrationDispatch (legacy)
Type: Informational
Fires: SubmissionService.php:733, only when no dispatcher is registered for the integration id. Pre-1.3 this was the only mechanism for plugins to observe integration dispatch; kept for backwards compat.
Payload:
php
[
'integrationId' => string,
'settings' => array<string,mixed>,
'form' => array<string,mixed>,
'payload' => array<string,mixed>,
'context' => array<string,mixed>,
'field_meta' => array<int,array<string,mixed>>,
]For new code: use onNxpEasyFormsBeforeIntegrationDispatch instead.
onNxpEasyFormsBeforeEmailSent (1.3+)
Type: Filter
Fires: EmailService.php, after the message is composed and before deliver(). Triggered from BOTH the admin-notification path (dispatchSubmission()) and the new submitter-confirmation path (sendOne()); use email_kind to differentiate.
Payload:
php
[
'form' => array<string,mixed>,
'submission' => array<string,mixed>,
'context' => array<string,mixed>,
'message' => array{
recipients:array<string>, subject:string, html:string,
text:string, from_name:string, from_email:string
}, // mutable
'reply_to' => ?string, // mutable; v1 string-only — see below
'email_kind' => 'notification' | 'submitter_confirmation',
]Read back: $event['message'] and $event['reply_to'].
Use cases: BCC injection, transactional template rerouting, per-tenant header tweaks, recipient overrides. Listeners that should only act on owner-facing notification mail (e.g. BCC the support team on every notification, but not on confirmation copies sent to the visitor) MUST branch on email_kind === 'notification' to avoid also intercepting the submitter-confirmation flow.
Not fired when: send_email is false in the form options for the notification path (short-circuit returns before this event). For the confirmation path, the dispatcher gate fires before sendOne() is called — no event when the option is disabled, the capability is unavailable, or the recipient is missing/invalid.
Reply-to resolution differs per kind:
email_kind === 'notification':- configured
email_reply_to - first valid email value found by scanning
field_meta(the implicit "reply to whoever submitted the form" fallback) context['submission_email']null
- configured
email_kind === 'submitter_confirmation':- configured
submitter_email.reply_to_emailonly - never field-scans, never falls back to the submitter's own email (that would loop replies back to the visitor — wrong semantic for a confirmation message)
- configured
v1 reply-to is string-only. The reply_to value is a bare email address or null. Display-name support (e.g. "Acme Sales <sales@acme.example>") is deferred to v2 because it requires widening the internal transport to carry a structured value across all 8 provider implementations. v2 will introduce an additive reply_to_name payload key alongside reply_to; existing listeners that read reply_to as a string keep working unchanged.
onNxpEasyFormsAfterEmailSent (1.3+)
Type: Informational
Fires: EmailService.php, after deliver() returns. Triggered from BOTH the admin-notification path (dispatchSubmission()) and the submitter-confirmation path (sendOne()); use email_kind to differentiate.
Payload:
php
[
'form' => array<string,mixed>,
'submission' => array<string,mixed>,
'context' => array<string,mixed>,
'message' => array<string,mixed>, // post-mutation, what was actually delivered
'reply_to' => ?string,
'result' => array{sent:bool, error?:?string},
'email_kind' => 'notification' | 'submitter_confirmation',
]Use cases: audit log of every email sent, metrics on send failures, retry counters. Same email_kind branching rule as the before event: listeners that audit only one direction of mail should filter on the discriminator.
onNxpEasyFormsAfterSubmission
Type: Informational
Fires: SubmissionService.php:418, end of the submission flow, after persistence + email + integration dispatch.
Payload:
php
[
'formId' => int,
'sanitised' => array<string,mixed>, // final stored values
'result' => array<string,mixed>, // success/uuid/message/etc
'form' => array<string,mixed>,
'context' => array<string,mixed>,
]Per-integration payload filters
These let plugins rewrite the JSON body of a specific outbound integration before it's serialised and sent. All have the same shape and behave identically; only the event name varies per dispatcher.
| Event | Vendor |
|---|---|
onNxpEasyFormsFilterWebhookPayload | Generic webhook + standalone |
onNxpEasyFormsFilterMailchimpPayload | Mailchimp |
onNxpEasyFormsFilterHubspotPayload | HubSpot |
onNxpEasyFormsFilterSalesforcePayload | Salesforce |
onNxpEasyFormsFilterBrevoPayload | Brevo |
Type: Filter
Payload:
php
[
'payload' => array<string,mixed>, // mutable: the JSON body
'form' => array<string,mixed>,
'submission' => array<string,mixed>,
'context' => array<string,mixed>,
'field_meta' => array<int,array<string,mixed>>,
]Read back: $event['payload'].
Use cases: per-vendor body customisation (extra Mailchimp tags from a custom field, HubSpot association mappings, Salesforce record-type overrides).
onNxpEasyFormsWebhookFailed
Type: Informational
Fires: WebhookDispatcher.php:179, on transport or HTTP error.
Payload:
php
[
'endpoint' => string, // REDACTED, webhook secrets are stripped
'integration' => string, // 'webhook'
'error' => string, // REDACTED message
'context' => array<string,mixed>,
'form' => array<string,mixed>,
'payload' => array<string,mixed>,
]Note: endpoint and error are passed through WebhookUrl::redact* before this event fires. Listeners that previously read the raw URL get a host + hash form instead. This was a 1.1.0 contract change to close a credential-leak vector.
User events
onNxpEasyFormsUserRegistered
Type: Informational
Fires: UserRegistrationHandler.php:229, after a user is created via the User Registration integration.
Payload: Joomla-style positional [$userId, $data].
onNxpEasyFormsUserLoggedIn
Type: Informational
Fires: UserLoginHandler.php:149, after a successful login via the User Login integration.
Payload: Joomla-style positional [$payload] containing the login result.
Form-builder lifecycle (admin-side)
onNxpEasyFormsFilterDuplicateTitle
Type: Filter (string)
Fires: FormRepository.php:220, when an admin duplicates a form, to derive the duplicate's title.
Payload:
php
[
'value' => string, // mutable: candidate title
'original' => array<string,mixed>, // the source form being duplicated
]Read back: $event['value'].
Default: appends (Copy) (or localised equivalent) to the original title; listener can replace with any string.
onNxpEasyFormsBeforeFormSave (1.3+)
Type: Filter
Fires: FormModel.php, in save() before parent::save(). Covers both standard Joomla form-save and AJAX builder save (both routes go through the same model).
Payload:
php
[
'formId' => int, // 0 if creating a new form
'data' => array<string,mixed>, // mutable: the about-to-be-saved data
'wasNew' => bool, // true when formId === 0
]Read back: $event['data'].
Use cases: force tags, inject audit metadata, normalize schema before persistence.
Cannot: veto the save. For veto semantics use Joomla's existing onContentBeforeSave event with setError().
onNxpEasyFormsAfterFormSave (1.3+)
Type: Informational
Fires: FormModel.php, after parent::save() succeeds. Does NOT fire on save failure.
Payload:
php
[
'formId' => int, // the persisted id (always > 0)
'data' => array<string,mixed>, // post-save data shape
'wasNew' => bool,
]Versioning
This document tracks the public event contract as of NXP Easy Forms 1.3. Events marked (1.3+) are new in this release. Events without a version tag predate the contract document; they have shipped since 1.0 or 1.1 and are stable.
For the broader extension architecture (capability registration via plugins, validation gate event, client-side registry) see docs/public-extension-contract.md.