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.

The report that shows nothing: only_full_group_by, and why catching the exception is the real bug

You write a reporting query, wrap it in try / catch so a bad query can never take down an AdminCP page, and ship it. The screen renders. The table is empty. You conclude there is no data yet.

There is data. The query is broken, and your own catch is what turned a fault into a plausible-looking zero.

The query

Db::i()->select(
    'conv_title, conv_url, conv_class, conv_item, COUNT(*) AS joined',
    'my_table',
    array( 'conv_item > 0' ),
    'joined DESC',
    25,
    array( 'conv_class', 'conv_item' )   // GROUP BY
);

MySQL 8 enables only_full_group_by by default and rejects it outright:

Expression #1 of SELECT list is not in GROUP BY clause and contains
nonaggregated column 'conv_title' which is not functionally dependent on
columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by

Every column in the SELECT must either appear in the GROUP BY or be wrapped in an aggregate. "But every row in the group holds the same value" is true and irrelevant — MySQL will not take your word for it.

The fix

Aggregate the columns you are only carrying along. Over identical values, MAX() returns exactly those values:

'MAX(conv_title) AS conv_title, MAX(conv_url) AS conv_url,
 conv_class, conv_item, COUNT(*) AS joined'

Adding them to the GROUP BY instead also silences the error, but changes the meaning — you would then be grouping by title as well, and two items that happen to share a title would stop merging. Aggregate; do not widen the grouping.

Two things worth knowing about IPS's query builder

  • A multi-column GROUP BY must be passed as an array. A comma-joined string is quoted as one identifier and the query fails.
  • Db::insert()'s third argument is a boolean, and the ON DUPLICATE KEY UPDATE it generates is col=VALUES(col) — a replace. It cannot increment a counter. For a daily counter, use INSERT IGNORE (fourth argument TRUE) followed by Db::i()->update( $table, "col=col+1", $where ), which is what core itself does for hit counters.

The larger point: log what you swallow

Wrapping report queries in try / catch is correct — an analytics screen should not be able to break the AdminCP. But a bare catch around a query that returns a list is uniquely dangerous, because an empty list is a completely believable answer. "No page has ever produced a member" and "the query does not run" look identical on screen, and only one of them is worth investigating.

catch ( \Exception $e )
{
    Log::log( $e, 'myapp_report' );   // ← the difference between a
    return array();                   //   mystery and a stack trace
}

The rule generalises: swallow an exception only where the caller can tell the difference between "nothing" and "broken". Where it cannot — reports, lists, counts, dashboards — leave a trace in core_log. Otherwise the first person to notice will be a customer telling you the feature does not work, and you will have nothing to go on.

Test it with data, not on an empty install

An empty-install test passes happily here: the correct answer and the broken answer are both an empty array. Build a fixture with a known answer — three rows that must group into one with a count of three — and assert the count. That is the only version of this test that can fail.


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.