Find that Placeholder Part VII

Some code cleanup.

By Bob Ray  |  December 2, 2025  |  3 min read
Find that Placeholder Part VII

In my last article, you saw the full code of the FindPlaceholder Snippet. You may have noticed that there are three particularly ugly sections we used to format the output to create links to edit the snippets, plugins, and files. In this final article, we'll clean that up by using PHP's heredoc format as we did with the regular expression patterns.

An Example

Here is the spectacularly ugly code we used for the snippet output:

foreach ($this->snippets as $snippetName => $snippetId) {
    $output .= "\n    * " . '<a href="' . $managerUrl .
        '?a=element/snippet/update?&id=' . $snippetId . '">' . $snippetName . '</a>';
}
$output .= "\n";

This code is so ugly and convoluted that it took me at least five tries to get it correct.

Here it is in heredoc format:

foreach ($this->snippets as $snippetName => $snippetId) {
$output .= <<<EOT $output .=<<<EOT
    * <a href="$managerUrl?a=element/snippet/update? &id=$snippetId">$snippetName</a>
EOT;
}
$output .= " \n";

This is so much cleaner and easier to write. All the variables are replaced by their values and the quotation marks and newlines (though invisible) come through as written. As a bonus, the newline and the indentation will also be rendered as written. Also, there's no worrying about matching up single and double quotation marks. There are no single quotes and only two double quotes in the entire code section.

I should mention that in heredoc syntax, there are situations where the variables names are rendered as written, rather than being replaced by their values. This is easily solved by surrounding them with curly braces, like this: {$snippetName}. When in doubt, you can always use the curly braces. Even when they're not needed, they'll still be handled correctly (though the process may be a few milliseconds slower).

It has probably occurred to you that there is another method that would have cleaned up this code. We could have used placeholders for the variables ([[+snippet_name]]). You could create an actual chunk, put the variables in an associative array, and send that array as a second argument to $modx->getChunk() like this: $this->modx->getChunk('chunkName', $placeholderArray). This method has the advantage that non-programmers can modify the chunk without having to mess with any PHP code. That chunk would look something like this:

/* FindPlaceholderChunk Content */
* <a href="[[+manager_url]]?a=element/[[+element_type]]/update? &id=[[+element_id]]">[[+element_name]]</a>
/* Snippet code */

/* Variables set earlier */
$placeholders = array(
    'element_type' => $elementType,
    'manager_url' => $managerUrl,
    'element_id' => $elementId,
    'element_name => $elementName,
);

$output .= $this-modx->getChunk('FindPlaceholderChunk', $placeholders);

As an alternative, you can create an "ad hoc" chunk with the chunk code using $modx newObject('modChunk'). Here's a simple example:

$output = '';
$adHocContent = 'Hello, [[+name]]! This is an ad hoc message.';

$placeholders = array(
    'name' => 'World'
);

$chunk = $modx->newObject('modChunk');
$chunk->setContent($adHocContent);
$output .= $chunk->process($placeholders);

echo $output;

The chunk here is nameless. It's created by MODX then discarded when the code is finished. In our case, you'd have to create the correct chunk content, and then create and set the placeholder array (making sure that the element type was correct for each loop). For me, it's much easier to just use the heredoc format. Once you get used to using it, it can save you a lot of programming time when you need to create a string for output that needs interspersed text and variable values. It's also nice for creating test strings when you're checking to make sure a regular expression matches what you want it to match.

The Full Code

Here's the full code of the final version of our placeholder finder:

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 normalizePath($path) {
        return str_replace('\\', '/', $path);
    }

    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->checkIncludes($content);
        }
    }

    public function getMediaSources() {
        $mediaSources = array();
        $mediaSourcePrefix = $this->modx->getVersionData()['version'] >= 3
            ? 'MODX\Revolution\Sources\\'
            : 'sources.';

        $sources = $this->modx->getCollection($mediaSourcePrefix . 'modFileMediaSource');

        foreach ($sources as $source) {
            $source->initialize();
            $basePath = $source->getBasePath();
            $basePath = $this->normalizePath($basePath);
            $id = $source->get('id');
            $mediaSources [$basePath] = $id;
        }
        return $mediaSources;
    }

    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 .= <<<EOT
                    * <a href="$managerUrl? a=element/snippet/update&id=$snippetId">$snippetName</a>
                EOT;
            }
            $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 .= <<<EOT
                    * <a href="$managerUrl?a=element/plugin/update&id=$pluginId">$pluginName</a>
                EOT;
            }
            $output .= "\n";
        } else {
            $output .= "\nPlaceholder not found in plugins";
        }

        /* Files */
        if (!empty($this->files)) {
            $modxBasePath = $this->normalizePath(MODX_BASE_PATH);
            $managerUrl = MODX_SITE_URL . basename(MODX_MANAGER_URL) . '/';
            $mediaSources = $this->getMediaSources();
            $output .= "\n### Files containing placeholder\n";
            foreach ($this->files as $file) {
                $mediaSourceId = null;
                $file = $this->normalizePath($file);
                $fileRelative = str_replace($modxBasePath, '', $file);

                foreach ($mediaSources as $basePath => $id) {

                    $fileToSearch = $basePath . $fileRelative;
                    if (file_exists($fileToSearch)) {
                        $mediaSourceId = $id;
                        break;
                    }
                }
                if ($mediaSourceId !== null) {
                    $output .= <<<EOT
                        * <a href="$managerUrl?a=system/file/edit&file=$fileRelative&source=$mediaSourceId">$file</a>
                    EOT;
                }
            }
            $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

In the next article, I'll retract some bad advice I gave in an earlier article about using instanceof in code meant to run in both MODX 2 and MODX 3.


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.