Skip to content

Custom page class (UserPage) not used for $user API variable when usePageClasses is enabled #2200

@adrianbj

Description

@adrianbj

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:

  1. Users::__construct() calls setPageClass('User')
  2. PagesType::getPageClass() copies 'User' onto the user template's pageClass property (because the template's own pageClass is empty)
  3. Templates::getPageClass() is called — Stage 1 resolves 'User' to ProcessWire\User
  4. Stage 3 (convention-based auto-detection) should then find UserPage and override — but the final cached result is still ProcessWire\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

  1. Enable $config->usePageClasses = true in site/config.php
  2. Create site/classes/UserPage.php:
<?php namespace ProcessWire;
class UserPage extends User {
    public function hello(): string {
        return 'world';
    }
}
  1. Log in as a front-end user
  2. In any template: echo get_class($user); → outputs ProcessWire\User
  3. echo $user->hello(); → throws WireException

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);

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions