The MODX Error Log

A closer look at logging errors in MODX, and a tip about "Bad link tag" error messages.

By Bob Ray  |  June 4, 2024  |  11 min read
The MODX Error Log

I've written about the MODX error log before, but I'd like to take a little deeper dive here to look at logging errors using $modx->log() and $modx->setLogTarget(). Along the way, we'll look at some very welcome new MODX code that handles "Bad link tag" messages in the error log.

Simple Error Messages

The MODX log() method (called with $modx->log()), takes six arguments, but only the first two are required. The first one is the level of the error ($level). Although a number would work for the level, for the sake of you and anyone else who reads your code, you should always use one of the MODX error-level constants in the first argument to the log() method.

The constants are defined by this code in the MODX config file:

define('MODX_LOG_LEVEL_FATAL', 0);
define('MODX_LOG_LEVEL_ERROR', 1);
define('MODX_LOG_LEVEL_WARN', 2);
define('MODX_LOG_LEVEL_INFO', 3);
define('MODX_LOG_LEVEL_DEBUG', 4);

They are also defined as constants, using the same number values, in the xPDO class, but without the MODX_ prefix:

const LOG_LEVEL_FATAL = 0;
const LOG_LEVEL_ERROR = 1;
const LOG_LEVEL_WARN = 2;
const LOG_LEVEL_INFO = 3;
const LOG_LEVEL_DEBUG = 4;

This means that there are three common ways to specify them in the first argument to the MODX log() method:

$modx->log(MODX_LOG_LEVEL_ERROR, ...);
$modx->log(modX::LOG_LEVEL_ERROR, ...);
$modx->log(xPDO::LOG_LEVEL_ERROR, ...);

They are mostly interchangeable, except that the third one will work in the relatively rare case where the xPDO class has been instantiated, but the modX class has not.

The first two will always work when the modX class has been instantiated. The first one works because of the defines in the MODX config file. The second one works because the modX class extends the xPDO class.

The second version above is the preferred method to use for snippets and plugins, where it's guaranteed that the modX class is available, though you may see the first version in some older extras.

The second argument is the message ($msg) you want to display in your error message. The default log target is the MODX error log, found in the core/cache/logs/ directory, with the filename error.log

That means that you can write a message to the error log with this simple code:

$modx->log(modX::LOG_LEVEL_ERROR, 'My Message');

That will produce a message that looks something like this:

[2024-03-17 01:12:44] (ERROR @ fiddle.php : 31) MyMessage

All error messages will start with the date and time the message was generated. This makes it easy to see what errors occurred today versus some earlier date. The date and time are followed by the level of the error (in this case ERROR). That means you should usually not put the error level constant in your message, or your log messages will look awkward:

[2024-03-17 01:12:44] (ERROR @ fiddle.php : 31) Error: MyMessage

If your log() call is in an extra of yours, it's good form to put the name of the extra at the beginning of the message, like this:

$modx->log(modX::LOG_LEVEL_ERROR, '[MyExtra] My Message');

Remember that your error message might show up in a long list of error messages. It's really helpful for users checking the error log to be able to tell what extra generated the message.

My Error Messages are not Showing Up

The log() method uses the MODX log_level System Setting to decide which messages to post. The lower the level, the fewer messages will appear in the log. If that level 1 (LOG_LEVEL_FATAL), only fatal messages will appear. If you set it to 3 (LOG_LEVEL_WARN), only fatal, error, and warning messages will be logged. On production sites, the setting is usually 1 (LOG_LEVEL_FATAL). During development, you usually want it set to 3 (LOG_LEVEL_INFO), or in some cases 4 (LOG_LEVEL_DEBUG).

You need to use the numbers rather than the constants when setting the log_level System Setting, but if you click on the little plus sign next to the setting, it will show you what level each number refers to.

You can change the System Setting in code by calling the $modx->setLogLevel() method with one of the constants. This is seldom done because it will mess with the error logging done by MODX and MODX extras. If you do it, you should always change it back after all your log() calls have been made to prevent that. The setLogLevel() method returns the current level, so you can do something like this:

$oldLogLevel = $modx->setLogLevel(modx::LOG_LEVEL_ERROR);
// your log() call here.
$modx->setLogLevel($oldLogLevel);
````

Package Manager does this during package installation. It sets the level to `LOG_LEVEL_INFO` before installing the package, then sets it back to the previous setting once the package is installed.

### Log Target

The third argument to the `log()` method is the log target (`$target`). It's an unusual argument in that it will accept three different PHP types. It can be a string (e.g., `ECHO` or `FILE`), an array (more on this in a bit), or an object.

If it's empty, or set to the string, `FILE` it will be set to the value of the current `log_target` System Setting unless you have overridden that with a call to `setLogTarget()`. The default value of the System Setting is `'FILE'`, and with that setting, the error message will go to the MODX error log file.

If the target argument is set to `ECHO` or `HTML`, it will be sent to `stdout`, normally the screen unless you have redirected output somewhere else. If you use `HTML`, the first part of the error message will be wrapped in `h5` tags and your message will be wrapped in `pre` tags.

If the target argument is an array, it will allow you to send the message to a custom error log anywhere you want to put it. This is the only reason for making it an array. We'll look at how to use a custom error log in my next article.

I said above that the target argument can be set to an object. The only object the `log()` method recognizes is the `modRegistry` object. This allows you to use the `log()` method to send messages to a file or the database and query them later with an API. This is fairly complex and rarely done (I've never used it). You can see information about that process <a href="https://docs.modx.com/3.x/en/extending-modx/services/modregistry" target="_blank" rel="noopener nofollow noreferrer">here</a>.

### Defining Structure

The fourth argument to the `log()` method is to report the defining structure the error occurred in (`$def`). It can take one of the PHP <a href="https://www.php.net/manual/en/language.constants.magic.php" target="_blank" rel="noopener nofollow noreferrer">Magic Constants</a> or a regular string. If your error message is in a class, you can put `__METHOD__` in that argument and the name of the method where the error occurred will be added to the error message. You can also use `__CLASS__` to show the class name. `ClassName::class` will show the fully qualified class name, which can be useful if you're using namespaces.

If the error message is in a function that's not in a class, you can use `__FUNCTION__`. This will also show anonymous functions (closures).

If the argument is a regular string, it will appear as is in the error message. MODX will add 'in' ahead of whatever is being displayed. If you put `'Something'` in the fourth argument, you'll see something like this at the beginning of the error message:

```php
[2024-03-18 00:22:48] (ERROR in Something

If this fourth argument is left blank, nothing will appear where it would normally be displayed. In MODX extras, I usually put the name of the extra at the beginning of the second argument ($msg). It could go in the fourth as a plain string, but I prefer use that argument to show methods, classes, and functions.

File

It's easy to think that the fifth argument to the log() method ($file) has something to do with the log file, but it doesn't. It's to show the file that the error occurred in. It defaults to __FILE__, which will show the filename of the script file where the error was thrown, so there's almost never a reason to send this argument.

Line

This sixth argument ($line) is to show the line number in the file where the error was thrown (in other words, the line your log() call is on). It defaults to __LINE__ so like the previous argument, there's no reason to send it unless you want to confuse and frustrate people looking for the location of the error.

LOG_LEVEL_FATAL

This is a special case. It is not for showing PHP fatal errors, which will stop execution before the log() method is ever called. It's for stopping execution of the script itself. Say, for example, that your code checks for an error that detects corrupted data before it is sent to the DB. You definitely don't want to log the error and have the script continue to the code that sends the corrupted data to the DB. Creating a LOG_LEVEL_FATAL error message will stop your script immediately and call exit(). I'm not sure why, but in my tests this does not display the error message in the log() call. It just displays "FATAL" and "Service temporarily unavailable". This may be because my code was running in my code editor. To be safe, you can use a LOG_LEVEL_ERROR call in the line above the LOG_LEVEL_FATAL line to print your message to the error log.

Error Messages from the MODX Core

Often, an error or warning will be thrown in the MODX core code, not because the MODX code is wrong, but because it spots a problem with the information sent to it, such as an empty required setting or a missing file path.

Unfortunately, the error messages that come from these errors are often not very helpful. Many of them come from the MODX parser, which parses all MODX Resources before they are displayed. It's not much help that to know that the parser has found an error because it doesn't tell you where the error occurred.

The "Bad link tag" error is one error where this is no longer true. If your version of MODX is up to date, MODX now uses this code to call the log() method when reporting bad link tags:

$this->modx->log(
    modX::LOG_LEVEL_ERROR,
    'Bad link tag `' . $this->_tag . '` encountered',
    '',
    $this->modx->resource
        ? "resource {$this->modx->resource->id}"
        : ($_SERVER['REQUEST_URI'] ? "uri {$_SERVER['REQUEST_URI']}" : '')
);

In the code above, the message is the same as before, but the fourth argument, MODX checks to see if $this->modx->resource is set. If it is, The fourth argument will contain the word "resource" and the Resource's ID. If it's not set, MODX will attempt to find the URL of the page being processed and display that instead.

If your version of MODX is not up to date, you can get this feature by upgrading to the current version. If you can't upgrade, and you have "Bad link tag" errors, you can replace the log() call with the code above (carefully! — if there's a syntax error, it will break your site).

When I make replacements like this, I comment out the original code then add the new code below do it's easy to undo the change. In MODX 2, the parser is at core/model/modx/modParser. In MODX 3, the "Bad link tag" error is handled in a separate file: core\src\Revolution\modLinkTag.php. In either case, edit the file and search for "Bad link tag." Note that if you edit the file in MODX, you won't be able to save it unless you have the php file extension listed in the upload_files System Setting.

Wrapping Up

You've probably noticed that the information sent in the arguments does not appear in the error message in the same order as the arguments. Here's a demonstration with an error message and its result:

$modx->log(modx::LOG_LEVEL_ERROR, 'two','three','four','five','six');

// result

[2024-03-18 00:53:37] (ERROR in four @ five : six) two

We couldn't use "one" for the first one, because without the error level specification, no error message will be created. Notice that "three" is missing. That's because argument three is the log target and "three" is not a recognized target, so the default is used.

If you're running a good code editor, like PhpStorm, as you type the arguments into the log() call, every time you type a comma, PhpStorm will show you the name of the variable you're about to enter as it appears in the function or method you're calling. This can save you a lot of time and frustration from jumping back and forth between your call and the function or method you're calling.

For comparison, here is a typical error message that is not inside a class, method, or function:

$modx->log(modx::LOG_LEVEL_ERROR, 'MyMessage', '', '', '', '');

//result

[2024-03-18 01:01:07] (ERROR @ testcode\fiddle.php : 32) MyMessage

The error occurred on line 32 of the fiddle.php file. Notice that MODX prepends an @ symbol to the file name. This is automatic and can't be changed without creating your own error handler.


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.