![]() |
|
URL of the article:
Issue:
05.2003
Portability and Backwards Compatibility
Ever have that feeling you forgot something? This is it.
Davey Shafik
Whenever we develop a commercial or open-source application, we want to be sure that it will work on as many platforms as possible with little work. Whilst a one size fits all solution is not always possible, a little knowledge, some foresight, and a little bit of planning can go a long way.
Portability and backwards compatibility are quite often thought of as being completely separate, but they are in fact inherently linked. We shall discuss these links, and how to combat the issues they raise. Introduction There have been quite a few changes to PHP in recent versions, starting with the addition of superglobals in PHP 4.1.0, register_globals now defaulting to off in 4.2.0 and the introduction of streams in PHP 4.3.0, along with the standard bout of new and deprecated functions. This article aims to help your application work around all of these obstacles and help you reach the largest audience with the least amount of work. register_globals One of the biggest changes, and certainly the one with the largest impact on the most scripts, is the change in register_globals default behaviour. Before PHP 4.2.0 the default register_globals setting was On. This meant that any variables such as those originating in the GET, POST or COOKIE portions of the HTTP request were registered in the global scope. This was one of the biggest attractions to PHP for a lot of people - post a form to the script, its values are there under a variable whose name is the same as the form elements name. The reason for this change, as noted by the PHP Group, is security. Many developers are not aware of the dangers here, and how to code correctly to avoid them. It's easier just to do without this `feature' and use the second biggest change in PHP's recent history superglobals. However, because this change only occurred in PHP 4.1.0, if your web-hosting company is unwilling or unable to upgrade beyond 4.0.6, you do not have access to these new superglobals. However, all is not lost! The original superglobal $GLOBALS has been in PHP since back in PHP3. Using this superglobal, we can emulate the new superglobals in versions of PHP prior to 4.1.0 By using $GLOBALS[`HTTP_*_VARS'] we can access the values we want from GET, POST, and other parts of the HTTP request within functions and other variable scopes, without needing to worry about whether or not superglobals exist. However, there is a drawback to this in PHP 5, which has introduced a new php.ini setting, register_long_arrays. This setting allows the older $HTTP_*_VARS to be turned off, the default for setting for which is on. We can however, get around this quite easily. As you see in the small function in Listing 1, just by creating variables with the same names as the new superglobals, and then calling them through $GLOBALS, we don't have to worry which version of PHP we're using. The rest of the code in this article will rely on this function to access its EGPCS data. Listing 1 <?php /** * Create $_* variables in the GLOBAL scope for PHP <= 4.0.6 * @return void */ function setGlobals() { if (!isset($_GET) && (isset($GLOBALS['HTTP_GET_VARS']))) { $GLOBALS['_GET'] = $GLOBALS['HTTP_GET_VARS']; } if (!isset($_POST) && (isset($GLOBALS['HTTP_POST_VARS']))) { $GLOBALS['_POST'] = $GLOBALS['HTTP_POST_VARS']; } if (!isset($_GET) && (isset($GLOBALS['HTTP_GET_VARS']))) { $GLOBALS['_GET'] = $GLOBALS['HTTP_GET_VARS']; } if (!isset($_SESSION) && (isset($GLOBALS['HTTP_SESSION_VARS']))) { $GLOBALS['_SESSION'] = $GLOBALS['HTTP_SESSION_VARS']; } if (!isset($_COOKIE) && (isset($GLOBALS['HTTP_COOKIE_VARS']))) { $GLOBALS['_COOKIE'] = $GLOBALS['HTTP_COOKIE_VARS']; } if (!isset($_ENV) && (isset($GLOBALS['HTTP_ENV_VARS']))) { $GLOBALS['_ENV'] = $GLOBALS['HTTP_ENV_VARS']; } if (!isset($_FILES) && (isset($GLOBALS['HTTP_FILES_VARS']))) { $GLOBALS['_FILES'] = $GLOBALS['HTTP_POST_FILES']; } if (!isset($_REQUEST) && (isset($GLOBALS['HTTP_GET_VARS'])) || (isset($GLOBALS['HTTP_POST_VARS'])) || (isset($GLOBALS['HTTP_COOKIE_VARS']))) { // quick and dirty error_reporting() hack to // stop any notices showing - you could // extend this (and the overhead) by checking // for each one and then + it // you might also want to make it take note of the EGPCS order $old_error_reporting = error_reporting(0); $GLOBALS['_REQUEST'] = $GLOBALS['HTTP_GET_VARS'] + $GLOBALS['HTTP_POST_VARS'] + $GLOBALS['HTTP_COOKIE_VARS']; error_reporting($old_error_reporting); } if (!isset($_SERVER) && (isset($GLOBALS['HTTP_SERVER_VARS']))) { $GLOBALS['_SERVER'] = $GLOBALS['HTTP_SERVER_VARS']; } } // call our new function setGlobals(); // now if we call our variables like this, // we should never have a problem echo $GLOBALS['_SERVER']['PHP_SELF']; ?> Where's that function gone? There are four reasons a script might not be able to find a function it's trying to use:
// Check for the function if (!function_exists('file_get_contents')) { // include our version include('file_get_contents.lib.php'); } $file = file_get_contents('foo.php'); ?> Listing 3 <?php // file_get_contents.lib.php function file_get_contents($filename) { return implode("\n", file($filename)); } ?> It's a good idea to check the manual for new functions regularly. People often add notes that include userland implementations of these fallback functions, as we have done with file_get_contents here. Extension! Extension! My Kingdom for an Extension! So, you've just wrote your brand spanking new application, it does everything and more. It's the Swiss Army Knife of PHP applications, what could possibly go wrong? Argh! What do you mean not every server is compiled --with-discombobulate!? Its ok to have a single function missing, but replacing an entire library would be time consuming and in most cases highly inefficient. So what do we do? You cry. Unfortunately, in this case, all you used to be able to do is display a message of some sort and stop the script. However, now there is another way... PEAR (PHP Application and Extension Repository) - or more precisely, PECL (PHP Extension Code Library). PECL is a way to easily distribute PHP Extensions, both standard extensions (such as mcrypt, currently being moved into PECL) and home grown extensions. Whilst it is not within the bounds of this article to discuss the creation, distribution and installation of extensions using PEAR, I would definitely suggest a trip to the PEAR website if you feel this might be the way for your application to go. Once you have your extension being distributed with your application, you can use it from within your application by using the dl() function. Listing 4 is an example of using dl() Listing 4 <?php if (!extension_loaded('mcrypt')) { if (strtoupper(substr(PHP_OS,0,3)) == 'WIN') { dl('php_mcrypt.dl'); } else { dl('mcrypt.so'); } } echo "<h1>Supported Mcrypt modes</h1>"; foreach (mcrypt_list_modes() as $mode) { echo $mode . '<br />'; } ?> PHP INI Settings There are many PHP INI settings all of which can be set differently on any server your script is being run on, with an enormous amount of permutations (2.6953641378882E+245 with the default configuration). Luckily, we can control quite a few of these settings ourselves. However, some we should just learn to not rely on. A perfect example of the latter, is short_open_tag, by using and not using or we never need to worry about this setting. The Tricks the INI file plays Up until a few months ago when after a great deal of agonising something was done to rectify the problem, the documentation for the INI settings was a bit sketchy, even now, the new docs are not as good as they could be (will fix that, look for additions soon). One of the things not made clear is that settings set using php_admin_value or php_admin_flag in httpd.conf cannot be overridden at all in .htaccess or by using ini_set(), the option, regardless of its original type, is now seen as being PHP_INI_SYSTEM. There is no way around this unfortunately, you will need to contact your system admin and ask them to set non-PHP_INI_SYSTEM settings using php_value and php_flag. The Ultimate Weapon It is possible to write our applications using a framework which will allow our application to run as well as it can on any given platform. By this, I mean that it will do the following:
BackPort is a class, written by myself, to aid the Backwards Compatibility and Portability of PHP Applications. You will find the full source to BackPort on the CD accompanying this magazine. BackPort is also awaiting acceptance into PEAR at this moment of writing. In the following parts of this article, you will learn to use the BackPort framework to make your scripts the portable packages you want them to be. BackPorting Functions Functions are undoubtably one of the most important part of any developers arsenal, therefore when a function is added, deprecated or removed, this affects us. One of BackPorts features is dealing with this. By allowing us to indicate which functions are new, what version they were added, and how to compensate in older versions, BackPort will handle everything for us. Lets now look at an example of using BackPort. Listing 5 <?php require 'backport.class.php'; $backport = new backport; // file_get_contents function only got added in // PHP 4.3.0 - we want to fallback to fileLoad // in fileLoad.func.php if we need too $backport->registerFunction( 'file_get_contents', 'fileLoad', 'fileLoad.func.php'); // Add our dependency for the function, in this // case its a version depenency, we 'load' a file // if it doesn't meet the requirements (more than // or equal to PHP 4.3.0) $backport->addDependency( 'file_get_contents', 'version', E_ERROR,'load', '4.3.0', '>='); // call our function through the framework, // arg1 == function, arg2+ == args for it $file_contents = $backport->callFunction( 'file_get_contents', 'backport.diff'); echo "<pre>$file_contents</pre>"; ?> As you can see from Listing 5, we need to call 2 methods to register (registerFunction()) and add dependencies (addDepenendency()) for our function before calling it using the callFunction() method. callFunction() works just like PHPs call_user_func() function, and is indeed, a wrapper for it, for the most part. callFunction() takes the original function name as the first argument, followed by a variable-length argument list which are passed as arguments to that function. The example in Listing 5 uses a `version' dependency, however BackPort also allows for the `exists' dependency. This - as you might expect - simply checks that a certain functions exists, if not it then uses the failsafe. Table 1 lists the dependencies and what they can be used with. Table 1: Dependencies
Table 2: Options
BackPorting Classes is very similar to BackPorting Functions, however, you must call them differently, this time we use the callClass() method. Listing 6 shows BackPorting of classes, you will note the reference used, this is essential, otherwise you will be working on a copy of the object, and not the object itself. Listing 6 <?php require 'backport.class.php'; $backport = new backport; // Register our class, helloWorld (which is // in helloWorld_43.class.php) - we don't // provide arguments 3 and 4 (failsafe and // failsafe_file) because we are using the // 'hide' failsafe - these are not needed $backport->registerClass( 'helloWorld', 'helloWorld_43.class.php'); // Add our dependency, this must be 'version' // or 'error' - we failsafe to 'hide' $backport->addDependency( 'helloWorld', 'version', E_NOTICE, 'hide', '4.3.0', '>='); // if the dependency didn't fall through, the // class is here, so we can use it if ($backport->isVisible('helloWorld')) { $hello =& $backport->callClass('helloWorld', phpversion()); $hello->say(); } else { // dependency not met echo "HelloWorld was hidden!"; } ?> As you can see, this time we use the `hide' failsafe. The implementation of this failsafe type is to allow you to disable parts of your application which will not run on the current PHP system. This allows the application to degrade gracefully. For instance, if your news application automatically creates RSS feeds using XSLT, and the XSLT extension is not compiled into the current PHP build, this feature can simply be hidden. Note again, the use of a reference within this example. As you will also notice, this time registerClass() is used instead of registerFunction, we still use addDependency() as with backporting functions. BackPorting Extensions BackPorting of Extensions is slightly different in that you don't call the extension, you just add the Dependency and it is resolved. The addExtension() method has the following prototype: bool addExtensions( string extension [, string extension_file_nix[, string extension_file_win[, failsafe_file]]]) The first argument, extension, is the name of the extension, the second and third, extension_file_nix and extension_file_win are the local path to a .so or .dll extension file respectively which can be loaded at run time, the last, failsafe_file is a userland implementation of the extension. Note: windows .dll extension loading at runtime is not yet implemented, this requires thread discovery, as dl() does not work on threaded web-servers. With BackPorting Extensions, the behaviour is slightly different on windows and *nix, the process is as follows at present:
require_once 'backport.class.php'; $backport = new backport; // Register our Extension 'ncurses', we have // our failsafes ncurses.so and also // ncurses.ext.php is a userland implementation $backport->registerExtension( 'ncurses', 'ncurses.so', FALSE, 'ncurses.ext.php'); // Add our dependency, this is always exists, // notice the E_NOTICE error type, this means that // it will failsafe to "hide" if all else fails $backport->addDependency( 'ncurses', 'exists', E_NOTICE, 'load'); // Check if the extension is loaded OR if the // function_exists, we can have one without the // other as the userland implementation MUST // implement the same API if ((extension_loaded('ncurses')) || (function_exists('ncurses_echo')) { echo "SUCCESS!"; } else { // Check if the extension is hidden so we can // skip any non-essential code that uses it if($backport->isHidden('ncurses')) { echo "ncurses is hidden!"; } } ?> You will notice the use of E_NOTICE and that we also supply ncurses.ext.php in addition to the *nix .so - we did not supply a windows .dll file because ncurses does not exist on Win32 at present, the fallback_file will be used on windows. For cross-platform extensions, such as mcrypt I recommend passing the .dll despite the fact that BackPort does not handle it at present, it makes the code more forwards compatible, and when BackPort does handle this correctly, all you will need to do is supply the .dll and upgrade backport.class.php (Note: this name will likely change to just backPort.php if it is included in PEAR) Table 3 contains the prototypes for all public methods of BackPort.
With the release of PHP 5 Beta now out in the wild, BackPort could play a vital role in the move to PHP 5 from PHP 4, whether in the longterm as a permenant solution to insure your application works on PHP 4 and 5, or if overhead is a major concern, the short-term whilst you write a full port to PHP 5. BackPort is an effective solution to a problem that, although it will evolve, will never leave us. What does the Future hold? Backwards Compatibility has to end somewhere, we cannot indefinitely support old versions of PHP, I personally consider PHP 4.0.6 the lowest common denominator. The reasoning for this is that PHP 4.0.6 spent a long time as the latest stable release, and was therefore installed on many machines. A lot of these machines have never been upgraded through admin relucatance, laziness or with some platforms (i.e. Cobalt RaQ) because it breaks warranty and/or the system control panel. Another large consideration in what to choose as your lowest common denominator is Debian, as a hugely popular GNU/Linux distribution its stable release's packages are somewhat older than the current PHP release. At the time of writing, they are at PHP 4.1.2, this means that although it has the superglobals it does not have some newer functions and also does not feature the streams API, I will go out on an admittedly rather thick limb, and say that that it will be some time before we start to see PHP 5 appearing in the stable packages. Portability is also a moving target, but this moves a lot slower. With major Windows releases occurring at least 2 years apart, and *nix changes not mattering all that much as the PHP Group is able to keep on top of them a whole lot better, it is just a case of keeping your eye on the ball. There will always be some things that just cannot be done on windows and to a lesser degree, things that can be done on Windows that cannot be done on *nix, here userland implementations can be created, but often are not desirable due to speed and resource limitations. I hope that this article has provided you with an insight into this much forgotten world that is perhaps one of the most crucial parts of commercial development, a product sells much better when it has more users to sell to. Additionally, I hope that you find BackPort as useful as I have intended it to be. Please feel free to email me with any questions you have about the subject and about BackPort at davey@php.net Links and Literature
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|