Overview
In my previous article, I wrote about using resolvers in a transport package to connect resources to other MODX objects. In this one, we'll see examples of using code in a resolver to update the content of resources in your package.
Not every package released to upgrade an extra (or MODX itself) does exactly what it should. No matter how careful you are, sometimes you'll have to release another version to fix things.
In my ClassExtender extra, I changed the names of two classes to conform to the PSR-1 standard, which calls for all
class names to begin with an uppercase letter. userData became UserData and resourceData became ResourceData. I
changed them everywhere I could, but some users encountered a "could not get table for class" error because they still
had snippet class files with userData or resourceData that did not match the new class names in their snippet
properties.
I advised them to make a quick fix while I worked on a solution: change the snippet properties to match the userData
and/or resourceData class names in the files.
In the corrected next release, I couldn't fix this in the main transport package, because my change wouldn't match the schemas containing their own custom fields. The only solution was to do an extensive search-and-replace operation on all the class names using a PHP resolver.
I thought I would document that process in this series of articles for people who might someday have to make a similar upgrade package. The examples in these article provide detailed information about how to modify MODX objects with a resolver during package updates.
The first step was to make a list of all the objects (resources, snippets, chunks, and files) that might contain the old class names. These will appear in the upcoming articles on how to make changes to various objects and files in an upgrade resolver.
A Useful Function
Next, I created a function for use in this process that could be used no matter what object or file was being processed. It checks for old class names that need to be changed. I searched for both obsolete class names in the same function. If neither one appears, there's no need to do anything. I wrote it as a generic function, so it can be used to search for any terms that might need to be changed. Having this code in a function shortens the code for the full resolver considerably. It slows things down by a few milliseconds, but it only runs once, and package upgrades are not very time-sensitive. Here's the code:
function checkContent(array $terms, string $content) {
foreach ($terms as $term) {
if (strpos($content, $term) !== false) {
/* Found one */
return true;
}
}
return false;
}
Modifying the Resource
Another reason the process of updating the class names has to be done in a resolver is that users will likely have a snippet tag for ClassExtender in one or more resources. Those tags will have properties specific to the needs of the user. I couldn't update the resources themselves without trashing the users properties, but I could do a search-and-replace operation on the class names, wherever they appeared.
Here's the code from the Upgrade section of the resolver that actually does the job. Notice that it calls the function from the previous section.
/* Class prefix for MODX 2 and 3 */
$prefix = $modx->getVersionData()['version'] >= 3
? 'MODX\Revolution\\'
: '';
/* Search and Replace strings */
$uSearch = 'UserData';
$uReplace = 'userData';
$rSearch = 'ResourceData';
$rReplace = 'resourceData';
/* Search and Replace arrays */
$searchArray = array(
$uSearch,
$rSearch,
);
$replaceArray = array(
$uReplace,
$rReplace,
);
/* Resources to Check */
$resources = array(
'Extend modUser',
'Extend modResource',
'My Extend modUser',
'My Extend modResource',
);
$count = 0;
$fixedResourceCount = 0;
foreach ($resources as $resource) {
$resourceObj = $modx->getObject($prefix . 'modResource', array('pagetitle' => $resource));
if ($resourceObj) {
$count++;
$content = $resourceObj->getContent();
if (checkContent($searchArray, $content)) {
$fixedResourceCount++;
$content = str_replace($searchArray, $replaceArray, $content);
$resourceObj->setContent($content);
$resourceObj->save();
}
}
}
At the top of the code above, the class prefix and the search and replace variables and arrays are set. Notice that the two arrays use the variables above them, so to use the code to replace other terms, you just need to change the search and replace variables at the top.
In the code above, we used $resourceObj->getContent() and $resourceObj->setContent() to get and set the content of
the resource. We could have used $resourceObj->get('content') and $resourceObj->set('content', $content) as well. I
recommend the first two for all resources and elements (except TVs), because it will make it easier to handle elements,
where the "content" field will not necessarily be named content. For chunks, surprisingly, it's snippet (at one time
it was htmlSnippet), and for snippets, it's also snippet, for plugins, it's plugincode, for templates, it's back
to content. These can be difficult to remember.
Using getContent() and setContent() will work with all elements (including static elements), except Template
Variables. For TVs, you need to use getValue() and setValue(), and you need to provide a resource ID in the call. We'll
look at modifying elements in a resolver in my next article.
The $count and $fixedResourceCount variables are set to 0 above the foreach loop. The $count variable holds the
number of resources in the $resources array that were actually found. The $fixedResources variable holds the number
that contained the old class names and were fixed.
It's easy to create a site with a known number of resources, elements, and files, that contain the old class names. We can run the code of the resolver in a scratch file (I call it fiddle.php) and get a count of the total found and the total fixed. These should be equal and the total of each should match how many we know are there.
We can also easily swap the search and replace terms at the top to flip the class names a back and forth between the old and new names. The numbers from each run should be the same. Because the testing it built into the code, this is called "Inherent Unit Testing." (Actually, it's not — I just made that term up, but it has a nice ring to it, don't you think?).
The code that does the work is simple enough. Inside the foreach loop, we try to get each resource in the $resources
array. If it exists (and it may not), we get its content with getContent(), feed that to our function checkContent()
with the search terms as the first argument to see if the content contains any old class names. If it does, we replace
them with str_replace(), and then save the resource. There's no need for an else with either if statement. If the
resource is not found, or if the search terms are not there, we do nothing and let the loop move on the next resource.
Notice that in the str_replace() call, the first two arguments are the two arrays we set up at the top of the code.
You might expect str_replace() to take an associative array of matching search and replace strings, but it doesn't. It
was written before associative arrays existed. Instead, it replaces the first member of the search array with the first
member of the replace array, the second with the second, and so on. That's why the search terms are on one array and the
replace terms are in another, and why neither is an associative array.
There is a very small strReplaceAssoc() function here that we could have used, but it would slow things down slightly, and
would make the code less clear. If you have a long list of search and replace terms, the function is quite useful,
especially if you need a dynamic list of search and replace terms where the array is created on the fly, or if you often
need to edit the list. Here's the function:
function strReplaceAssoc(array $replace, $subject) {
return str_replace(array_keys($replace), array_values($replace), $subject);
}
The $replace variable in the first argument above is an associative array where the keys are the search terms and the
values are their replacements. The $subject variable holds the text you want the search-and-replace operation to
change. The line of code inside the function returns the $subject variable after the replacements have been made.
Coming Up
In my next article, we'll look at modifying elements in a resolver, with chunks as an example.
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.