Savant: Revoking the 2.3.0 release

I am revoking the Savant 2.3.0 release from yesterday. The file will still be available, but I can no longer vouch that it will work everywhere for everyone, and I cannot promise that the new function provided therein ("calling of plugins as native methods") will be in a future Savant 2.x release. I recommend reverting to 2.2.0, particularly for PHP5 users.

For 2.3.0, the only major change was that you could call plugins as native Savant functions through the magic of overload() and __call(). The use of __call() turned out to be unwise, as it exposed a serious compatibility issue between PHP4 and PHP5.

I'm a big believer in reporting failure as well as success; it's important to let others learn from your mistakes. It's embarrassing, but necessary. This is one of those times, I'm afraid. The use of __call() turned out to be a serious problem. Yes, overload() is marked as EXPERIMENTAL in the PHP4 documentation, and brother, do they mean it.

Read on for more information about how __call() bit me in the ass, and why aiming for simultaneous compatibility on PHP4 and PHP5 can be unexpectedly difficult when trying to do "neat stuff."

__call() in PHP4 and PHP5

The first problem is that the arguments for __call() are different in PHP4 and PHP5 and are enforced differently.

In PHP4, you need three arguments: __call($method, $params, &$return). $method is the method called, $params is an array of parameters passed to that method, and &$return is a reference to the variable into which the method call should return a value. The "real" return value of __call() is true (indicating that the call succeeded) or false (which will trigger an error that $method does not exist). You can leave off the &$return parameter if you like, and PHP4 will not complain.

In PHP5, you need exactly two arguments: __call($method, $params), both of which act the same as in PHP4. If you have a third argument, PHP5 will issue a parsing error (which means the script will not load). Under PHP5, the return value of __call() is passed the way you would expect with a normal method, which is why you don't need the &$return argument.

You can see already why this would cause trouble: any __call() definition that works in PHP5 will limit its PHP4 equivalent to never giving a return value (because you can't have the &$return argument in PHP5). Similarly, any fully-functional PHP4 __call() method cannot possibly work under PHP5 (because the &$return argument will cause the PHP5 parser o choke).

__call() in PHP4.3.2 and 4.3.8

When I originally tested the __call() method in 4.3.2, it would successfully set &$return to an object. However, in 4.3.8, it would not set &$return to an object (scalar or array came back OK, but no objects).

Possible Solutions

First, I tried having alternate class definitions in the same file, and picking the definition based on phpversion(). Did not work. PHP5 chokes when parsing the file with a "bad" __call() definition, even if its inside the PHP4 portion of the if() block.

Then I tried extending a base _Savant2 class into Savant2 class proper with an include at the end of the Savant2.php file; that is, if ($php4) include 'Savant2v4.php'; if ($php5) include 'Savant2v5.php'. Each of the version-specific files worked in their respective environments, and I thought I had a successful solution.

However, further testing showed that PHP 4.3.8 would not set &$return to an object (necessary in Savant as it reports plugin errors using the Savant2_Error class). To my recollection, it had worked properly in 4.3.2.

For me, this was the final straw; if __call() didn't work consistently even under 0.0.1 versions of PHP4, then it was not good for production use.

Whose Fault?

Mine, of course. First, I used overload() which is clearly marked as experimental. Then, I tested it thoroughly, but on only one version of PHP (4.3.2) and figured it would be forwards compatible (wrong!). These two errors, combined, resulted in a broken release under some versions of PHP. My goal for Savant is that it run everywhere, and this release failed to meet that goal; to boot, it broke severely under PHP5, which userbase is only growing.

I tried to fix the release, but I fear there is no elegant solution. Rather than excuse my bad planning through documentation (i.e., "Savant works this way under PHP4 but it's not quite right, and under PHP5 it works this other way, so look out!") I decided to pull the release. It's only been out 24 hours, and the added functionality is convenient, not critical. (Still embarrassing and irresponsible to have released it like that in the first place, though.)

So the blame lies with me; I'm sorry if I broke your app with this release. Having said that, oh how I wish the __call() method was identical in both PHP4 and PHP5.

Are you stuck with a legacy PHP application? You should buy my book because it gives you a step-by-step guide to improving your codebase, all while keeping it running the whole time.