Understanding addPackage, loadClass and getService

Level-up your MODX development. Join in on Bob’s exploration of when and why to use each of MODX’s three class loading methods.

By Bob Ray  |  December 2, 2021  |  10 min read
Understanding addPackage, loadClass and getService

I have never completely understood the differences between getService(), addPackage(), and loadClass(), and I find most descriptions of them somewhat thin and misleading (including the ones in my book). I recently ran some tests, and although my understanding is still imperfect, I thought I’d share what I learned.

The three methods are used during the process of loading classes in MODX. Why not just use include or require? Of course you can use the traditional include or require to load your classes, but if there are any errors and E_NOTICE is on, you can get a nasty and potentially confusing surprise. The MODX methods are much better behaved. If they encounter an error, they write it to the MODX Error Log (often, with some useful information) rather than stdout. The MODX methods also provide some extra control over the loading process and they’ll make use of the cache.

You probably know that if PHP includes the same class twice and the class is not wrapped in if (! class_exists('classname')) {}, it will throw an error telling you that the class is already defined. This often happens with plugins that might be called more than once during the same request. Using loadClass() will avoid this problem. It can be called multiple times in a row on the same class with no error. MODX realizes that the class is already loaded and doesn’t try to include it.

addPackage()

public function addPackage($pkg= '', $path= '', $prefix= null)

addPackage() loads the class and sets up the path to the directories of the package, but with this code, addPackage() is successful and loadClass() fails:

$loginModelPath = MODX_CORE_PATH . 'components/login/model/';

$success = $modx->addPackage('Login',$loginModelPath , '');
echo "\naddPackage " . ($success? 'OK' : 'Failed');
// OK

$success = $modx->loadClass('login.Login');
echo "\nloadClass " . ($success  ? 'OK' : 'Failed');
// Failed

The lesson here is that, in this case, loadClass() needs a path for its second argument, even though addPackage() was called first. The reason is that the Login class is not a MODX object stored in the database. If you’re loading a class that represents a MODX object with a schema and map file in addition to the class file, loadClass() would not need a path after addPackage() was called, but it never hurts to supply one.

As I read them, the docs seem to imply that addPackage() will let you access other classes in the package, but this code works fine without addPackage():

$loginModelPath = MODX_CORE_PATH . 'components/login/model/';
$success = $modx->loadClass('recaptcha.reCaptcha', $loginModelPath, true, true);
$reCaptcha = new reCaptcha($modx);

The reCaptcha class is in a subdirectory below the login/model/ directory. So, as long as loadClass() has a proper base path (almost always ending in model/), it will load any classes in the package when you give a fully-qualified name (dots represent the directory separator). In this case, addPackage() is, again, not necessary or appropriate because the reCaptcha class is not an object stored in the database.

The final argument to addPackage() ($prefix) allows you to specify a table prefix. This is necessary if your class represents a database object and its DB table has a different prefix than the MODX classes. Using a different table prefix for your own classes is strongly recommended.

loadClass()

public function loadClass($fqn, $path= '', $ignorePkg= false, $transient= false)

The first argument to loadClass(), $fqn, stands for "fully qualified name". In a call to loadClass (as in the reCaptcha example above) the part after the last dot in that argument should be the actual name of class itself, which is why it's in mixed-case. When converted to lowercase, it should also be the name of the class file itself (.class.php will be appended to it automatically). The part up to and including that last dot is a directory specification with the directory separator replaced with dots.

In other words, if you convert the whole $fqn argument to lowercase, prepend the $path, add .class.php to the end, and convert all dots to slashes, you should have the full path to your class file (including its file name).

If your arguments to loadClass() look like this:

// MODX_CORE_PATH = usr/public_html/modx/core/
$fqn = 'login.Login';
$path = MODX_CORE_PATH . 'components/login/model/';

MODX will try to include the following file (after converting MODX_CORE_PATH to your actual core path):

usr/public_html/modx/core/components/login/model/login/login.class.php

It will assume that the actual class is called Login.

If you are loading a class called MyClass and it is directly under your /model/ directory, the call would look something like this:

$path = MODX_CORE_PATH . 'components/mycomponent/model/';
$modx->loadClass('MyClass', $path, true, true);

If the class were in the directory /model/utilities/myclass/, it would look like this:

$path = MODX_CORE_PATH . 'components/mycomponent/model/';
$modx->loadClass('utilities.myclass.MyClass', $path, true, true);

In the example just above, myclass is the name of the directory containing the class file (myclass.class.php) and MyClass is the actual name of the class. The file name would be myclass.class.php. Class files in MODX should always be named classname.class.php

What about those last two arguments ($ignorePkg, $transient)? The first of those, $ignorePkg tells loadClass() whether to ignore the package. It defaults to false and its value is not critical. If it’s false, MODX will look for a class-related package, but the call will be successful whether it finds one or not. If you know that addPackage() has not been called, you can speed things up a little by setting it to true so MODX will know to skip the package search.

The second argument is critical. It tells addPackage whether the class is transient or not. If it is set to false, that tells MODX that the package is not transient, which means that it is stored in the database. If your class does not represent a database object, MODX won’t find one. The call will fail, and the class file won’t be loaded.

If your class does not represent a database object, the final argument should always be set to true. If it is a database object, and your code will be doing any interaction with the database, it should be set to false.

There are cases, though, where you might not want to do that. Say, for example, that your code instantiates the object for temporary use and never reads from or writes to the database. Maybe you’re just creating a temporary Chunk object to use in formatting output. In that case, you could speed things up a bit by setting the final argument to true.

getService()

public function &getService($name, $class= '', $path= '', $params= array ())

getService() loads the class file (by calling loadClass()), but it also adds the class to the $modx object sp you can call its methods with $modx->className->methodName(). getService() returns null on failure, and as long as you include the path, you don’t need to run either addPackage() or loadClass() before calling it:

$loginModelPath = MODX_CORE_PATH . 'components/login/model/';
$success = $modx->getService('login', 'login.Login', $loginModelPath);
echo "\ngetService" . ($success != null ? 'OK' : 'Failed');
// OK

if ($modx->login instanceof Login) {
echo "\n Login OK";
}
// Login OK

The doodles class, for example, has a method called getChunk() which overrides the MODX method of the same name. So if you’ve added the doodles class with getService(), for example, you could use $modx->doodles->getChunk() to call that method.

This technique will work inside another class or anywhere the $modx object is available, though the service class is only available during the request where it was loaded as a service. This is very handy for CMPs like Doodles.

If a service has already been loaded, getService() just returns a reference to it. Since it’s already attached to the $modx object, though, you don’t really need the reference, so the return value of getService() is almost never used in code. If the service doesn’t exist, MODX will create it and it can be used throughout that request cycle.

Certain service classes like modLexicon and modError, are almost always available in MODX, but it never hurts to make sure by calling getService() for them. In code running outside of MODX, it’s critical. Code that runs outside of MODX should generally load the error service. Otherwise, the program will crash if any MODX errors are triggered.

$modx->getService('error', 'modError');
$modx->getService('lexicon', 'modLexicon');
$modx->getService('fileHandler', 'modFileHandler');

The first argument to getService(), $name, is an alias that can be used to call the service. The second argument, $class, must be the actual name of the class. So, if you want to use the MODX lexicon after the service has been created with getService() (either by you or by MODX), you do it with the alias:

$message = $modx->lexicon('file_not_found');

If the service is a built-in MODX service (lexicon, mail, error, registry, etc.), you don’t need to specify a path because the MODX core/model/ path is the default value of the $path argument and that's where almost all of them are located, though some, like registry and mail, are under the MODX model directory, but a little deeper:

$modx->getService('registry', 'registry.modRegistry');
$modx->getService('mail', 'mail.modPHPMailer');

The final argument to getService(), (options), can contain an associative array of options that will be available in the $scriptProperties array inside the service class.

Summing Up

The loadClass() method is a good alternative to include and require. It will load the database-specific information for an object, which is essential if your class represents a database object and you’re using a database other than MySQL. All three methods will skip loading the class if it’s already loaded.

In addition, loadClass() will let you specify the file as "transient", in which case it will skip loading the database-specific information and some other steps in the process. Setting the $transient argument to true is critical if your class does not represent a database object—otherwise the call will fail.

The addPackage() is only useful for classes that represent objects in the database. The Login class, for example, is not a database object, it’s just a service class, so addPackage() doesn’t really help it, though there would be no harm in calling it before calling loadClass().

Both addPackage() and loadClass() return false on failure, but I’ve found that in some circumstances (e.g., when you fail to include the path), they can return non-false but you still can’t instantiate the class. getService() will return null on failure.

Once you’ve registered a package with addPackage(), its DB-related objects will have loadClass() called automatically (if the class is not loaded already) when you try to access them with any xPDO object method like newObject(), getObject(), or getCollection(). With these classes, you should always use $modx->newObject() to instantiate them, instead of just new. In that case, there’s no need to call loadClass.

The getService() method will attach the class to the $modx object. Once that’s done, you can call members of the class with $modx->serviceAlias->methodName() at any time during the current request cycle. For built-in MODX services, you can use the shorthand version: $modx->lexicon().

Cheatsheet

  • If you just want to instantiate some class that doesn’t represent a DB object, use loadClass(). Be sure to specify a path and set the fourth argument to true, or the call will fail.

  • If you want to instantiate some class that does represent a DB object and have generated class and map files for it, use addPackage(). loadClass() will be called automatically with any xPDO object call that accesses the database, but if you’ll be using your class before that happens, you’ll need to call loadClass() yourself first.

  • If you want the class to be available as $modx->className, or if the class is already registered as a MODX service (e.g., modMail, modLexicon, modError), use getService(). In the latter case, you don’t need a path, but if the package is in a subdirectory under the core/ directory, you’ll need to specify it: $modx->getService('registry, registry.modRegistry').

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.