-
Notifications
You must be signed in to change notification settings - Fork 2
Custom page class (UserPage) not used for $user API variable when usePageClasses is enabled #2200
Description
The $user API variable is instantiated as a plain ProcessWire\User instead of the custom UserPage class, even when $config->usePageClasses = true and site/classes/UserPage.php exists and correctly extends User.
Current behavior
get_class($user) returns ProcessWire\User in templates. Calling any custom method defined in UserPage throws:
WireException: Method User::hasDisabledAccount does not exist or is not callable in this context
However, class_exists('ProcessWire\UserPage') returns true at the same point in execution, confirming the class is autoloadable.
$pages->get($user->id) also returns a plain User (page instance cache returns the same object). Only $pages->getFresh($user->id) correctly resolves to the custom page class.
Expected behavior
$user should be an instance of UserPage when usePageClasses is enabled and the class exists in site/classes/.
Analysis
The issue appears to be in the page class resolution/caching chain during bootstrap:
Users::__construct()callssetPageClass('User')PagesType::getPageClass()copies'User'onto the user template'spageClassproperty (because the template's ownpageClassis empty)Templates::getPageClass()is called — Stage 1 resolves'User'toProcessWire\User- Stage 3 (convention-based auto-detection) should then find
UserPageand override — but the final cached result is stillProcessWire\User
Once cached in Templates::$pageClassNames[$templateId], all subsequent loads (including $pages->get()) return plain User objects for the user template.
I suspect this may be related to the bootstrap refactor in 2b17d114 (splitting __construct() into __construct() + boot()) which changed initialization order.
// Before: everything in constructor
$wire = new ProcessWire($config);
// After: two-phase boot
$wire = new ProcessWire($config, '/', false); // false = don't boot yet
$wire->boot($config);
This reordering likely causes the user template's pageClass to be resolved and cached before the usePageClasses auto-detection has fully initialized — so it caches User instead of UserPage, and Stage 3 never
gets a chance to override it.
Reproduction
- Enable
$config->usePageClasses = trueinsite/config.php - Create
site/classes/UserPage.php:
<?php namespace ProcessWire;
class UserPage extends User {
public function hello(): string {
return 'world';
}
}- Log in as a front-end user
- In any template:
echo get_class($user);→ outputsProcessWire\User echo $user->hello();→ throwsWireException
Workaround
Reload $user via getFresh() in site/init.php:
$_u = $this->wire('user');
if ($_u->isLoggedIn() && !($_u instanceof UserPage)) {
$_reloaded = $this->wire('pages')->getFresh($_u->id);
if ($_reloaded instanceof UserPage) {
$this->wire('user', $_reloaded);
}
unset($_reloaded);
}
unset($_u);