I am evaluating moving a fairly large site from Classic ASP to PHP and of course I'm looking at all of the potential problems and speedbumps first. The PHP version will initially still operate under IIS.
Development involves a local mySQL server and a few local domains e.g. localhost.website.com, localhost.cdn.website.com, localhost.api.website.com etc. but the live system uses none of them and instead just uses the live domains etc.
At the moment in classic ASP the system switches between "live" and "dev" by using a semaphore file which is ONLY present on the server. It does this in global.asa and goes on to set a bunch of application variables dependent on the presence of the file.
I'm not especially familiar with PHP but having looked around it appears to operate a "share nothing" principle and so has no "application scope" stuff.
I'm wondering how I can achieve a similar environment where the code locally can be uploaded with no changes and the system automatically recognise the system it's on in an efficient way.
In the past, we have used this technique with a lot of success:
DevLevel.0
stands for Production, DevLevel.1
stands for Staging, DevLevel.2
stands for development. We had a PHP global include examine the __FILE__
magic constant and see what place we were in, and then set a DevLevel
constant accordingly. Then in various places, we would have:
if(DevLevel == 0)
run credit card in live mode;
else
run credit card in test mode;
But the problem with this was that we outgrew it. Sometimes sites have 2 staging locations, and 10 development locations, with subtle differences.
Since then, we developed an automated script which examines the environment and emits a Local.php in the project, which is NOT under version control. It's really flexible and allows us to auto-generate apache, nginx, mysql, postgresql, redis, ssl, and project configurations in any environment with one command.
But back to your project. Here is what I suggest:
/path/to/project/conf/example-local.php
/path/to/project/conf/local.php
/path/to/project/conf/.gitignore (contents: local.php)
(replace git ignore with whatever ignore mechanism your SCM system uses)
Put an "example" config in example-local.php
. Then, any time a developer or release engineer checks out a copy of it, they simply need to copy example-local.php
to local.php
and tweak it.
Your application should have a global include file (like global.php) which is included FIRST on every script PERIOD. It should in turn include local.php, and then verify that the proper constants were defined and valid.
Here is a snippet of a global.php
<?
///////////////////////////////////////////////////////////////////////////////
define('App_Path', dirname(dirname(__FILE__)));
define('CORE_PATH', App_Path.'/Web/');
ini_set("max_execution_time", "0");
// For the purposes of including pear embedded in the application
set_include_path(App_Path . '/PHP/pear' . PATH_SEPARATOR . get_include_path() );
require_once(App_Path . "/AutoConf/Local.php");
///////////////////////////////////////////////////////////////////////////////
// Utility function to assert that a constant has been defined.
function DefinedOrDie($constant, $message)
{
if(! defined($constant))
die("Constant '$constant' not defined in site configuration. $message");
}
// Utility function to check if a constant is defined, and if not, define it.
function DefinedOrDefault($constant, $default)
{
if(! defined($constant))
define($constant, $default);
}
/////////////////////////////////////////////////////////////////////////////////
// These are the constants which MUST be defined in the AutoConf file
DefinedOrDie('DevLevel', "DevelopmentLevel. 0=Production, 1=Preview, 2=Devel.");
if(DevLevel !== 0 && DevLevel !== 1 && DevLevel !== 2)
die("DevLevel constant must be defined as one of [0, 1, 2].");
DefinedOrDie('DEFAULT_HOSTNAME', "Canonical hostname of the site. No trailing slash or leading protocol!");
DefinedOrDie('DB_HOST', "Database server hostname.");
DefinedOrDie('DB_NAME', "Database name.");
DefinedOrDie('DB_USER', "Database login username.");
DefinedOrDie('DB_PASS', "Database password.");
if(DevLevel > 0){
DefinedOrDie('Email_OnlySendIfMatch', "Preg regex email must match to be sent.");
DefinedOrDie('Email_OverrideAddress', "Email address to send all emails to.");
DefinedOrDie('Fax_OnlySendIfMatch', "Preg regex fax number must match to be sent.");
DefinedOrDie('Fax_OverrideNumber', "Fax number to send all faxes to.");
DefinedOrDie('Text_OnlySendIfMatch', "Preg regex text message must match to be sent.");
DefinedOrDie('Text_OverrideNumber', "Text number to send all texts to.");
DefinedOrDie('Phone_OnlySendIfMatch', "Preg regex phone number must match to be sent.");
DefinedOrDie('Phone_OverrideNumber', "Phone number to place all phone calls to.");
DefinedOrDefault('PROCESS_BILLING', false);
DefinedOrDefault('MEMCACHE_ENABLED', false);
}else{
DefinedOrDefault('PROCESS_BILLING', true);
DefinedOrDefault('MEMCACHE_ENABLED', true);
}
///////////////////////////////////////////////////////////////////////////////
// These are the constants which MAY be defined in the AutoConf file
// Default settings for live site
if(DevLevel == 0)
{
define('DEVEL', false);
define('DEVEL_DEBUG', false);
error_reporting (E_RECOVERABLE_ERROR | E_ERROR | E_PARSE);
ini_set("display_errors", false);
}
// Default settings for preview site
elseif(DevLevel == 1)
{
define('DEVEL', true);
define('DEVEL_DEBUG', false);
error_reporting (E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED);
ini_set("display_errors", true);
}
// Default settings for development and other sites
else
{
// TODO: remove E_DEPRECATED, and take care of all warnings
define('DEVEL', true);
define('DEVEL_DEBUG', false);
error_reporting (E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED);
ini_set("display_errors", true);
}
// Memcache settings are required, here are defaults
DefinedOrDefault('MEMCACHE_HOST', 'localhost');
DefinedOrDefault('MEMCACHE_PORT', '11211');
Here is an example of a more complicated (but matching) local.php
<?php
define('DevLevel', 1);
define('DB_HOST', 'localhost');
define('DB_NAME', '*******');
define('DB_USER', '*******');
define('DB_PASS', '*******');
define('DEFAULT_HOSTNAME', '***********');
define('MEMCACHE_SESSION_HOST', 'localhost');
//define users who will be notified when different alerts are sent by the system. Separate users with a |
define('ALERT_LEVEL_NOTICE_SMS', '122342342319');
define('ALERT_LEVEL_NOTICE_EMAIL', 'j3@example.com');
define('PROCESS_BILLING', false);
define('Email_OnlySendIfMatch', '/(appcove.com|example.com)$/');
define('Email_OverrideAddress', false);
define('FAXES_LIVE', true); //false to use phaxio's test keys (no faxes are sent)
function HN_LiveToDev($host){
return str_replace('.', '--', $host).'.e1.example.net';
}
function HN_DevToLive($host){
$count = 0;
$host = str_replace('.e1.example.net', '', $host, $count);
if($count > 0){
return str_replace('--', '.', $host);
}else{
die('URL does not look like a dev URL. Could not translate DevToLive.');
}
}
I'm not familiar with IIS, but on apache I've had the webserver set an environment variable only on my dev box webserver config. Then in php:
define('DEV', isset($_ENV['my_env_var']));
This is faster than checking for files because there's never any IO. It also has the desirable property that it defaults to "live" mode in the absence of the environment variable.
$_ENV
is a predefined variable: http://php.net/manual/en/reserved.variables.php
beware - in php an undefined constant gets interpreted as an unquoted string, and will coerce to boolean true. Just make sure the constant always gets defined an you'll be fine.