Just wanted to update this topic with some solutions, which are now available to download and use from
http://www.timechannels.com
Since very early versions of php were not object-oriented, I made two versions of this: one for OO, and one that I'm calling "Legacy", which just uses global variables instead of static class variables.
Here's the OO version:
Code: Select all
<?php
/*
* This is the TimeChannels Artificial System Clock implementation class for PHP.
* Everywhere in your application codebase that obtains the date/time directly, replace with a call
* to the getSystemDate() function in this class.
*
* For simple unit-testing, you can call setToStaticTestState() and pass it a specific date-time value.
*
* For system, integration, acpt testing, call setToTestState() and pass it a specific channel id you have setup in
* the TimeChannels.com control panel.
*
* Return to normal time by calling setToProductionState()
*/
class TimeChannels {
private static $tcURL = "http://www.timechannels.com/GetChannel";
private static $tcURLms = "http://www.timechannels.com/GetChannelMS";
private static $tcAscState = "TimeChannels.state.Production";
private static $tcChannel = "0";
private static $tcAscDateTime = null;
private static $tcServiceURL = "http://www.timechannels.com/GetChannel";
private static $tcMillisecondMode = "false";
public static function setToProductionState() {
self::$tcAscState = "TimeChannels.state.Production";
}
public static function setToStaticTestState($aDateTimeValue) {
self::$tcAscState = "TimeChannels.state.TestDate";
self::$tcAscDateTime = $aDateTimeValue;
}
public static function setToTestState($aChannel) {
self::$tcAscState = "TimeChannels.state.TestChannel";
self::$tcChannel = $aChannel;
}
public static function setToMillisecondMode() {
self::$tcMillisecondMode = "true";
self::$tcServiceURL = self::$tcURLms;
}
public static function setToStandardMode() {
self::$tcMillisecondMode = "false";
self::$tcServiceURL = self::$tcURL;
}
public static function getSystemDate() {
if (self::$tcAscState == "TimeChannels.state.Production") return mktime();
if (self::$tcAscState == "TimeChannels.state.TestDate") return self::$tcAscDateTime;
if (self::$tcAscState == "TimeChannels.state.TestChannel") return self::getChannelFromTimeChannelsService(self::$tcChannel);
return mktime();
}
private static function getHTTPContent( $url ) {
$options = array( 'http' => array(
'user_agent' => 'timechannelsclient', // who am i
'max_redirects' => 10, // stop after 10 redirects
'timeout' => 120, // timeout on response
) );
$context = stream_context_create($options);
$page = @file_get_contents($url, false, $context);
$result = array();
if ($page != false)
$result['content'] = $page;
else if (!isset($http_response_header))
return null; // Bad url, timeout
$result['header'] = $http_response_header;
$nLines = count($http_response_header);
for ($i = $nLines-1; $i >= 0; $i--) {
$line = $http_response_header[$i];
if (strncasecmp("HTTP", $line, 4) == 0) {
$response = explode(' ', $line);
$result['http_code'] = $response[1];
break;
}
}
return $result;
}
private static function getChannelFromTimeChannelsService($aChannel) {
$url = self::$tcServiceURL . "?channel=" . $aChannel;
$result = self::getHTTPContent($url);
if ($result == null) return mktime();
if ($result['http_code'] != 200) return mktime();
$channelValue = $result['content'];
if (self::$tcMillisecondMode == "true") {
$retVal = (1 * $channelValue) / 1000;
}
else {
$year = 1 * substr($channelValue, 0, 4);
//echo "year: $year <br>";
$month = 1 * substr($channelValue, 5, 2);
//echo "month: $month <br>";
$day = 1* substr($channelValue, 8, 2);
//echo "day: $day <br>";
$hour = 1 * substr($channelValue, 11, 2);
//echo "hour: $hour <br>";
$minute = 1 * substr($channelValue, 14, 2);
//echo "minute: $minute <br>";
$ampm = substr($channelValue, 17, 2);
//echo "ampm: $ampm <br>";
if ($ampm == "PM") $hour += 12;
$retVal = mktime($hour, $minute, 0, $month, $day, $year);
}
return $retVal;
}
}
?>
Here's the sample usage/test code for the above:
Code: Select all
<?php
include 'TimeChannels.php';
$tab = " ";
echo "======================================================================<br>";
echo $tab . "<TimeChannelsModernClient><br>";
/////////// --- Don't do anything yet; test that TimeChannels class defaults to Prod. Mode:
echo $tab . $tab . "---> default result: " . date("Y-m-d h:i A", TimeChannels::getSystemDate());
echo "<br>";
/////////// --- Now set to Test Mode with a static date:
$staticDate = mktime(9, 15, 0, 3, 13, 1969);
TimeChannels::setToStaticTestState($staticDate);
echo $tab . $tab . "---> static test state result: " . date("Y-m-d h:i A", TimeChannels::getSystemDate());
echo "<br>";
/////////// --- Now set to Test Mode with a channel:
TimeChannels::setToTestState("2");
echo $tab . $tab . "---> channel 2 test state result: " . date("Y-m-d h:i A", TimeChannels::getSystemDate());
echo "<br>";
/////////// --- Now set to Millisecond Mode:
// Please note: millisecond timestamps are not supported directly in php, so this call will only be precise
// to the second.
TimeChannels::setToMillisecondMode();
echo $tab . $tab . "---> channel 2 test state result from milliseconds: " . date("Y-m-d h:i:s A", TimeChannels::getSystemDate());
echo "<br>";
echo $tab . "</TimeChannelsClient><br>";
echo "======================================================================<br>";
?>
Here is the "Legacy" version of the client:
Code: Select all
<?php
/* This is a rudimentary implementation of the TimeChannels Artificial System Clock, intended for use with very old
* php versions which don't support classes. PHP 4+ users should use the class-based TimeChannels.php source.
*
* Please note the following global vars - do not overwrite these. If there is a conflict with other global vars in
* your application, these can be renamed, but of course make sure all references in this file match.
*
* Everywhere in your application codebase that obtains the date/time directly, replace with a call
* to the getSystemDate() function in this class.
*
* For simple unit-testing, you can call setToStaticTestState() and pass it a specific date-time value.
*
* For system, integration, acpt testing, call setToTestState() and pass it a specific channel id you have setup in
* the TimeChannels.com control panel.
*
* Return to normal time by calling setToProductionState()
*/
$tcURL = "http://www.timechannels.com/GetChannel";
$tcURLms = "http://www.timechannels.com/GetChannelMS";
$tcAscState = "TimeChannels.state.Production";
$tcChannel = "0";
$tcAscDateTime = null;
$tcServiceURL = $tcURL;
$tcMillisecondMode = "false";
function setToProductionState() {
global $tcAscState;
$tcAscState = "TimeChannels.state.Production";
}
function setToStaticTestState($aDateTimeValue) {
global $tcAscState, $tcAscDateTime;
$tcAscState = "TimeChannels.state.TestDate";
$tcAscDateTime = $aDateTimeValue;
}
function setToTestState($aChannel) {
global $tcAscState, $tcChannel;
$tcAscState = "TimeChannels.state.TestChannel";
$tcChannel = $aChannel;
}
function setToMillisecondMode() {
global $tcMillisecondMode, $tcServiceURL, $tcURLms;
$tcMillisecondMode = "true";
$tcServiceURL = $tcURLms;
}
function setToStandardMode() {
global $tcMillisecondMode, $tcServiceURL, $tcURL;
$tcMillisecondMode = "false";
$tcServiceURL = $tcURL;
}
function getSystemDate() {
global $tcAscState, $tcAscDateTime, $tcChannel;
if ($tcAscState == "TimeChannels.state.Production") return mktime();
if ($tcAscState == "TimeChannels.state.TestDate") return $tcAscDateTime;
if ($tcAscState == "TimeChannels.state.TestChannel") return getChannelFromTimeChannelsService($tcChannel);
return mktime();
}
function getHTTPContent( $url ) {
$options = array( 'http' => array(
'user_agent' => 'spider', // who am i
'max_redirects' => 10, // stop after 10 redirects
'timeout' => 120, // timeout on response
) );
$context = stream_context_create($options);
$page = @file_get_contents($url, false, $context);
$result = array();
if ($page != false)
$result['content'] = $page;
else if (!isset($http_response_header))
return null; // Bad url, timeout
$result['header'] = $http_response_header;
$nLines = count($http_response_header);
for ($i = $nLines-1; $i >= 0; $i--) {
$line = $http_response_header[$i];
if (strncasecmp("HTTP", $line, 4) == 0) {
$response = explode(' ', $line);
$result['http_code'] = $response[1];
break;
}
}
return $result;
}
function getChannelFromTimeChannelsService($aChannel) {
global $tcServiceURL, $tcMillisecondMode;
$url = $tcServiceURL . "?channel=" . $aChannel;
$result = getHTTPContent($url);
if ($result == null) return mktime();
if ($result['http_code'] != 200) return mktime();
$channelValue = $result['content'];
if ($tcMillisecondMode == "true") {
$retVal = (1 * $channelValue) / 1000;
}
else {
$year = 1 * substr($channelValue, 0, 4);
//echo "year: $year <br>";
$month = 1 * substr($channelValue, 5, 2);
//echo "month: $month <br>";
$day = 1* substr($channelValue, 8, 2);
//echo "day: $day <br>";
$hour = 1 * substr($channelValue, 11, 2);
//echo "hour: $hour <br>";
$minute = 1 * substr($channelValue, 14, 2);
//echo "minute: $minute <br>";
$ampm = substr($channelValue, 17, 2);
//echo "ampm: $ampm <br>";
if ($ampm == "PM") $hour += 12;
$retVal = mktime($hour, $minute, 0, $month, $day, $year);
}
return $retVal;
}
?>
And here's the sample usage/testing code for the "Legacy" version:
Code: Select all
<?php
include 'timechannelsLegacy.php';
$tab = " ";
echo "======================================================================<br>";
echo $tab . "<TimeChannelsClient><br>";
/////////// --- Don't do anything yet; test that TimeChannels class defaults to Prod. Mode:
echo $tab . $tab . "---> default result: " . date("Y-m-d h:i A", getSystemDate());
echo "<br>";
/////////// --- Now set to Test Mode with a static date:
$staticDate = mktime(9, 15, 0, 3, 13, 1969);
setToStaticTestState($staticDate);
echo $tab . $tab . "---> static test state result: " . date("Y-m-d h:i A", getSystemDate());
echo "<br>";
/////////// --- Now set to Test Mode with a channel:
setToTestState("2");
echo $tab . $tab . "---> channel 2 test state result: " . date("Y-m-d h:i A", getSystemDate());
echo "<br>";
/////////// --- Now set to Millisecond Mode:
// Please note: millisecond timestamps are not supported directly in php, so this call will only be precise
// to the second.
setToMillisecondMode();
echo $tab . $tab . "---> channel 2 test state result from milliseconds: " . date("Y-m-d h:i:s A", getSystemDate());
echo "<br>";
echo $tab . "</TimeChannelsClient><br>";
echo "======================================================================<br>";
?>
I hope you will all consider using this when developing projects (PHP or otherwise). Converting your date-time calls to instead use the timechannels class method ( getSystemDate() ) is really easy, and there's not much risk to it either, as the default state is to Production mode, so it's only going to reflect the regular (real) date-time anyway, until you turn the ASC on.
When I first implemented this ASC concept, it was on a large project that was about a year-and-a-half and three releases into development, with over a million lines of Java code. Still, it only took myself and another developer about two days to go through the entire codebase with our initial pass and replace date-time calls. We of course missed several spots, but these were of course caught in various levels of routine testing.
At first blush, I'd say that this should be much easier in php, because it looks like in php there's only one way to deal with date-times, yes? It's harder in Java because Java provides multiple, convoluted ways of dealing with date-times, and these often conflict with each other, so each occurrence must be carefully studied to ensure that the ASC is going to return a value that is compatible with the receiving code.
Anyway, hope this helps, and I would love to hear your feedback/suggestions.
Cheers,
John