Resolvers and Validators

First in a series of articles for MODX Extra developers about using validators and resolvers, with emphasis on using resolvers to during package upgrades.

By Bob Ray  |  May 27, 2025  |  2 min read
Resolvers and Validators

Overview

In my last article, I wrote about creating and using utility snippets. That got me thinking about a similar topic: validators and resolvers in a transport package. These are often very similar to utility snippets, especially when you want to make modifications to your package during an upgrade. The only real difference is that the code is embedded in the transport package.

This article is the first in a series of articles on this topic. In this one, we'll look at the operation of validators in transport packages. In future articles, we'll see how resolvers work, and how to use code in a validator or resolver to modify the content of resources, elements, and files. We'll also look at how to adjust the values in properties, and property sets.

Validators and Resolvers

Some Extra's transport packages have no resolvers or validators. Neither one is required. A simple transport package for an extra can just install resources, elements, and/or users, and be done. But what if your extra will be released to the world, and you want to adjust the package for the site it's being installed on.

For example, you might want to make sure the site is equipped to handle your package. Your extra might require a graphics package or multibyte capability. You might also want to make sure your resources use the site's default template.

The trouble is, the information you need is not available when you build the package. At that point, there's no way for you to know the ID of the site's default template, or what optional PHP modules are installed. In these cases you need a validator or resolver to do the work for you.

In many ways, validators and resolvers are very similar. Both allow you to do just about anything that can be done in PHP. There are some differences in their capabilities, but the primary difference is in how they are used. Let's take a look at validators.

Validators

Validators are meant to run at the beginning of package installation. They are most often used to check for things necessary for your extra to work. Sometimes, they also do things like create DB tables or users. One key thing about validators is that they can abort the installation of the package — more on this in a bit. Let's look at the structure of a typical validator:

<?php
/**
 * Validator for Example extra
 *
 * Copyright 2025 yourname <Your email or website URL>
 * Created on 01-11-2025
 *
 * {License would go here}

 * @package example
 * @subpackage build
 */

/* @var $object xPDOObject */
/* @var $modx modX */
/* @var array $options */
/** @var modTransportPackage $transport */

/* Set up $modx variable for use later */
if ($transport) {
    $modx =& $transport->xpdo;
} else {
    $modx =& $object->xpdo;
}

switch ($options[xPDOTransport::PACKAGE_ACTION]) {
    case xPDOTransport::ACTION_INSTALL:
        /* return false if conditions are not met */
        $modx->log(modX::LOG_LEVEL_INFO, 'Validator: Install section executing');

        /* {your code} */

    /* (optional) if some condition is not met, return false */
        break;
    case xPDOTransport::ACTION_UPGRADE:
        /* return false if conditions are not met */
        /* {your code} */
        $modx->log(modX::LOG_LEVEL_INFO, 'Validator: Update section executing');
        break;

    case xPDOTransport::ACTION_UNINSTALL:{your code}
        $modx->log(modX::LOG_LEVEL_INFO, 'Validator: Uninstall section executing');
        /* {your code} */
        break;
}

return true;

The three sections of the switch statement above correspond to the three possible conditions under which the code will execute: new installation, upgrade (installing a new version), or uninstall. The three constants you see (xPDOTransport::ACTION_*) are set by MODX before the transport package code executes. In many packages the first two sections are combined, since the same actions are performed during new installations and upgrades, like this:

case xPDOTransport::ACTION_INSTALL:
case xPDOTransport::ACTION_UPGRADE:

/* Code here */
    break;

As you can see in the validator code above, there is often a test for some condition. The code returns false if the condition is not met. Even when there is no condition to test, you may want to return false if any operation of the validator fails. If, for example, the validator creates a new DB table that's necessary for your extra, it should return false if it can't create the table. Returning false, by itself, will not abort the installation. We'll see how to make that happen in the next section.

Attaching the Validator to your Package

Usually, the validator is attached to the first "vehicle" in the package. The vehicle is created, then before it's attached to the package, the validator is attached to the vehicle with code like this:

/* $file is the full path (including the filename) of the
 * validator code above */

$file = $sources['validators'] . 'MyValidator.php';

/* Attach the validator */
$vehicle->validate('php', array('source' => $file,));

The first argument to the validate() method is always ;'php' because that's the only kind of validator there is. It's there for possible future development of specialized validators, but it's required and the validator will fail if it's not there.

The $sources array is set earlier in the transport file. It's just an array of locations with convenient names as the keys and the appropriate directory as the value for each key. Here's an example:

/* define sources */
$root = dirname(dirname(__FILE__)) . '/';
$sources = array(
    'root' => $root,
    'build' => $root . '_build/',
    'config' => $root . '_build/config/',
    /* note that the next two must not have a trailing slash */
    'source_core' => $root . 'core/components/' . PKG_NAME_LOWER,
    'source_assets' => $root . 'assets/components/' . PKG_NAME_LOWER,
    'resolvers' => $root . '_build/resolvers/',
    'validators' => $root . '_build/validators/',
    'data' => $root . '_build/data/',
    'docs' => $root . 'core/components/' . PKG_NAME_LOWER . '/docs/',
);

(There's a good explanation of transport packages here).

What about how to abort the installation based on the return value of the validator? You don't have to do it, but if the site doesn't have something you need for your package to run, there's no point in continuing the installation. Let's look at how to abort the installation in that case.

Every vehicle in a package has an array of attributes. They're used as the second argument to createVehicle() when the vehicle is created. A typical attribute array looks like this:

$attributes = array(
    xPDOTransport::UNIQUE_KEY => 'pagetitle',
    xPDOTransport::PRESERVE_KEYS => false,
    xPDOTransport::UPDATE_OBJECT => true,
);

If you want to abort the installation when your validator returns false, you just need to add this line to the attributes array:

    xPDOTransport::ABORT_INSTALL_ON_VEHICLE_FAIL] => true,

I think the attribute above will also work for resolvers, but I've never seen it used for a resolver and I can't think of a use case where you'd want it to. The point of the validator's ability to abort the installation is that it will happen before any of the code in the package's transport has executed. That way, if the package is aborted, there are no changes to the site.

A Validator Quirk

At present, a validator attached to a vehicle will execute once for every object in the vehicle, though this may have changed by the time you read this. This is not true for resolvers, which always execute only once. Having a resolver execute more than once is usually harmless. It will slow down the installation slightly, but not enough to be noticeable. This can create an issue, though, if your validator code provides one or more progress messages, as in the example validator above. In that case, the user may see each message multiple times, which can make your package look unprofessional to users who install it.

For example, extras created by MyComponent create the category first, then attach the elements in that category to the category object. That means a validator attached to the first vehicle (which is where you want it), will execute once for the category object and once again for each snippet, chunk, template, TV, or plugin in that category. Any message produced in the validator could appear many times in a row.

There are a couple of ways to prevent the repetition of the messages. The easiest, which many extras adopt, is to display no messages in the validator. Another way is to set a $_SESSION variable in the validator when the code runs for the first time, then consult it before showing the message. Here's an example:

/* Near the top of the Validator file, abort if this validator
   has already run */

if (isset($_SESSION['validator_run'])) {
    return true;
}

/* later */

$modx->log(xPDO::LOG_LEVEL_INFO,'Checking Environment');
$success = false;
switch($options[xPDOTransport::PACKAGE_ACTION]) {
    case xPDOTransport::ACTION_INSTALL:
        $_SESSION['validator_run'] = 1;
        $modx->log(xPDO::LOG_LEVEL_INFO,'Checking for GD library');

        if (! function_exists('imagegd2')) {
            $modx->log(xPDO::LOG_LEVEL_INFO,'GD library not found');
            return false;
        }
        break;

/* Repeat $_SESSION['validator_run'] = 1 in each case */
    /* ... */
}

Once the validator has run, there's no need to repeat it, so it returns true if the $_SESSION variable is set. It's good form to unset the $SESSION['validator_run'] array member at the end of your package in a resolver.

If you'd rather not use the _$SESSION for this, you could create a tiny file, check for it with file_exists(), then delete it in your resolver.

Coming Up

In my next article, we'll look at resolvers.


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.