Invision Community lets an application react to things that happen elsewhere in the suite — a member registering, a topic being posted, an invoice being paid — by registering a listener. There are 58 hooks across eight listener types.
They are not documented anywhere. The only record that a hook exists is an @method annotation in the abstract class, so this page is a transcription of those, checked against the code that fires them.
Registering a listener
data/listeners.json in your application, keyed by class name:
{
"MemberEvents": {
"type": "MemberListenerType",
"classname": "IPS\\myapp\\listeners\\MemberEvents",
"extends": "IPS\\Member"
}
}
Then the class itself. Implement only the hooks you want — the rest are inherited and do nothing.
namespace IPS\myapp\listeners;
use IPS\Events\ListenerType\MemberListenerType;
use IPS\Member;
class MemberEvents extends MemberListenerType
{
public static string $class = 'IPS\Member';
public function onCreateAccount( Member $member ): void
{
// ...
}
}
Every page on the site breaks if you omit $class
Event::loadListeners() calls getExtendedClass() on every registered listener from every application. A listener without public static string $class therefore does not break your application — it breaks the entire site, on every request.
The error names the dispatcher, not the listener that caused it, so there is nothing in the trace pointing at the responsible app.
The first argument is not what you expect
This is the trap that matters most, because it fails silently.
Hooks are fired like this:
Event::fire( 'onPaid', $item, array( $this ) );
The subject is passed first and everything else follows. For member and content hooks that reads naturally — the member or the content object comes first. For Commerce package hooks it does not: the first argument is the InvoiceItem, and the object you actually want is the second.
// WRONG - and it will not error
public function onPaid( $purchase ): void
{
// $purchase is really the INVOICE ITEM
}
// RIGHT
public function onPaid( $item, $invoice = null ): void
{
$member = $invoice->member; // Invoice::get_member() returns a Customer
}
PHP discards extra arguments to a non-variadic method, so the wrong signature raises nothing at all. You get an object of the wrong type, your property lookups return null, and the feature simply never runs. No log entry, no exception, no clue.
Note which object arrives second, because it differs by hook:
onPaid,onUnpaid,onAddToInvoice,onInvoiceCancel— the InvoiceonRenew,onExpire,onExpireWarning,onCancel,onReactivate,onChange,onTransfer,onDelete— the PurchaseonPurchaseGenerated— the Purchase, then the Invoice
Both Invoice and Purchase expose member, so one accessor covers either once you are holding the right object.
Listeners run inside the operation, so never throw
A listener runs in the middle of somebody registering an account or paying an invoice. An uncaught exception in your hook becomes their failed registration. Wrap the body and swallow:
public function onCreateAccount( Member $member ): void
{
try
{
// your work
}
catch ( \Throwable $e )
{
// never let an extension be the reason a registration fails
}
}
For the same reason, do not do slow work here. Queue it.
The catalogue
Signatures below are transcribed from the @method annotations on each listener type. Class names are shortened for readability.
MemberListenerType — 22 hooks
Extends IPS\Member.
onCreateAccount( Member $member ) onValidate( Member $member ) onLogin( Member $member ) onLogout( Member $member, Url $returnUrl ) onProfileUpdate( Member $member, array $changes ) onSetAsSpammer( Member $member ) onUnSetAsSpammer( Member $member ) onMerge( Member $member, Member $member2 ) onDelete( Member $member ) onEmailChange( Member $member, string $new, string $old ) onPassChange( Member $member, string $new ) onJoinClub( Member $member, Club $club ) onLeaveClub( Member $member, Club $club ) onEventRsvp( Member $member, Event $event, int $response ) onReact( Member $member, Content $content, Reaction $reaction ) onUnreact( Member $member, Content $content ) onFollow( Member $member, object $object, bool $isAnonymous ) onUnfollow( Member $member, object $object ) onCourseComplete( Member $member, Course $course ) onModuleComplete( Member $member, Module $module ) onLessonComplete( Member $member, Lesson $lesson ) onQuizComplete( Member $member, Quiz $quiz )
ContentListenerType — 10 hooks
Extends any content class, e.g. IPS\forums\Topic.
onBeforeCreateOrEdit( Content $object, array $values, bool $new = FALSE ) onCreateOrEdit( Content $object, array $values, bool $new = FALSE ) onDelete( Content $object ) onStatusChange( Content $object, string $action ) onMerge( Content $object, array $items ) onItemMove( Item $item, Model $oldContainer, bool $keepLink = FALSE ) onCommentMove( Comment $comment, Item $oldItem, bool $skip = FALSE ) onItemView( Item $item ) onItemSplit( Item $item, Item $oldItem ) onReport( Content $object, Report $report )
PackageListenerType — 13 hooks
Extends IPS\nexus\Invoice\Item.
onAddToInvoice( InvoiceItem $item, Invoice $invoice ) onPaid( InvoiceItem $item, Invoice $invoice ) onUnpaid( InvoiceItem $item, Invoice $invoice, string $status ) onInvoiceCancel( InvoiceItem $item, Invoice $invoice ) onPurchaseGenerated( InvoiceItem $item, Purchase $purchase, Invoice $invoice ) onRenew( InvoiceItem $item, Purchase $purchase, int $cycles ) onExpireWarning( InvoiceItem $item, Purchase $purchase ) onExpire( InvoiceItem $item, Purchase $purchase ) onCancel( InvoiceItem $item, Purchase $purchase ) onReactivate( InvoiceItem $item, Purchase $purchase ) onChange( InvoiceItem $item, Purchase $purchase, Package $newPackage, int|RenewalTerm $chosenRenewalOption = NULL ) onTransfer( InvoiceItem $item, Purchase $purchase, Member $newCustomer ) onDelete( InvoiceItem $item, Purchase $purchase )
PollListenerType — 5 hooks
Extends IPS\Poll.
onCreateOrEdit( Poll $poll ) onDelete( Poll $poll ) onVote( Poll $poll, Vote $vote ) onVoteRecount( Poll $poll ) onStateChange( Poll $poll, string $state )
InvoiceListenerType — 3 hooks
Extends IPS\nexus\Invoice.
onStatusChange( Invoice $invoice, string $status ) onCreateAccountForGuest( Invoice $invoice, Member $member, array $guestData ) onCheckout( Invoice $invoice, string $step )
ClubListenerType — 3 hooks
Extends IPS\Member\Club.
onCreate( Club $club ) onEdit( ?Club $club ) onDelete( Club $club )
FileListenerType — 1 hooks
Extends IPS\File.
onDownload( File $file, object $content, array $extra )
NodeListenerType — 1 hooks
Extends IPS\Node\Model.
onDelete( Model $node )
Finding hooks yourself
grep -h '@method' system/Events/ListenerType/*.php
That is the whole index. If a hook is not in that output, it does not exist.
Verified against
Invision Community 5.0.19, by reading system/Events/ListenerType/*.php and the code that fires each hook. Argument order for the Commerce hooks was confirmed against applications/nexus/sources/Invoice/Invoice.php.
Recommended Comments