The 103 hooks about the customer account itself: signing up, signing in, verifying, blocking, sub-accounts and affiliates.
Overview
This domain carries the account itself: its opening, its sign-in, its verification, its blocking, its deletion. The screens a customer sees are a domain of their own.
Some of these hooks fire in the management panel, because an operator can change an account too. So when writing a listener, the answer to "who did this" is not always the customer.
Reference
The account's life
action:user.createdAn account was opened. The return is ignored.
action:user.updatedAccount details changed. The return is ignored.
action:user.status_changedThe account status changed. The return is ignored.
action:user.account_blockedThe account was blocked, or unblocked. The return is ignored.
action:user.deletedThe account was deleted. The return is ignored.
action:user.deleted_dormantA long-dormant account was deleted on its own. The return is ignored.
Sign-in and security
action:user.logged_inA sign-in happened. The return is ignored.
action:user.login_failedA sign-in attempt failed. The return is ignored.
action:user.logged_in_asA staff member entered a customer account. The return is ignored.
action:user.password_changedThe password changed. The return is ignored.
action:user.two_factor_changedTwo-step verification was switched on or off. The return is ignored; by here the change is already saved.
action:user.session_revokedA session was ended. The return is ignored.
action:user.account_switchedThe customer switched to another account. The return is ignored; the session is already written.
Gates
Eighteen gates stand in front of every weighty account action. They share one contract: a non-empty string stops the action. An empty return — null or '' — lets it through.
gate:user.createOpening an account. A filled return stops it, and that text becomes the error.
gate:user.loginSigning in — after the password checked out. A filled return stops it; a string and ['message' => '…'] are both taken.
gate:user.login_asA staff member entering a customer account. A filled return stops it.
gate:user.deleteDeleting an account. A filled return stops it, and the delete answers false.
gate:user.blockBlocking an account. A filled return stops it.
gate:user.add_fundsAdding funds — before the invoice is issued. A filled return stops it, and the text reaches the customer.
gate:user.email_changeChanging the e-mail address. A filled return stops it.
gate:user.two_factor_disableSwitching two-step verification off. A filled return stops it; a string and ['message' => '…'] are both taken.
gate:user.affiliate_enrollJoining the affiliate programme. A filled return stops it, and no record is opened.
gate:user.affiliate_withdrawalAn affiliate payout request. A filled return stops it.
gate:user.gdpr_processProcessing a data request. A filled return stops it.
gate:user.subuser_inviteInviting a sub-account. A filled return stops it, and the text reaches the customer.
Filters
These eight do not share one contract. Three shapes stand side by side, so read the return of the one you are writing for.
filter:user.required_fieldsThe required fields on signing up and on the profile. Return an array; every listener's return is merged in.
filter:user.login_resolveWhich account the entered identity resolves to. The value changes by reference; the return is not used.
filter:user.password_verifyThe password check. Returning true counts the password as good; every other return is ignored.
filter:user.login_redirectWhere a sign-in lands. The value changes by reference; the return is not used.
filter:user.add_funds_amountThe amount being added. The value changes by reference; the return is not used.
filter:custom_field.save_valueThe value a custom field saves. The value changes by reference; the return is not used.
filter:custom_field.load_valueThe value a custom field loads. The value changes by reference; the return is not used.
filter:user.verification_fieldsThe fields on a verification document. The fields change by reference; the return is ignored.
Example
coremio/hooks/acme-account.php
// A GATE: no account from a blocked country.
Hook::add('gate:user.create', 10, function ($data) {
$cc = strtoupper((string) ($data['country'] ?? ''));
if (Acme::blockedCountry($cc)) return 'We take no sign-ups from there.';
return null;
});
// AN EVENT: put the new account on the marketing list.
Hook::add('action:user.created', 10, function ($user_id, $data) {
Crm::subscribe((int) $user_id, (string) ($data['email'] ?? ''));
});
Pitfalls
Both the customer and an operator change an account
Most hooks here fire from two places: the customer's own screen and the management panel. Writing a gate to say "the customer cannot do this" stops the operator as well. Where you need to tell them apart, read the actor the hook hands you.
The sign-in gate does not check the password
The sign-in gate runs after the credentials check. The work there is not checking a password but stopping for another reason. To touch the password check itself, a filter of its own exists.
Sub-accounts and switching blur the identity
A customer can enter another account as a sub-user. The person signed in and the account being worked on are then different. Use the account id the hook hands you; an id read from the session points at the wrong person after a switch.
The delete hook is a point of no return
Deleting an account takes its records with it. By the time the delete event fires the work is done; gather what you need at the gate instead. The gate is earlier, and it can stop the action where that is wanted.