Events and your own code
Three levers, in increasing order of force: an event listener, your own route, your own controller. Take the weakest one that will do the job — the weaker the lever, the more of your code keeps being updated along with the component.
Do not edit files in core/components/pbauth/
They are overwritten on update, and your edit disappears silently. Everything below is a way of avoiding that.
Event listener
A class with a handle(array $params, string $event) method. Put it anywhere in core/App/, name it in listeners.
<?php
namespace PageBlocks\App\Events\Auth;
use Boshnik\PageBlocks\Support\Mail;
class SendWelcomeLetter
{
public function handle(array $params, string $event): void
{
$user = $params['user'] ?? null;
$validated = $params['validated'] ?? [];
if (!$user) {
return;
}
Mail::to($validated['email'])
->subject('Welcome!')
->view('file:auth/chunks/email.welcome', ['username' => $user->username])
->send();
}
}<?php
use Boshnik\PbAuth\Events\Dispatcher;
use PageBlocks\App\Events\Auth\SendWelcomeLetter;
return [
'listeners' => [
Dispatcher::AFTER_REGISTER => [SendWelcomeLetter::class],
],
];You can hang as many classes on a single event as you like — they run one after another. Instead of a class name, any callable is accepted, including a closure.
To register a listener on the fly, without the config:
Dispatcher::listen(Dispatcher::AFTER_LOGIN, function (array $params, string $event) {
// …
});A failed listener does not break the action
An exception inside handle() is caught, written to the MODX log and interrupts neither registration nor login. That is by design: losing a registration because of an unsent email is not acceptable. It also follows that a listener cannot forbid an action — forbidding needs your own controller.
Two moments, and the difference between them matters
- Before saving (
USER_SAVING) — the user is not written to the database yet. This is where you change the object itself: add fields, substitute values. No need to save, the component will save right after. - After (
AFTER_REGISTER,AFTER_LOGINand the rest) — everything is already written. This is where you send emails, write to a journal, call an external service.
All events
| When | Constant | What arrives in $params |
|---|---|---|
| before saving — both on registration and on profile editing | USER_SAVING | user, profile, validated, action (register or profile) |
| user registered | AFTER_REGISTER | user, profile, validated |
| user logged in | AFTER_LOGIN | user |
| user logged out | AFTER_LOGOUT | user |
| profile saved | AFTER_PROFILE_UPDATE | user, profile, validated |
| email confirmed via the link from the letter | AFTER_VERIFY_EMAIL | user |
| password recovered via the link | AFTER_RESET_PASSWORD | user |
| password changed in the profile | AFTER_CHANGE_PASSWORD | user |
| confirmation link sent again | AFTER_RESEND_VERIFICATION | user, email |
| user enabled the second factor | TWO_FACTOR_ENABLED | user |
| user disabled the second factor | TWO_FACTOR_DISABLED | user |
| social network linked to the account | SOCIAL_LINKED | user, provider |
| social network unlinked | SOCIAL_UNLINKED | user, provider |
| manager logged in under someone else's account | AFTER_IMPERSONATE | user, manager |
The constants are in Boshnik\PbAuth\Events\Dispatcher. validated is an array of what came from the form and passed validation.
MODX plugins work too
A MODX system event of the same name is fired at the same time (pbAuthAfterRegister, pbAuthUserSaving and so on) — in case plugins are more familiar to you.
| Class in the config | Plugin on a system event | |
|---|---|---|
| after a deploy | works right away | has to be re-attached in System → System Events |
| what it receives | objects: user, profile, validated | scalars only: user_id, profile_id, email |
| can change the object | yes, on USER_SAVING | no |
Objects are deliberately not passed to the plugin: a modUser in $scriptProperties is useless to a plugin and breaks event caching. That is why USER_SAVING via a plugin is pointless — there is nothing to change there.
Your own routes
The component's routes live in core/components/pbauth/routes/auth.php, and you should not edit them. Add your own pages in your own file in core/App/routes/:
<?php
use Boshnik\PageBlocks\Facades\Route;
Route::middleware('auth')->group(function () {
Route::get('/profile/settings', 'ProfileSettingsController@show')->name('profileSettings');
});middleware('auth') — for logged-in users only, middleware('guest') — for guests only. The controller goes into core/App/Http/Controllers/ — from there on it is ordinary PageBlocks development, pbAuth has nothing to do with it.
Declaration order does not matter: an exact route always beats a wildcard one. /profile/settings will open your page even if /profile/{alias} is declared somewhere.
Just do not name the file auth.php
As long as a file with exactly that name sits in core/App/routes/, pbAuth assumes the site runs on the old scheme and does not register its own routes at all — /login will stop opening. Any other name will do.
Your own controller
The last lever — for when settings and events were not enough. For example, you need to forbid registration from certain addresses: a listener cannot do that, it is unable to say "no".
core/App/Http/Controllers/Auth/RegisterController.php:
<?php
namespace PageBlocks\App\Http\Controllers\Auth;
use Boshnik\PageBlocks\Http\Request;
class RegisterController extends \Boshnik\PbAuth\Http\Controllers\Auth\RegisterController
{
public function register(Request $request)
{
if ($this->isBlacklisted($request->ip())) {
return response()->error('Registration from this address is closed');
}
return parent::register($request); // then as usual
}
protected function isBlacklisted(string $ip): bool
{
return in_array($ip, ['203.0.113.7'], true);
}
}'controllers' => [
'register' => \PageBlocks\App\Http\Controllers\Auth\RegisterController::class,
],After that the component's routes lead into your class. Everything you have not overridden keeps working from the component and keeps being updated — so override only what you really need to.
The keys: auth (email confirmation), login, register, profile, forgot_password, reset_password, change_password, confirm_password, resend_verification, two_factor, social, impersonate.
The logic lives in the controllers, not in a service
The component currently has no separate $auth object with a public API: the login and registration scenarios are implemented right in the controllers. So "call registration from my own code" means calling a controller method — not $auth->register($data).
This is a recognised debt of the component, not an architectural feature. If you need registration from cron, from a console command or from your own API endpoint, inherit the controller and call its method; do not duplicate the logic on your side — it will diverge from the component on the very first update.
JSON instead of a redirect
The component has no separate API, but the form answers differently depending on the request: PageBlocks looks at expectsJson(), that is, at Accept and X-Requested-With.
POST /login
Accept: application/json
X-Requested-With: XMLHttpRequest{
"success": false,
"message": "Check the fields",
"errors": {
"username": "User not found"
},
"redirect": ""
}Success is the same thing, with success: true and an address in redirect; on validation errors the response code is 422. That is enough for your own frontend to draw the form: this is exactly how the stock PageBlocks script works when pageblocks_load_scripts is on.
This is a form response, not an API contract
The set of response fields is common to all PageBlocks forms, and you can rely on it. But /login and /register remain forms: they expect a CSRF token and live in the session. For a sessionless client (a mobile app) this is not enough — pbAuth does not issue tokens.
Both of the ordinary scenarios work without JS: with no script the form is submitted by an ordinary POST and answers with a redirect, with the errors in the session.