Find that Placeholder Part III

Cleaning up our snippet code by using a PHP class.

By Bob Ray  |  October 21, 2025  |  2 min read
Find that Placeholder Part III

At the end of the last article, I pointed out how ugly and inelegant our snippet had become. This happens to me a lot. I have an idea for a simple utility snippet. I knock it out and make sure it works. I often do this without considering what the snippet might look like in the future. I just dash off the snippet without much regard for its design, figuring that it just needs to work and serve its original purpose.

As I've mentioned before, this may be a result of all the procedural coding I did before Object-Oriented Programming existed. The result is that when additional features get added, I shoehorn them in, and the code becomes uglier and uglier, like the code in my previous article.

Without getting into the weeds of strict OOP, some OOP purists would say that we should create a Placeholder class and give it methods to find itself in various objects (which would also be classes). In my opinion, though, this wouldn't fit our use case very well and would unnecessarily complicate our code. Instead, we'll create a MODX PlaceholderFinder class designed to complete our task with a lot less fuss and bother. It will separate the search process from the reporting process and hopefully make the code much simpler.

As a reminder, here's our snippet tag:

[[!FindPlaceholder? &placeholder=`SomePlaceholder`]]

Class Variables

  • $modx
  • $placeholder (string containing the placeholder name)
  • $snippets (array or snippet names)
  • $plugins (array of plugin names)
  • $files (array of file paths)
  • $prefix (class prefix)
  • $errors (array of errors)

Class Methods

  • init()
    • findInObjects(string $objectType)
    • checkContent($content)
    • findIncludes($content)
    • checkIncludes($filepath)
    • report()
    • addError()

The Class Code

Here is full class code, much of which is just copying our functions, removing the reporting code, and making small changes to let it make use of our class variables. Below it is the small set of lines necessary to run it.

<?php
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) {

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

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

        if (!empty($success)) {

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

                /* 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;
                        };
                    }
                }
            }
        }
    }

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

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

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

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

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

        }
    }

    public function report():string  {
        $output = '';

        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 $snippet) {
               $output .= "\n    * " . $snippet . '';
           }
           $output .= "\n";
        } else {
           $output .= "\nPlaceholder not found in snippets";
        }

        /* Plugins */
        if (!empty($this->plugins)) {
           $output .= "\n### Plugins containing placeholder\n";
           foreach ($this->plugins as $plugin) {
               $output .= "\n    * " . $plugin . '';
           }
           $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;

Checking Included Files

The code is now fairly straightforward. The new findInObjects() method checks both snippets and plugins for the placeholder. If it finds the placeholder, it adds the name of the object to either the snippets or plugins array, which contains the names of snippets and plugins that contain the placeholder. While it has the content of each object, it sends the content to the checkIncludes() method, which checks any included or required files for the placeholder. If it finds any that hold the placeholder, it adds their file path to the files array.

Once that's over, it's a simple matter for the new display() method to generate the final report.

Each method (function) is now much simpler since the argument list is very short and the methods don't have to deal with reporting the results.

We've also added an $errors class variable, so we can report any errors that occurred along the way.

Now that the code is restructured, it should be easier to add new features, as we'll see in the next few articles.

A word about the report() method.

You'll notice that the report section is not very pretty. You may also wonder about the \n characters and spaces, since they have no effect in HTML output. They're there because I run test the code by running it in my code editor (PhpStorm). Since it's running in CLI mode, HTML tags are printed literally, and have no effect on the output. The newline characters and spaces create line breaks an indentation, so I can see the formatted output. I like to do this in my extras as well, since it's helpful for people who look at the source code of a page.

If this were not a utility snippet, The report() section would be more attractive, and easier to maintain, if it had some Tpl chunks like getResources() and other MODX extras. Creating them is left as an exercise for the reader.

Coming Up

Our code will fail to find placeholders in included or required files where the file path uses one of the MODX constants (like MODX_ASSETS_PATH). In the next article will see a fix for that.


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.