Find that Placeholder Part V

Modifying our report() method to provide a link to edit each Snippet or Plugin.

By Bob Ray  |  November 18, 2025  |  4 min read
Find that Placeholder Part V

In the previous article, we updated our FindPlaceholder snippet to recognize and process MODX constants like MODX_CORE_PATH when used in a file path.

In this one, we'll modify our report() method to provide links to edit each Snippet or Plugin that contains our placeholder. In order to accomplish this, we need to make a link that looks like the URL used to edit elements in the MODX Manager. For Snippets and Plugins, that looks like this for my local installation:

/* For Snippets */
http://localhost/addons/manager/?a=element/snippet/update?&id=12

/* For Plugins */
http://localhost/addons/manager/?a=element/plugin/update?&id=12

The first step is to produce the first part of that string, up to, but not including the question mark. You might think that you could just use the MODX Constant, MODX_MANAGER_URL. Unfortunately, that produces this string:

/addons/manager/

The line above is obviously missing the protocol and domain name. How about this?

MODX_SITE_URL . 'manager/';

That's no good because the manager directory might have been renamed. You could just hard-code in the name of your Manager directory, but if you later decide to rename it, the code will fail. How about this:

MODX_SITE_URL . MODX_MANAGER_URL

That produces this string:

http://localhost/addons//addons/manager/

Not only does it contain a double slash, but the "addons" directory is in there twice, so that's not going to work. You could create code that repairs that URL, but there's a better way. We can get the name of the Manager directory like this:

$managerDir = basename(MODX_MANAGER_URL);
$managerUrl = MODX_SITE_URL . 'ManagerDir' . '/;

MODX_SITE_URL produces this string:

http://localhost/addons/

And basename(MODX_MANAGER_URL) is just replaced by the raw name of the Manager directory. All we have to do is put the two together and tack on a slash at the end. (MODX_MANAGER_PATH would work as well here, but since we're trying to produce a URL, (MODX_MANAGER_URL) makes more sense.) It will be a tiny bit faster to do it like this:

$managerUrl = MODX_SITE_URL . basename(MODX_MANAGER_URL) . '/';

Now that we have the actual Manager URL, we're almost ready to create the link. We still need the ID of the snippet or plugin to complete it, though.

We need to make a small change the code of our findInObjects() method to store the ID of each object that contains our placeholder, in addition to the name of the object.

Changes to findInObjects()

foreach ($objects as $object) {
    $name = $object->get('name');
    $id = $object->get('id'); /* New */
    $content = $object->getContent();

    if ($this->checkContent($content)) {
        if ($objectType == 'modSnippet') {
            $this->snippets[$name] = $id; /* New */
        }

        if ($objectType == 'modPlugin') {
            $this->plugins[$name] = $id; /* New */
        }
    };
    $this->checkIncludes($content);
}

Now, each member of the associative array will be in the form:

ObjectName => ObjectId;

Next, we need to modify the Snippet and Plugin sections of the report. Instead of these lines:

foreach ($snippets as $snippet) {
foreach ($plugins as $plugin) {

we'll have these lines:

foreach ($snippets as $snippetName => $snippetId) {
foreach ($plugins as $pluginName => $pluginId) {

Finally, we need to generate the link using the ID we saved earlier. At the top of the report() method, we'll do this:

$managerUrl = MODX_SITE_URL . basename(MODX_MANAGER_URL) . '/';

Now, we're ready make the report show links to edit each snippet or plugin:


/* In the snippet loop: */ foreach ($this->snippets as $snippetName => $snippetId) { $output .= "\n * " . '<a href="' . $managerUrl . '?a=element/snippet/update?&id=' . $snippetId . '">' . $snippetName . '</a>'; } /* In the plugin loop: */ foreach ($this->plugins as $pluginName => $pluginId) { $output .= "\n" . ' * ' . '<a href="' . $managerUrl . '?a=element/plugin/update?&id=' . $PluginId . '">' . $pluginName . '</a>'; }

Now all the user has to do is click on the link and they'll be editing the snippet or plugin containing the placeholder.

Full Code

Here is the full code:

class PlaceholderFinder {
    public modX $modx;
    public string $placeholder = '';
    public array $snippets = array();
    public array $plugins = array();
    public array $files = array();
    public string $prefix = '';
    public array $errors = array();

    public function __construct($modx) {
        $this->modx = $modx;
    }

    public function init($scriptProperties):void {
        /* Make this work in both MODX 2 and MODX 3 */
        $this->prefix = $this->modx->getVersionData()['version'] >= 3
            ? 'MODX\Revolution\\'
            : '';
        $this->placeholder = $this->modx->getOption('placeholder', $scriptProperties);

        if (empty($this->placeholder)) {
            $this->addError('placeholder is empty');
        }
    }

    public function addError($error):void {
        $this->errors[] = $error;
    }

    public function checkContent($content):bool {
        $found = 0;
        if (strpos($content, $this->placeholder) !== false) {
            $found = 1;
        }
        return $found;
    }

    public function checkIncludes($content) {
        $cs = array(
                'MODX_CORE_PATH' => MODX_CORE_PATH,
                'XPDO_CORE_PATH' => MODX_CORE_PATH,
                'MODX_ASSETS_PATH' => MODX_ASSETS_PATH,
                'MODX_PROCESSORS_PATH' => MODX_PROCESSORS_PATH,
                'MODX_CONNECTORS_PATH' => MODX_CONNECTORS_PATH,
                'MODX_MANAGER_PATH' => MODX_MANAGER_PATH,
                'MODX_BASE_PATH' => MODX_BASE_PATH,
        );

        $pattern = <<<EOT
/(?:include|require)(?:_once)?[^"']*['"]([^'"]+)['"]/im
EOT;

        $success = preg_match_all($pattern, $content, $matches);

        if (!empty($success)) {
            $i = 0;
            foreach ($matches[1] as $path) {
                /* Skip includes that have a PHP variable in them */
                if (strpos($path, '$') !== false) {
                    continue;
                }

                $fullMatch = $matches[0][$i];
                foreach ($cs as $constName => $constValue) {
                    if (strpos($fullMatch, $constName) !== false) {
                        $path = $constValue . $path;
                        break;
                    }
                }

                /* Make sure the file exists */
                if (file_exists($path)) {
                    /* Make sure it's not a directory */
                    if (is_dir($path)) {
                        continue;
                    }
                    $c = file_get_contents($path);
                    if (!empty($c)) {
                        if ($this->checkContent($c)) {
                            $this->files[] = $path;
                        };
                    }
                }
                $i++;
            }
        }
    }

    public function findInObjects(string $objectType):void {
        $objects = $this->modx->getCollection($this->prefix . $objectType);

        foreach ($objects as $object) {
            $name = $object->get('name');
            $id = $object->get('id');
            $content = $object->getContent();

            if ($this->checkContent($content)) {
                if ($objectType == 'modSnippet') {
                    $this->snippets[$name] = $id;
                }

                if ($objectType == 'modPlugin') {
                    $this->plugins[$name] = $id;
                }

                // $this->objectType[] = $name;
            };
            $this->checkIncludes($content);
        }
    }

    public function report():string  {
        $output = '';
        $managerUrl = MODX_SITE_URL . basename(MODX_MANAGER_URL) . '/';

        if (!empty($this->errors)) {
            foreach($this->errors as $error) {
                $output .= "\nError: " . $error . '';
            }

            /* Uncomment the next line if you want to quit
               if there's an error */
            // return $output;

        }

        /* Snippets */
        if (!empty($this->snippets)) {
           $output .= "\n### Snippets containing placeholder\n";
           foreach ($this->snippets as $snippetName => $snippetId) {
               $output .= "\n    * " . '<a href="' . $managerUrl .
                    '?a=element/snippet/update?&id=' . $snippetId . '">' . $snippetName . '</a>';
           }
           $output .= "\n";
        } else {
           $output .= "\nPlaceholder not found in snippets";
        }

        /* Plugins */
        if (!empty($this->plugins)) {
           $output .= "\n### Plugins containing placeholder\n";
           foreach ($this->plugins as $pluginName => $pluginId) {
               $output .= "\n    * " . '<a href="' . $managerUrl .
                   '?a=element/plugin/update?&id=' . $pluginId . '">' . $pluginName . '</a>';
           }
           $output .= "\n";
        } else {
           $output .= "\nPlaceholder not found in plugins";
        }

        /* Files */
        if (!empty($this->files)) {
           $output .= "\n### Files containing placeholder\n";
           foreach ($this->files as $file) {
               $output .= "\n    * " . $file . '';
           }
           $output .= "\n";
        } else {
           $output .= "\nPlaceholder not found in files";
        }
        return $output;
    }
}   /* End of class */


$pf = new PlaceholderFinder($modx);
$pf->init($scriptProperties);
$pf->findInObjects('modSnippet');
$pf->findInObjects('modPlugin');
$output = $pf->report();
return $output;

Coming Up

Doing the same thing with files is possible, but more complex. links to edit the included files containing the placeholder in my next article.


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.