huoxin / money-with-history
A unified Flarum extension that adds a virtual currency system with full transaction history tracking.
Package info
github.com/huoxin233/flarum-ext-money-with-history
Type:flarum-extension
pkg:composer/huoxin/money-with-history
Requires
- flarum/core: ^2.0.0-rc
Requires (Dev)
- flarum/approval: *
- flarum/flags: *
- flarum/likes: *
- flarum/tags: *
- flarum/testing: ^2.0.0-beta
- fof/byobu: *
Provides
- antoinefr/flarum-ext-money: ^v2.0.0-rc.1
This package is auto-updated.
Last update: 2026-07-02 18:27:31 UTC
README
A Flarum extension that adds a virtual currency system with full transaction history tracking.
As discussed here, this extension would be a merge of antoinefr/flarum-ext-money and mattoid/flarum-ext-money-history into a single, standalone package so no cross-extension dependency management is required. With this extension, I hope that we can eliminate the need of mattoid/flarum-ext-money-history-auto which relies on middleware to intercept API requests and record balance changes. However, achieving full compatibility will require support and integration from other extension developers.
Features
- Award money for posts, discussions, and likes
- Configurable auto-removal when content is hidden or deleted
- Cascade removal for posts when a discussion is deleted
- Minimum post length requirement
- Manual balance editing by moderators
- Full balance history
- Per-tag money disable permission
Installation
composer require huoxin/money-with-history php flarum migrate php flarum cache:clear
Updating
composer update huoxin/money-with-history php flarum migrate php flarum cache:clear
Warning
Breaking Change for Developers & Integrators: In 1.2.0, all database setting keys and translation strings have been standardized to snake_case semantic formats (e.g., moneyname is now money_name). If your custom theme or third-party extension directly queries these keys from the Flarum settings repository, you must update your code to match the new snake_case format. Normal forum administrators are unaffected, as the php flarum migrate command seamlessly updates existing configurations.
Migrating From Legacy Extensions
Do note that some of the more complex ones are not covered, you will have to manually migrate it yourself if you want a 100% clean money history.
Important
Enabling this extension for the first time will run migration tasks automatically. If you previously had a large money history database, the process may take some time to complete. It is recommended to enable the extension via CLI:
php flarum extension:enable huoxin-money-with-history
Warning
Timezone DST Inaccuracy Fallback: If your Flarum database relies on SQLite, PostgreSQL, or a MySQL instance without global timezone tables loaded, the historical timezone migration will fall back to a PHP-computed offset. For timezones that observe Daylight Saving Time (e.g., America/New_York), records created during the opposite DST phase of when the migration is run will be shifted inaccurately by ±1 hour.
If you were previously using antoinefr/flarum-ext-money and/or mattoid/flarum-ext-money-history:
- Backup your database.
- Install this extension alongside the old ones.
- Run
php flarum migrate— idempotent migrations will:- Add the
moneycolumn anduser_money_historytable if missing - Rename legacy columns (
type→source,money→balance_delta, etc.) - Normalize
sourcevalues (e.g.POSTWASPOSTED→POST_POSTED) - Migrate
source_keytranslation prefixes tohuoxin-money-with-history.forum.money-history.* - Copy settings keys from
antoinefr-money.*andmoney-history.*tohuoxin-money-with-history.*
- Add the
- Disable and uninstall the old extensions.
Legacy data from the deprecated mattoid-money-history-auto extension is also migrated.
For Other Extension Authors
This extension is the main balance-changing entry point. Other extensions should inject:
use Huoxin\MoneyWithHistory\Service\BalanceManager;
Available Methods
Method comparison
| Method | Transaction | Row lock | Saves user | Best for |
|---|---|---|---|---|
adjustBalance() |
Opens its own | Locks internally | Yes, internally | Standalone one-user changes |
adjustBalances() |
Opens its own | Locks all rows | Yes, internally | Batch rewards (same amount for everyone) |
adjustBalancesByUserIds() |
Yes, atomic | Row level | No, caller chunks | Batch updates (different amount for everyone) |
transferBalance() |
Opens its own | Locks both users | Yes, internally | User-to-user transfers |
applyBalanceChange() |
You provide | You lock | You call $user->save() |
Saving money alongside your own domain fields |
adjustBalance()
Single user balance change. Opens a transaction, locks the user row, updates the balance, writes history, and dispatches events — all self-contained. Accepts a User object or a raw integer user ID.
$this->balances->adjustBalance( $user, -12.5, 'MYEXTENSION_PURCHASE', 'vendor-my-extension.forum.money-history.purchase', ['itemTitle' => 'VIP Badge'], $actor, preventOverdraft: true );
Returns false if the user has insufficient balance (when preventOverdraft is enabled).
adjustBalances()
Batch update for multiple users in a single transaction. Preferred for system rewards and bulk grants. Accepts an array of User objects or raw integer user IDs.
Best Practice: If processing thousands of users simultaneously, chunk your input array (e.g., 500 users per call). This prevents PHP memory exhaustion and prevents MySQL lock exhaustion (since all rows are locked simultaneously during the transaction).
$totalUpdated = 0; // Fetch and process users using chunkById to preserve memory and prevent lock exhaustion User::query()->where('is_vip', true)->chunkById(500, function ($users) use (&$totalUpdated, $actor) { $totalUpdated += $this->balances->adjustBalances( $users->all(), // Convert Eloquent Collection to array 5.0, 'DAILY_REWARD', 'vendor-my-extension.forum.money-history.daily-reward', [], $actor ); });
Returns the count of users actually updated. Silently skips users who can't afford the debit when preventOverdraft is enabled.
adjustBalancesByUserIds()
Bulk update for multiple users when each user needs a different delta amount.
Best Practice: This method executes all delta updates within a single atomic database transaction. To prevent MySQL lock wait timeouts and to ensure safe retries in queue jobs, do not pass massive arrays (e.g., > 500 users) into this method at once. If you have thousands of users, use array_chunk on your dataset and dispatch multiple Queue Jobs to process them safely.
$userDeltas = [ 1 => 5.0, 2 => -10.0, 3 => 25.5 ]; $this->balances->adjustBalancesByUserIds( $userDeltas, 'COMPLEX_REWARD_CALCULATION', 'vendor-my-extension.forum.money-history.complex-reward', [], $actor );
transferBalance()
Atomic user-to-user transfer. Always prevents overdraft on the sender side. Both the sender and receiver parameters accept a User object or a raw integer user ID.
$this->balances->transferBalance( $sender, $receiver, 25.0, 'MYEXTENSION_TRANSFER', 'vendor-my-extension.forum.money-history.sent', 'vendor-my-extension.forum.money-history.received', [ 'giverUsername' => $sender->username, 'receiverUsername' => $receiver->username, ], $actor );
applyBalanceChange()
Use when your extension already manages its own database transaction and needs to persist the balance change alongside other domain fields atomically.
Unlike adjustBalance() which opens its own transaction and calls save() internally, applyBalanceChange() only mutates $user->money on the model object. History recording and event dispatching are deferred to an Eloquent afterSave callback — they only execute after your $user->save() succeeds. If the save fails or the transaction rolls back, no orphaned history row is written.
The caller is responsible for:
- Opening a database transaction
- Locking the user row (
SELECT ... FOR UPDATE) - Calling
$user->save()after this method
$this->connection->transaction(function () use ($user, $actor) { $lockedUser = User::query()->whereKey($user->id)->lockForUpdate()->first(); // Your domain field $lockedUser->last_checkin_time = now(); // Mutates $lockedUser->money on the model — does NOT save or write history yet $this->balances->applyBalanceChange( $lockedUser, 5.0, 'DAILY_CHECKIN_REWARD', 'vendor-my-extension.forum.history.checkin-reward', ['streakDays' => 7], $actor ); // One save persists both last_checkin_time AND money atomically. // The afterSave callback then writes the history row and dispatches MoneyUpdated. $lockedUser->save(); });
Background Queue Jobs
BatchAdjustBalances (Job)
If you need to bulk-adjust balances for thousands of users simultaneously (e.g., a system-wide reward) without blocking the user's web request, you can dispatch this generic background job.
use Huoxin\MoneyWithHistory\Job\BatchAdjustBalances; use Illuminate\Contracts\Queue\Queue; // Build an array map of [user_id => delta_amount] $userDeltas = [ 1 => 50.0, 2 => 50.0, 3 => -15.0 ]; resolve(Queue::class)->push( new BatchAdjustBalances( $userDeltas, 'DAILY_LOGIN_BONUS', // The source string for the history log 'vendor-ext.forum.money-history.daily-bonus', // The translation key null // Optional actor ID ) );
Best Practice: Do not pass massive arrays (e.g., > 500 users) into this job at once. If you have thousands of users, use array_chunk on your dataset and dispatch multiple Queue Jobs to prevent MySQL lock exhaustion and guarantee atomic rollbacks on failure.
source, sourceKey, sourceParams
| Field | Purpose | Example |
|---|---|---|
source |
Stable machine-readable identifier | STORE_BUY_GOODS |
sourceKey |
Frontend translation key | vendor-ext.forum.money-history.purchase |
sourceParams |
Flat key-value data for the translation | ['itemTitle' => 'VIP Badge'] |
sourceParams conventions:
- Plain values:
itemTitle,postNumber,username - Translated values: keys ending with
Key(e.g.purchaseTypeKey) - Link values: keys ending with
LinkHref(e.g.itemLinkHref)
Optional Integration (Soft Dependency)
If your extension wants to offer money features without requiring this extension:
use Huoxin\MoneyWithHistory\Service\BalanceManager; if ($this->extensions->isEnabled('huoxin-money-with-history')) { $balanceManager = $this->container->make(BalanceManager::class); $balanceManager->applyBalanceChange(...); }
Concurrency And Locking
BalanceManager locks affected user rows during write transactions to keep balance snapshots consistent.
- Prefer
adjustBalances()andtransferBalance()over hand-written loops - Keep transaction work small and avoid slow side effects inside it
Screenshots
Credits
A special thanks to the original creators and contributors who made this project possible:
- AntoineFr for the
flarum-ext-moneyextension. - Mattoid for the
flarum-ext-money-historyandflarum-ext-money-history-autoextension. - Ernest Defoe and the FriendsOfFlarum Team for the automated Flarum Community release workflow.