PHP include files & DB-enabled apps

from the Artful MySQL Tips List


How do we code the locations of "include" files so our PHP scripts can find them in any environment where the scripts are likely to run?

There's not much written about this, and that's odd because everyone who has a development environment and a production environment has to solve the problem.

If you develop non-trivial PHP webapps, you have a development environment on a local server, and a production environment on a web server you own, lease or share. Those servers can't have the same web address, and rarely have exactly the same file structure. A lot of PHP development is done in Windows, but most PHP webapps run on Linux servers, so the odds are that your development and production servers run different operating systems. And if you use an integrated PHP development environment (even a simple one like PHP Designer), that tool may run your scripts in debugging mode by invoking PHP in the background from a commandline instead of from your development web server. You may even need to debug PHP scripts running on some other machine, in which case $_SERVER[] info returned by a debugger running on your local machine will be entirely misleading.

True, there's one circumstance when the php.ini include_path setting can solve this problem entirely—when the development server is a fairly exact model of a dedicated production web server. That's not common, but it happens.

Much more often, the development server is home to many projects, and so is the web server but to different projects. And if the web server does shared hosting, it will be awkward or even impossible to use a general purpose php.ini include_path setting.

So how are we to bullet-proof include and require directives? It's irresponsibly insecure to put scripts containing database connection or other sensitive information in the web server document tree, so a database-enabled webapp needs access to an includes folder which may be app-specific, is outside the web document tree, and is specified at runtime. A common and convenient location is one level below the web document root folder, for example off /home/username/ on a shared hosting server, off /apache/ on a Linux or Windows server that you own or lease, off /inetpub/ on an IIS server, or off the /var/ folder in a Linux development machine. Usually, our script will have to run in two of these environments. And there's no guarantee that our debugging environment is actually a web server at all, so there may be three environments that "include" file code has to manage seamlessly.

First, let's lay out the simplest possible solution:

1. On your local development server, create the folder apache/includes for your include files. On your hosting server, create an includes folder in the folder just below public_html.

2. Assuming your include file is called myincfile.php, start your web pages with code like this ...

$host = ( isset( $_SERVER['HTTP_HOST'] )) ? $_SERVER['HTTP_HOST'] : "localhost";
if( $host=='localhost' ) {
  $incfile = "c:/apache/includes/myincfile.php";
}
else {
  $docroot = $_SERVER["DOCUMENT_ROOT"];
  $home = substr( $docroot, 0, stripos( $docroot, "/public_html" ));
  $incfile = "$home/includes/myincfile.php";
}
require_once( $incfile );

That will work for many simple cases. If your development and production environments offer more challenging complications, read on.

For convenience, define a debug flag and set it. Again call the includes folder "includes":

$debug = TRUE;
$include_folder_name = "includes";

If you manage multiple webapps, it may be best to add app-specific subfolders off the includes folder, and adjust require_once() calls accordingly.

PHP debuggers often do not populate $_SERVER['HTTP_HOST'] or $_SERVER['DOCUMENT_ROOT'], so we need an alternative way to find the path of the script that is running. Dirname(__FILE__) returns the full path of the PHP script it is called from. PHP silently translates the forward slash to the absurd Windows backslash folder separator in path arguments sent to Windows, so once we substitute forward slashes for backslashes in paths that we fetch from the operating system, we can forget about Windows backslashes:

define( 'FDIV', "/" );
define( 'CURRENT_FOLDER', str_replace( "\\", FDIV, dirname( __FILE__ )));

If the script is running in a debugger, we can't count on getting info from $_SERVER['HTTP_HOST'] or $_SERVER['DOCUMENT_ROOT'], so we need a list of known document root folders. If your system uses a home folder name that's missing from this list, add it:

$known_web_roots = array( "public_html", "htdocs", "wwwroot", "www" );

If the script is running on a web server, $_SERVER['HTTP_HOST'] and $_SERVER['DOCUMENT_ROOT'] give us the path information we need. Otherwise, parse CURRENT_FOLDER to find the SERVER_ROOT path segment just before the web home folder. The only way this code can fail is if the script is running in a debugger on a system whose "home" folder is not listed in $known_home_roots, so if you run this section of code and it fails, add your home folder name to $known_home_roots and try again:

if( isset( $_SERVER['HTTP_HOST'] )) {
  define( 'HTTP_HOST', str_replace( "\\", FDIV, $_SERVER['HTTP_HOST'] ));
  if( isset( $_SERVER['DOCUMENT_ROOT'] ) && !empty( $_SERVER['DOCUMENT_ROOT'] )) {
    define( 'SERVER_ROOT', str_replace( "\\", FDIV, $_SERVER['DOCUMENT_ROOT'] ));
    define( 'SITE_ROOT', "http://" . HTTP_HOST );
  }
  else exit( "Unknown server architecture, cannot proceed." );
}
else {
  foreach( $known_web_roots as $f ) {
    $str = stristr( CURRENT_FOLDER, $f, TRUE );
    if( $str !== FALSE ) {
      define( 'SERVER_ROOT', $str . $f );
      break;
    }
  }
}
if( !defined( 'SERVER_ROOT' )) exit( "Unknown server architecture, cannot proceed." );

At this point, the script knows its SERVER_ROOT path. Here are some debugging stubs for results to this point:

if( $debug ) {
  echo 'CURRENT_FOLDER: ', CURRENT_FOLDER, "<br />";
  echo 'HTTP_HOST: ', ( defined( 'HTTP_HOST' ) ? HTTP_HOST : "Unknown" ), "<br />";
  echo 'SITE_ROOT: ', ( defined( 'SITE_ROOT' ) ? SITE_ROOT : "Unknown" ), "<br />";
  echo "SERVER_ROOT: ", SERVER_ROOT, "<br />"; 
}

The final step: find the home folder substring in the SERVER_ROOT path, define it as HOME, and build the definition of INCLUDE_FOLDER by concatenating HOME and the $include_folder_name:

foreach( $known_web_roots as $f ) {
  $pos = stripos( SERVER_ROOT, $f );
  if( $pos !== FALSE ) {
    define( 'HOME', substr( SERVER_ROOT, 0, $pos ));
    define( 'INCLUDE_FOLDER', HOME . $include_folder_name . FDIV );
    break;
  }
}   

If the previous code succeeded but this loop fails, we missed an exigency: let us know about it.

Here is the complete script:

<?php

/*
 * includes.php
 * artfulsoftware.com
 * Usage:
 *   adjust $include_folder_name if need be for an app-specific include folder
 *   require_once( "includes.php" ) in web script
 *   then in that script, reference required files with require_once( INCLUDE_FOLDER . <filename> );
 */

// SETTINGS
$debug FALSE;
define'FDIV'"/" );
define'CURRENT_FOLDER'str_replace"\\"FDIVdirname__FILE__ )));
$known_web_roots = array( "public_html""htdocs""wwwroot""www" );
$include_folder_name "includes";

// FIND WHERE THIS SCRIPT IS RUNNING
if( isset( $_SERVER['HTTP_HOST'] )) {
  
define'HTTP_HOST'str_replace"\\"FDIV$_SERVER['HTTP_HOST'] ));
  if( isset( 
$_SERVER['DOCUMENT_ROOT'] ) && !empty( $_SERVER['DOCUMENT_ROOT'] )) {
    
define'SERVER_ROOT'str_replace"\\"FDIV$_SERVER['DOCUMENT_ROOT'] ));
    
define'SITE_ROOT'"http://" HTTP_HOST );
  }
  else exit( 
"Unknown server architecture, cannot proceed." );
}
else {
  foreach( 
$known_web_roots as $f ) {
    
$str stristrCURRENT_FOLDER$fTRUE );
    if( 
$str !== FALSE ) {
      
define'SERVER_ROOT'$str $f );
      break;
    }
  }
}
if( !
defined'SERVER_ROOT' )) exit( "Unknown server architecture, cannot proceed." );

// STUBS
if( $debug ) {
  echo 
'CURRENT_FOLDER: 'CURRENT_FOLDER"<br />";
  if( 
defined'HTTP_HOST' )) echo 'HTTP_HOST: 'HTTP_HOST"<br />";
  if( 
defined'SITE_ROOT' )) echo 'SITE_ROOT: 'SITE_ROOT"<br />";
  echo 
"SERVER_ROOT: "SERVER_ROOT"<br />"
}

// BUILD INCLUDES FOLDER NAME
foreach( $known_web_roots as $f ) {
  
$pos striposSERVER_ROOT$f );
  if( 
$pos !== FALSE ) {
    
define'HOME'substrSERVER_ROOT0$pos ));
    
define'INCLUDE_FOLDER'HOME $include_folder_name FDIV );
    break;
  }
}   
if( !
defined'HOME' )) exit( "Unknown home folder, cannot proceed." );

if( 
$debug ) {
   echo 
'HOME: 'HOME"<br />";
  if( 
defined'INCLUDE_FOLDER' )) echo 'INCLUDE_FOLDER: 'INCLUDE_FOLDER"<br />";
}

?>


How to use it in each webapp environment:
  • create the includes folder off your "home" folder as described above
  • put a copy of this code as "includes.php" in desired webapp folders
  • add require_once("includes.php") to target PHP scripts
  • for each "include" file, add require_once( INCLUDE_FOLDER . <filename> );
  • to the relevant script.
Then you're done with fussing about "include" files.

Return to the Artful MySQL Tips page