Ever have that feeling you forgot something? This is it.
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:
- you forgot to code it,
- you forgot to include/require it,
- it doesn't exist in the PHP version the script is running on,
- the function is defined in an extension which is not available.
Whilst the first two are coding errors not discussed in this article, the second pair pose serious problems for projects which aim to be portable and backwards compatible. Listing 2 shows one method for falling back on a userland implementation of an unavailable function (in this case, the eminently useful
file_get_contents[1]), while Listing 3 shows the implementation. This is commonly referred to as a fallback function.
Listing 2 <?php
// 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:
- Allow you to create custom dependencies for functions, classes and extensions
- Allow for the use of userland fail-safe (or .so libraries in the case of extensions)
- Allow for us to hide non-essentials parts of our application if dependencies are not met
- As a last resort - error.
The code for this job is
BackPort.
BackPort
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
|
Dependency
|
Action |
For |
|
version
|
Compares current PHP version against dependency requirement
|
Functions, Classes
|
|
exists
|
Checks that the dependency exists
|
Functions, Extensions |
|
The example also uses the `
load' failsafe, again BackPort allows
for more than one type of failsafe. The options available to you are shown in
Table 2.
Table 2: Options
| Failsafe |
Action |
For |
| failsafe |
Use Alternate Function |
Functions |
| load |
Load a file and use failsafe within it |
Functions, Classes, Extensions |
| error |
Omit an error, and optionally halt the script. If error is not
fatal, mark as hidden (see below) |
Functions, Classes, Extensions |
| hide |
Mark as hidden, allows you to bypass unessential parts of your
application for smart degradation |
Functions, Classes, Extensions |
|
BackPorting Classes
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:
- Check if extension is compiled in/included in php.ini
If not, on *nix try to dl() the .so, this will be added to win32 in the future
- If this fails (enable_dl is off or safe_mode is on, or file is not found) try to use the failsafe_file
- If this also fails, if the error_type (see addDepenendency()) is not E_ALL, `hide' the extension, otherwise issue a fatal error.
Listing 7 shows an example of BackPorting Extensions.
Listing 7 <?php
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.
| Method |
Prototype |
Note |
| iniSet() |
bool iniSet( string ini_param, mixed value) |
Sets php.ini settings and allows for easy reversion using iniRevert()
|
| iniRevert() |
bool iniRevert( string ini_param) |
Reverts php.ini settings set using iniSet() |
| registerFunction() |
bool registerFunction( string function[, failsafe[,failsafe_file]])
|
Add function to the BackPort registry |
| registerClass() |
bool registerClass( string class, string class_file[, string
failsafe[, string failsafe_file]]) |
Add class to the BackPort registry |
| registerExtension() |
bool registerExtension( string extension[, string extension_file_nix[,
string extension_file_win[, string failsafe_file]]]) |
Add extension to the BackPort Registry |
| addDependency() |
bool addDepenency( string name, string dependency_type, int
error_type, string failsafe_type[, string dependency_version[, string
dependency_version_relation]]) |
Add a dependency for one of either function, class
or extension as above |
| isHidden() |
bool isHidden( string name) |
Check if a function, class or extension is
hidden or not, returns TRUE if it is hidden |
| isVisible() |
bool isVisible( string name) |
Alias of isHidden() returns the opposite, TRUE is
it is not hidden |
| callFunction() |
mixed callFunction( string function[, mixed arg1[, mixed ]])
|
Call a function from the registry whose dependencies have
been resolved |
| callClass() |
object callClass( string class[, mixed arg1[, mixed ]])
|
Call a class from the registry whose dependencies have been
resolved |
|
BackPorts Future
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