In the previous article, we looked at how to extend processors and other objects in code for Revo 3. In this article, we’ll see how to handle the same task in code that works in both Revo 2 and Revo 3.
The Problem
We hinted at this in an earlier article, but it’s time to face it. None of the classes we’ve looked at in earlier articles were descendants of MODX classes. In other words, none of them “extended” an existing MODX class. Here’s the problem shown in a general way. In Revo 2, extending a typical class, like modUser, modResource, or modElement, would look like this:
class MyClass extends modUser {
/* Your class code here */
}
In Revo 3, it would look like this:
class MyClass extends MODX\Revolution\modUser {
/* Your class code here */
}
It seems like the following code should work, since unnecessary use
statements are discarded at compile-time:
use MODX\Revolution\modUser
class MyUserClass extends modResource {}
When you try to run that in Revo 2, though, you get this error:
Uncaught Error: Class 'MODX\Revolution\modUser' not found
So that’s out, what about this?
$className = $isMODX3? 'MODX\Revolution\modUser' : 'modUser';
class MyUserClass extends $className {}
This version won’t run in either Revo 2 or Revo 3. It generates this error: PHP Parse error: syntax error, unexpected '$className' (T_VARIABLE), expecting identifier.
That’s because the name of the class being extended is parsed a compile-time, where the variable’s value is not available.
Here’s another try:
if ($isMODX3) {
class MyUserClass extends MODX\Revolution\modUser {}
} else {
class MyUserClass extends modUser {}
}
This last option actually works, but your entire class would be between the curly braces—Twice! You’d have two complete copies of the class, and you’d have to maintain both of them, which kind of defeats the purpose of having code that will run in both versions of MODX.
The Solution
There is a way out of this dilemma. It’s often referred to as the “Dynamic Parent” method. I wish I knew who first came up with this ingenious solution, so I could give them the credit they deserve.
The Dynamic Parent method involves having two abstract classes in an if/else statement, so that only one of them is declared. The two dynamic parent classes must have the same name, but they extend different classes. Your class just extends whichever one of them gets created. This may make more sense with an example. Here’s a class that extends modUser
:
$isMODX3 = $modx->getVersionData()['version'] >= 3;
if ($isMODX3) {
abstract class DynamicUserParent extends MODX\Revolution\modUser {}
} else {
abstract class DynamicUserParent extends modUser {}
}
class MyUserClass extends DynamicUserParent {
protected modX $modx;
protected string $prefix;
public function __construct(&$modx) {
$this->modx = $modx;
$this->prefix = $modx->getVersionData()['version'] >= 3
? 'MODX\Revolution\\'
: '';
parent::__construct($modx);
}
public function getCount() {
$users = $this->modx->getCollection($this->prefix . 'modUser');
return count($users);
}
}
$myUserClass = new MyUserClass($modx);
$count = $myUserClass->getCount();
$u = $myUserClass->set('username', 'SomeUser');
echo "\nCount: " . $count;
if ($u === true) {
echo "\nSuccess";
} else {
echo "\nFailure";
}
We need to have a constructor in our class, and call the parent constructor in our class (though not in our dynamic parent classes). This is necessary because at some level, an ancestor class needs to have $this-modx
set. Constructors are inherited like any function, so when we call parent::__construct()
in our class, PHP sees that the parent has no constructor and calls its parent’s constructor. In this case, the modUser
class also has no parent, nor does its parent (modPrincipal
), nor does its parent (xPDOSimpleObject
), so what actually gets called is its parent’s constructor, xPDOObject::__construct()
.
The ultimate parent here (xPDOObject
) takes only one argument in its constructor, the xPDO object. Since $modx
extends the xPDO class, we can send it instead. Be careful, though, the object you’re extending could have its own constructor, which might take other arguments in addition to this one. You’ll have to send those arguments along to the constructor if they’re not optional. Most of the commonly used MODX objects like modResource
, modElement
, modUser
, etc., just need the one argument, though modProcessor takes an optional second argument, a $properties
array.
The name of the two dynamic parent classes must be the same. It should also include some clue to what it’s going to extend (hence the “User” in “DynamicUserParent”). It’s not a good idea to have multiple classes called DynamicParent
. It might not cause problems, but it’s not a good practice and a good code editor will complain about it, so it’s best to create a unique name for each pair of dynamic parents. The names in each pair can be the same, since only one of them will be instantiated.
Technically, the dynamic parent classes don’t have to be abstract classes, but it’s appropriate since they should always be extended and should never be instantiated.
$myUserClass = $modx->newObject('MyUserClass');
MODX will handle the construction for you.
Obviously, the last six lines of the MyUserClass
code above would not be in your finished code. They’re there just to make sure the class is working as it should. If you see a count, and you see “Success” ($u
is true
), there are no errors in the code, your functions are being called, and you’re able to set an actual field of the MyUserClass
object.
Notice that we used the class prefix ($this->prefix
) in our class. If your code doesn’t refer to any other MODX objects, you can leave that part out.
If you prefer, you can skip the creation of the $isMODX3
variable and use this, since this class will only exist in Revo 3:
if (class_exists('MODX\Revolution\modUser')) {
Processors
Since we used the modProcessor
class as an example, here’s a dynamic parent solution for a class that extends that base processor class (it could also extend another processor class with minor changes):
$isMODX3 = $modx->getVersionData()['version'] >= 3;
if ($isMODX3) {
abstract class DynamicBaseProcessorParent extends MODX\Revolution\Processors\Processor {}
} else {
include 'MODX_CORE_PATH . model\modx\modprocessor.class.php';
abstract class DynamicBaseProcessorParent extends modProcessor {}
}
class MyProcessorClass extends DynamicBaseProcessorParent {
public $modx;
protected string $prefix;
public function __construct(&$modx, $properties, $options) {
$this->modx = $modx;
$this->prefix = $modx->getVersionData()['version'] >= 3
? 'MODX\Revolution\\'
: '';
parent::__construct($modx, $properties, $options);
}
public function process() {
/* Do something */
echo "Hello";
}
}
$properties = array(
'someKey' => 'someValue',
);
$options = array(
'processors_path' => 'path/to/processor',
);
$myProcessorClass = new MyProcessorClass($modx, $properties, $options);
$myProcessorClass->process();
The abstract processor classes require that you implement a process()
method. The second and third arguments to the constructor are optional, but you may want to send them, especially if you're calling $modx->runProcessor()
to execute it, since runProcessor()
will need the path to the processor file in the $options
third argument. Usually, that path will be the same in Revo 2 and Revo 3. If not, you’ll have to send a different path depending on which version of MODX you’re running under.
Controllers
This works pretty much like the regular classes like modUser
and modResource
. You’ll still need to use the dynamic parent method, but the classes you’d want to extend, modManagerController
, modExtraManagerController
, and modParsedManagerController
still exist in Revo 3. All three are abstract classes, so you’ll have to extend one of them. There is no modController
or Controller
class.
The modManagerController
class is the base class and extends nothing. It provides some basic methods for setting placeholders, getting a template, and some other things, but many of its methods are empty.
The modExtraManagerController
extends modManagerController
. It adds a defDefaultController()
method, which sets the default controller to index
, and overrides the getPagetitle()
method to return an empty string. Again, many of its methods, including the required process()
method are empty. It also provides a render()
method, which sets placeholders and returns the HTML for the view.
The modParsedManagerController
class extends modExtraManagerController
. It provides only two additional methods. The initialize()
method adds come code to make an Extra play nice with modExt and calls its parent’s initialize()
method. You may want to override that and call parent::initialize in your controller. It also provides a render()
method, which sets placeholders and returns the HTML, but with the added feature of processing any MODX tags in the output.
Other Uses
This is slightly off-topic, but I wanted to mention that the dynamic parent method can be used for other things than making code run in Revo 2 and Revo 3. It’s useful any time the code of a class might be working under different circumstances.
For example, when creating GoRevo, a premium MODX Extra for converting a site from MODX Evolution to MODX Revolution, I needed to create a .zip archive, add to it, and later unzip it.
ZipArchive is included in PHP for this, but I wasn’t sure the user would have that enabled. As an alternative, I included a copy of PclZip in the package. Rather than have two completely different sets of code for the two zip utilities, I created two dynamic parents that extended one or the other of them. My own class serves as middleware to deal with the details and handle the differences between the classes. Here’s an abbreviated version of the code:
if (class_exists('ZipArchive', false)) {
class DynamicZipParent extends ZipArchive {
public function addFile($location, $name) {
parent::addFile($location, $name);
}
}
} else {
require 'pclzip.lib.php';
class DynamicZipParent extends PclZip {
public function open($path) {
Pclzip::PclZip($path, $options);
return true;
}
public function addFile($path) {
$toRemove = MODX_BASE_PATH;
if (empty($path)) return;
$path = ltrim($path, '/\\');
$this->add($path, PCLZIP_OPT_REMOVE_PATH, $toRemove);
}
public function close() {}
}
}
/* My Class */
class FlxZipArchive extends DynamicZipParent {
protected $excludeDirs;
protected $excludeFiles;
public function init($excludeDirs = array(), $excludeFiles = array()) {
@unlink('evo.zip');
$this->excludeDirs = $excludeDirs;
$this->excludeFiles = $excludeFiles;
}
/* Add Dir with Files and Subdirs to the archive
* @param string $location Real Location
* @ adapted from code by Nicolas Heimann */
public function addDir($location, $name) {
/* Prep Code here */
$this->addDirDo($location, $name);
}
/**
* Add Files & Dirs to archive.
*
* @param string $location Real Location
* @param string $name Name in Archive
* @ adapted from code by Nicolas Heimann */
private function addDirDo($location, $name) {
/* Actually add the file and directories */
$do = (filetype($location . $name) == 'dir')
? 'addDir'
: 'addFile';
while ($file = readdir($name)) {
if ($file == '.' || $file == '..') {
continue;
}
$this->$do($location . $file, $name . $file);
}
}
}
addFile()
method in the first dynamic parent above is unnecessary, since the parent’s method would be called automatically, but I think it makes the code clearer.
Now, in my code, I can do something like this without worrying about which archiving tool is being used:
function zipIt($fileName, $folderToArchive) {
$zip_file_name = $fileName;
$the_folder = $folderToArchive';
$za = new FlxZipArchive($fileName);
/* These will normally not be empty */
$excludeDirs = array();
$excludeFile = array();
$za->init($excludeDirs, $excludeFiles);
$res = $za->open($this->dir . '/' . $zip_file_name,
$options);
if ($res === true) {
$za->addDir($the_folder, basename($the_folder));
$za->close();
} else {
echo 'Could not create a zip archive';
}
}
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.