Why Extend modUser?

Extending the modUser object for speed and efficiency


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. What you may not have seen, however, is much information on *why* you'd want to extend modUser.

In this article, I'd like to cover 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 a 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 = $user->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.

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 a single query using $modx->getCollectionGraph(). While we're at it, we'll sort them by fullname:

$c = $modx->newQuery('modUser');
$c->sortby('fullname', 'ASC');
$c->where(
   array('Data.last_name' => 'Johnson'),
);
$users = $modx->getCollectionGraph('modUser', '{"Profile":{},"Data":{}}', $c);

After the code above has run, the $users variable will contain an array of all users whose 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>';
foreach($users as $user) {
    $fields = $user->toArray();
    unset($fields['password'], $fields['cachepwd'], $fields['salt'], $fields['hash_class'] );
    if ($user->Profile) {
        $fields = array_merge($user->Profile->toArray(), $fields);
    }
    if ($user->Data) {
        $fields = array_merge($user->Data->toArray(), $fields);
    }

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

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 (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 based on the extra profile object.

If you use ClassExtender's default settings (recommended) to extend the modUser object, the related object's alias will be "Data".

With the modUser object, we get the user profile with this code:

$profile = $user->getOne('Profile');

For our extended modUser object (extUser with the default setting, the equivalent code looks like this:

$data = $user->getOne('Data');

Once you have the $data object, you can get any of its fields using get() just like any other MODX object:

$data = $user->getOne('Data');
$userHeight = $data->get('height');

Because the extended class is an xPDO object, you can also get all the extended fields into an array with this code:

$fields = $data->toArray();

You can also display user data using a Tpl chunk (much like getResources does for Resources) with this code:

$fields = $data->toArray();
$output .= $modx->getChunk('MyUserChunk', $fields);

Finally, if you have retrieved the Data object directly (say, as $dataObject), you can go the other way and get its associated user:

$dataObject->getOne('User');

How?

Extending the modUser object involves a number of steps. 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 the extension_packages System Setting 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 names and types of the 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. You even have the option of creating the table you need (or using an existing one) and letting ClassExtender create the schema for you.

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 lets you show aggregated Resource 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.

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.



Comments (2)

  1. Martin GartnerMay 27, 2014 at 11:08 PM

    The problem with extending the user object is, that you can do it only once. Using your sample above + trying to additionally extend the user object to provide a Facebook login simply can't work. In GoodNews, my group mailer add-on, I ended up removing the "Extension" and solving the problem manually with a plugin which removes entries from the extended table if a user is deleted. (Marc Hamstra brought me to this conclusion).

  2. Bob RayMay 29, 2014 at 10:00 PM

    True, but I believe you could add more fields to your extended table and then regenerate the class and map files.

    My understanding is that since the rows in the extended table are related objects of the modUser object, they will automatically be deleted if the user is removed, though I haven't tested it. If that's not the case, I don't think the leftover rows would cause much harm unless you have a lot of users coming and going.


Please login to comment.

  (Login)