Skip to content
View in the app

A better way to browse. Learn more.

ernestdefoe.online

A full-screen app on your home screen with push notifications, badges and more.

To install this app on iOS and iPadOS
  1. Tap the Share icon in Safari
  2. Scroll the menu and tap Add to Home Screen.
  3. Tap Add in the top-right corner.
To install this app on Android
  1. Tap the 3-dot menu (⋮) in the top-right corner of the browser.
  2. Tap Add to Home screen or Install app.
  3. Confirm by tapping Install.
ernestdefoe.online

Extensions, themes & support for Flarum and Invision Community

Vibe coding for the community web. Report a bug, request a feature, or dig into the source — this is where the tools you use get built, in the open.

We do custom Bespoke Invision Community apps. If you have an idea for something you want then use the contact form to get in touch with us.
Knowledge base

Things that cost me a day, so they cost you none

Working notes from building Invision Community and Flarum applications. Mostly the failures that give no error at all — the ones where everything installs cleanly and quietly does the wrong thing.

92 articles

Invision Community 5

86 articles

Extensions and contracts

39

What each extension point is for, what it must declare, and what happens when it is wrong — which is usually nothing visible.

Languages and text

5

The string table, translation, and the places where text does not appear where you expected it to.

Theming, templates and forms

9

Theme hooks, CSS that survives both colour schemes, and building forms that do not throw on render.

Background work and scheduled tasks

5

The queue system, work that has to happen after the response, and jobs that finish without doing anything.

Data, settings and storage

11

The database layer, settings, tags, file storage, and backing up a live site.

AI features and expectations

5

What these features do, what they cost, and what buyers reasonably but wrongly assume they do.

Application structure and releases

11

The JSON files an application is made of, versioning and upgrade steps, and testing from the command line.

Realtime, chat and calls

1

WebSocket gateways, relays and the server-side pieces live features depend on — where "it works when I test it" and "it works for your members" are different claims.

Nothing matches that.

Event listeners: all 58 hooks, and the argument-order trap

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 Invoice
  • onRenew, onExpire, onExpireWarning, onCancel, onReactivate, onChange, onTransfer, onDelete — the Purchase
  • onPurchaseGenerated — 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.


User Feedback

Recommended Comments

There are no comments to display.

Account

Navigation

Search

Search

Configure browser push notifications

Chrome (Android)
  1. Tap the lock icon next to the address bar.
  2. Tap Permissions → Notifications.
  3. Adjust your preference.
Chrome (Desktop)
  1. Click the padlock icon in the address bar.
  2. Select Site settings.
  3. Find Notifications and adjust your preference.