Why Extend modUser?

Why and how to extend the modUser object.

By Bob Ray  |  January 3, 2023  |  10 min read
Why Extend modUser?

If you read the MODX forums, you’ve probably seen a number of posts suggesting that people extend the modUser object to store user-related data that goes beyond the built-in user-related fields like username, email, fullname, etc. You may also have seen the somewhat complex set of steps for doing so here (for Revo 2) and here (for Revo 3).

What you may not have seen, however, is much information on why you’d want to extend modUser.

In this series of articles, I’ll be discussing three basic topics: first, the advantages of extending the modUser object, second, what it means to extend the modUser object, and third, the fact that extending modUser (or modResource) is now much easier to do with the ClassExtender Extra package.

Why?

The first question you might ask about extending modUser is, “why would I want to when I already have the user extended fields?”

User extended fields are kind of a pain. First, you have to manually create the extended field for each user, or write a utility Snippet to do so. Even if you manage to get all the extended fields created, you’ll be up against it when you want to use them to search for users or sort them based on the value or an extended field.

The problem is that all of the extended field data for each user is stored in a single field of the user profile. The data is stored as one JSON string, which is converted to a PHP array when you call $user->get('extended'). The fact that all the data for the various extended fields is packed into a single string makes it almost impossible to search reliably for specific users based on their extended fields (unless you only have one extended field).

Suppose you have extended fields for first_name, last_name, supervisor, and company. If you want to search for users named Johnson, you can get them, but you’re also going to get people whose supervisor is named Johnson and people whose company name contains Johnson.

Because you can’t search the extended field reliably with a single database query, you’re reduced to getting every single user on the site, looping through the users to get their user profiles, getting the extended fields from the user profile, and then examining the relevant field for your search string. When you find one, you add it to an array, which you then have to sort after it’s completed. Needless to say, that’s going to be very slow and will use much more memory than it should.

How much nicer (and infinitely faster) would it be to use code like this:

$userDataObjects = $modx->getCollection('userData', array('last_name' => 'Johnson'));

foreach($userDataObjects as $dataObject) {
    $user = $dataObject->getOne('User');
    $profile = $dataObject->getOne('Profile');
    /* Do something here with the information */
}

We’re still looping through the users, but only the users named Johnson, rather than every user in the database. Note that the code examples here are not complete, they just illustrate the use of the custom class. The class need to be loaded and the class object needs to be instantiated for before using it.

The code above will be much more efficient than looking through all the site’s users, but we can improve on it dramatically by getting all the users along with their profiles and extra fields in using $modx->getCollectionGraph(). While we’re at it, we’ll sort them by last name:

$c = $modx->newQuery('userData');
$c->sortby('last_name', 'ASC');
$c->where(
   array('last_name' => 'Johnson'),
);
$users = $modx->getCollectionGraph('userData', '{"Profile":{},"User":{]]', $c);

After the code above has run, the $users variable will contain an array of all userData objects where the last name is Johnson along with their associated profiles and extra data. From there, it’s a fairly short step to displaying their data with a Tpl Chunk using code something like this (following the code above):

$output = "<h3>Users</h3>\n";
foreach ($users as $userData) {
    $finalFields = $userData->toArray();

    if ($userData->Profile) {
        $fields = $userData->Profile->toArray();
        $finalFields = array_merge($finalFields, $fields);
    }
    if ($userData->User) {
        $fields = $userData->user->toArray();
        unset($fields['password'], $fields['cachepwd'],
            $fields['salt'], $fields['hash_class']);
        $finalFields = array_merge($finalFields, $fields);
    }

    $output .= $modx->getChunk('UserChunk', $finalFields);
}

return $output;

The method above makes exactly one query to the database! It will be blindingly fast compared with getting the same results using the modUser object’s extended field.

What?

What does it mean to extend the modUser object? Without going into too much detail, it basically involves giving each user an extra user profile. We have to put our extra fields somewhere, and the existing MODX tables should never be modified.

As you probably know, the modx_users table in the database doesn’t contain a lot of information you’d want to retrieve. Mainly just the user’s username, active status, and primary user group. The rest of the user information shown on the Create/Edit User panel in the Manager is in the user profile object (the modx_user_attributes table).

You should never modify those two tables (or any standard MODX tables), but we can extend the modUser object to give each user the equivalent of an extra profile, containing any new fields we need for storing our extra data. Once that’s done, we can search and sort on the extra fields with a very fast and memory-efficient query, like the one shown earlier in this article, based on the extra profile object.

As in the tutorials in the MODX official documentation, you’re going to want to make your custom object(s) compatible with xPDO, so you can use xPDO methods like getObject(), getCollection(), etc., on them.

Two Approaches

There are two basic approaches to extending modUser.

With the one in the MODX official documentation, your new custom object (extUser) extends the standard modUser object. It inherits the Profile alias from modUser and creates a separate alias (Data) for your custom table. It requires that your extUser class has a new class key (in the class_key field). In the docs this class key is extUser.

ClassExtender takes a slightly different approach (though you can still use it to create a setup like the one above). It does not use a different class key for the user object. Instead, the custom table is for a standalone object called userData. This object has two aliases, User and Profile. Which get the related modUser object and the related modUserProfile object. The ID of the user is kept in the userdata_id field of the custom object in the same way that the modUserProfile object has the user’s ID in the internalKey field.

This is basically backwards from the way the MODX docs version does it. It has the advantage that the user object’s class key is not changed, which simplifies some operations, but has the disadvantage that if you have the user object, you can’t call $user->getOne('Data');. You have to use the user’s ID and do this:

$modx->getObject('userData', array('userdata_id' => $id));

Getting All User Fields

There’s a way to dramatically increase page-load speeds when getting all the user fields. As we saw earlier in this article, instead of getting the fields of the modUser, modUserProfile, and userData objects with three queries (and usually three calls to setPlaceholders()) is to use the xPDO getCollectionGraph() method. It gets everything in one query and allows you to make a single call to setPlaceholders(). Here’s the code (lifted from ClassExtender’s GetExtUsers Snippet). The Tpl Chunk should contain the HTML for showing the results, with placeholders for all the fields you want to display.

    /* User class will need a namespace prefix
       (extendeduser/) in MODX 3 */
$userClass= 'userData';
$output = '';
$tplChunk = 'name of tpl chunk';
$c = $modx->newQuery($userClass);
$users = $modx->getCollectionGraph($userClass, '{"Profile":{},"User":{]]', $c);

$count = count($users);

if (!$count) {
    return '<p class="ce_error">' . $modx->lexicon('ce.no_users_found') . '';
}
foreach ($users as $user) {
    $fields = $user->toArray();

    if ($user->Profile) {
        $fields = array_merge($user->Profile->toArray(), $fields);
    }
    if ($user->User) {
        $fields = array_merge($user->User->toArray(), $fields);
    }

    unset($fields['password'], $fields['cachepwd'], $fields['salt'], $fields['hash_class']);

    $output = $modx->getChunk($tplChunk, $fields);

    return $output;

How?

If you do everything yourself, extending the modUser object involves a number of steps, as you see in the MODX docs. You need a schema for the new object and a set of class and map files so that xPDO knows what to do with it. The user class constructor needs to set the class_key field to the new class name. You will probably also want to register the new class in a modExtensionPackageobject, so that the custom user class will be available on every page load. Next, you need a Plugin that will modify the Create/Edit User form to include all the new user fields and save them when it’s submitted. Finally, you need to modify the class_key of all existing users and new users yet-to-be created.

The ClassExtender extra will do all of that for you (and more). You’ll still need to modify some things to tell ClassExtender the name of your package and create a schema that includes the extra object fields you need, but once you’ve done that, you can just view the 'Extend modUser' Resource, submit the form it contains, and all the necessary changes will be made automatically. As a bonus, ClassExtender will create an autoloader for you. It will also modify a Revo 2 style schema for use in Revo 3 if necessary.

The ClassExtender package also includes a Snippet called getExtUsers, which will display the information for users selected by some criteria in much the same way getResources or pdoResources will let you show aggregated user data.

Another Snippet in the package, setUserPlaceholders, will set placeholders for all the extra fields for the current user, or any other user whose ID you specify in the Snippet tag.

See the ClassExtender documentation for more details.

I’ll discuss how to use ClassExtender to extend the modUser object in my next article.

Wrapping Up

Extending the modUser object is not for everyone. It requires some skill to make it work the way you want it to. It’s not rocket science, though, and ClassExtender will do most of the heavy lifting for you. Extending modUser can dramatically reduce your page-load times and will create possibilities for searching and sorting that would be impossible with the extended fields of the user profile.


Bob Ray is the author of the MODX: The Official Guide and dozens of MODX Extras including QuickEmail, NewsPublisher, SiteCheck, GoRevo, Personalize, EZfaq, MyComponent and many more. His website is Bob’s Guides. It not only includes a plethora of MODX tutorials but there are some really great bread recipes there, as well.