952 days continuous production uptime, 40k+ tp/s single node. Original corpo Bitbucket history not included — clean archive commit.
580 lines
22 KiB
PHP
580 lines
22 KiB
PHP
<?php
|
|
/**
|
|
* gasConfig -- static configuration class
|
|
*
|
|
* The data framework is intended to read two configuration files of varying types to create a single structure
|
|
* containing all of the configuration options.
|
|
*
|
|
* subsequent requests/instantiations to different configuration files *overwrite* the duplicated-tag sections.
|
|
*
|
|
* this approach allows us to load a core configuration, then to modify the structure generated with a second
|
|
* configuration file.
|
|
*
|
|
* class is designed as a singleton so that only one "authoritative" config file can exist.
|
|
*
|
|
* To invoke, call the singleton function with the path/filename and the file type:
|
|
*
|
|
* $foo = gasConfig::singleton('./config/base.xml', 'xml');
|
|
*
|
|
* all configuration file names, and their file types, are defined in the global constants file.
|
|
*
|
|
* all key-value-paired data is loaded, stored, and accessed in the classes' $settings member - given a key (tag,
|
|
* index, etc.), you can access/pull the data using the following:
|
|
*
|
|
* $foo = gasConfig::$settings[$key];
|
|
*
|
|
* which will either return a value or a sub-array depending on what $key indexes.
|
|
*
|
|
* KNOWN LIMITATIONS:
|
|
* ------------------
|
|
* Config files have to be supported by the corresponding PHP function that parses that config file type. As of this
|
|
* writing, only the following file types are supported:
|
|
*
|
|
* -- xml
|
|
* -- ini
|
|
* -- json
|
|
*
|
|
*
|
|
* @author mike@givingassistant.org
|
|
* @version 1.0
|
|
*
|
|
* HISTORY:
|
|
* ========
|
|
* 06-07-17 mks original coding
|
|
* 06-08-18 mks CORE-1035: console logging upgrade
|
|
* register $setting member function added
|
|
* 06-15-18 mks CORE-1045: deprecated CONFIG_ID_NODE XML tag
|
|
* 09-24-19 mks DB-136: deprecated getSyslog() method
|
|
* 01-08-20 mks DB-150: PHP7.4 class member type-casting
|
|
*
|
|
*/
|
|
class gasConfig
|
|
{
|
|
private static ?gasConfig $instance = null; // self-pointer
|
|
public static ?string $status = null; // used to validate successful instantiation
|
|
|
|
// private static $env = [ // defines the valid environments
|
|
// CONFIG_ID_NODE_NAMASTE,
|
|
// CONFIG_ID_NODE_ADMIN,
|
|
// CONFIG_ID_NODE_DEV
|
|
// ];
|
|
|
|
public static ?array $settings = null; // hold all the configs stuffs
|
|
|
|
CONST AUTO = 0; // config file format-type associations
|
|
CONST JSON = 2;
|
|
const PHP_INI = 4;
|
|
const XML = 16;
|
|
|
|
static private string $res = 'CNFG: '; // logger id tag
|
|
static private array $CONF_EXT_RELATION = ['json' => 2, 'ini' => 4, 'xml' => 16];
|
|
|
|
/**
|
|
* __construct() -- private method
|
|
*
|
|
* constructor function for the class - determines the type of configuration file to read (if none is provided,
|
|
* then attempt to determine by the file's extension).
|
|
*
|
|
* depending on a file extension, invoke the appropriate function to parse the config file in an internal
|
|
* data structure.
|
|
*
|
|
*
|
|
* @author mshallop@pathway.com
|
|
* @version 2.1.7
|
|
*
|
|
* @param $_cFile
|
|
* @param string $_fType
|
|
*
|
|
*
|
|
* HISTORY:
|
|
* ========
|
|
* 06-07-17 mks original coding
|
|
*
|
|
*/
|
|
private function __construct(string $_cFile, string $_fType = gasConfig::AUTO)
|
|
{
|
|
//register_shutdown_function('pgsConfig::__destruct');
|
|
self::getConfig($_cFile, $_fType);
|
|
if (gasConfig::$settings[CONFIG_DEBUG]) consoleLog(static::$res, CON_DEBUG, INFO_CONFIG_LOADED);
|
|
}
|
|
|
|
|
|
/**
|
|
* registerEnvironment() -- private static method
|
|
*
|
|
* This method has no input parameters and returns a boolean to indicate successful processing.
|
|
*
|
|
* The method requires that the XML configuration be pre-loaded prior to invocation.
|
|
*
|
|
* Method creates an array of services and assigns a boolean value to each service (associative array)
|
|
* which is then transferred to a member variable.
|
|
*
|
|
* Note that "available services" are relative to the local service and not indicative of overall service
|
|
* ability for a distributed cluster.
|
|
*
|
|
*
|
|
*
|
|
* @author mike@givingassistant.org
|
|
* @version 1.0
|
|
*
|
|
* @return bool
|
|
*
|
|
*
|
|
* HISTORY:
|
|
* ========
|
|
* 06-08-18 mks CORE-1035: original coding (transferred from startBrokers.php)
|
|
* 09-08-20 mks DB-168: updated for XML service locality re-configuration
|
|
* 11-09-20 mks DB-171: update check for local service registration to also be qualified on ACTIVE setting
|
|
*
|
|
*/
|
|
private static function registerEnvironment(): bool
|
|
{
|
|
$environments = [];
|
|
// using the environment in the XML config -- generate a list of currently-active services.
|
|
foreach (gasConfig::$settings[CONFIG_REGISTERED_SERVICES][CONFIG_DATABASE_MONGODB_ADMIN_REPLSET_SET] as $service) {
|
|
if (isset(gasConfig::$settings[CONFIG_BROKER_SERVICES][$service]) and is_array(gasConfig::$settings[CONFIG_BROKER_SERVICES][$service])) {
|
|
if (isset(gasConfig::$settings[$service][CONFIG_IS_LOCAL])) {
|
|
if (isset(gasConfig::$settings[$service][CONFIG_ACTIVE])) {
|
|
$environments[$service] = boolval(gasConfig::$settings[$service][CONFIG_IS_LOCAL]) && boolval(gasConfig::$settings[$service][CONFIG_ACTIVE]);
|
|
} else {
|
|
$hdr = sprintf(INFO_LOC, basename(__METHOD__), __LINE__);
|
|
$msg = $hdr . sprintf(CONFIG_XML_SERVICE_SETTING, CONFIG_BROKER_SERVICES . ARROW . $service . ARROW . CONFIG_IS_LOCAL);
|
|
consoleLog(static::$res, CON_ERROR, $msg);
|
|
return false;
|
|
}
|
|
} else {
|
|
$hdr = sprintf(INFO_LOC, basename(__METHOD__), __LINE__);
|
|
$msg = $hdr . sprintf(CONFIG_XML_SERVICE_SETTING, CONFIG_BROKER_SERVICES . ARROW . $service . ARROW . CONFIG_IS_LOCAL);
|
|
consoleLog(static::$res, CON_ERROR, $msg);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (gasConfig::$settings[CONFIG_ID][CONFIG_ID_ENV] == ENV_PRODUCTION) {
|
|
// if we're starting in a production environment -- this requires services (appServer, Admin, Segundo and Tercero)
|
|
// to all be started on separate instances. This block enforces that all services are discrete per instance.
|
|
if (array_sum($environments) > 1) {
|
|
$msg = CONFIG_XML_SERVICE_VIOLATION;
|
|
foreach ($environments as $key => $value) {
|
|
if ($value == 1) $msg .= $key . ', ';
|
|
}
|
|
$msg = rtrim($msg, ', ');
|
|
consoleLog(static::$res, CON_ERROR, $msg);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// save the local services to the gasConfig object
|
|
static::$settings[CONFIG_REGISTERED_SERVICES] = $environments;
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* getConfig() -- private static method
|
|
*
|
|
* reads the configuration file (passed in by $_cFile) from the DIR_CONFIG directory and merges the files into
|
|
* (or onto) the existing configuration structure ($settings) on subsequent calls to this method.
|
|
*
|
|
* supported file types:
|
|
* -- json
|
|
* -- php.ini
|
|
* -- xml
|
|
*
|
|
* if the files cannot be accessed, or if an invalid file type is given, then dump the error to stdout (logfile)
|
|
* and exit.
|
|
*
|
|
*
|
|
* @author mike@givingassistant.org
|
|
* @version 1.0
|
|
*
|
|
* @param $_cFile -- config file path and name
|
|
* @param string $_fType -- config file type
|
|
*
|
|
*
|
|
* HISTORY:
|
|
* ========
|
|
* 06-07-17 mks original coding
|
|
*
|
|
*/
|
|
private static function getConfig(string $_cFile, string $_fType = gasConfig::AUTO)
|
|
{
|
|
$tSettings = null; // temporary holder for file settings
|
|
|
|
if ($_fType == self::AUTO) {
|
|
$_fType = self::$CONF_EXT_RELATION[pathinfo($_cFile, PATHINFO_EXTENSION)];
|
|
}
|
|
switch ($_fType) {
|
|
case self::JSON :
|
|
$result = file_get_contents($_cFile, true);
|
|
if (!$result) {
|
|
consoleLog(static::$res, CON_ERROR, CONFIG_FTL_JSON . $_cFile);
|
|
return;
|
|
} else {
|
|
$tSettings = json_decode($result);
|
|
}
|
|
break;
|
|
|
|
case self::PHP_INI :
|
|
$result = parse_ini_file($_cFile, true);
|
|
if (!$result) {
|
|
consoleLog(static::$res, CON_ERROR, CONFIG_FTL_INI . $_cFile);
|
|
return;
|
|
} else {
|
|
$tSettings = $result;
|
|
}
|
|
break;
|
|
|
|
case self::XML :
|
|
$result = simplexml_load_file($_cFile);
|
|
if (!$result) {
|
|
consoleLog(static::$res, CON_ERROR, CONFIG_FTL_XML . $_cFile);
|
|
return;
|
|
} else {
|
|
try {
|
|
$tSettings = self::objectToArray($result);
|
|
} catch (TypeError $t) {
|
|
consoleLog(static::$res, CON_ERROR, ERROR_TYPE_EXCEPTION . COLON . $t->getMessage());
|
|
return;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
if (!is_null($tSettings)) {
|
|
if (is_null(self::$settings)) {
|
|
self::$settings = $tSettings;
|
|
} else {
|
|
self::$settings = self::recursiveArrayMerge(self::$settings, $tSettings, true);
|
|
}
|
|
try {
|
|
self::recursiveArrayPurge(self::$settings);
|
|
} catch (TypeError $t) {
|
|
consoleLog(static::$res, CON_ERROR, ERROR_TYPE_EXCEPTION . COLON . $t->getMessage());
|
|
return;
|
|
}
|
|
}
|
|
// // validate the environment
|
|
// if (!in_array(self::$settings[CONFIG_ID][CONFIG_ID_NODE], self::$env)) {
|
|
// consoleLog(static::$res, CON_ERROR, CONFIG_UNK_ENV . self::$settings[CONFIG_ID][CONFIG_ID_NODE]);
|
|
// exit(1);
|
|
// }
|
|
}
|
|
|
|
|
|
/**
|
|
* recursiveArrayMerge() -- private static method
|
|
*
|
|
* this takes the original configuration array and merges subsequent configuration files on top of it.
|
|
*
|
|
* for example, if you have a structure:
|
|
*
|
|
* $this[database][mysql] section defined:
|
|
*
|
|
* <database>
|
|
* <mysql>
|
|
* <db_hostname>localhost</db_hostname>
|
|
* <db_username>user_name</db_username>
|
|
* <db_password>user_pass</db_password>
|
|
* <db_port>3306</db_port>
|
|
* <db_database>some_database_name</db_database>
|
|
* </mysql>
|
|
*
|
|
* and you want to change the database name for your local environment, then the
|
|
* subsequent configuration file would bear an identical parent structure, and an
|
|
* identical element naming structure....changed data within the elements would be
|
|
* copied over the existing parent structure:
|
|
*
|
|
* <database>
|
|
* <mysql>
|
|
* <db_database>some_database_name</db_database>
|
|
* </mysql>
|
|
*
|
|
* with the resulting output:
|
|
* [database] => Array
|
|
* (
|
|
* [mysql] => Array
|
|
* (
|
|
* [db_hostname] => localhost
|
|
* [db_username] => user
|
|
* [db_password] => password
|
|
* [db_port] => 3306
|
|
* [db_database] => some_database_name
|
|
* )
|
|
* )
|
|
*
|
|
* @author mike@givingassistant.org
|
|
* @version 1.0
|
|
*
|
|
* @param $array1
|
|
* @param $array2
|
|
* @param bool $overwrite
|
|
* @return array
|
|
*
|
|
* HISTORY:
|
|
* ========
|
|
* 06-07-17 mks original coding
|
|
* 06-20-18 mks CORE-1045: trapped PHP fatal error -- if there's an error in the XML where you have
|
|
* re-declared a variable (to a different value), the framework IPL will crash.
|
|
* This fix will prevent the crash from happening and output a console message.
|
|
*
|
|
*/
|
|
public static function recursiveArrayMerge(array $array1, array $array2, bool $overwrite = true)
|
|
{
|
|
foreach ($array2 as $key => $val) {
|
|
if (isset($array1[$key])) {
|
|
if (is_array($val)) {
|
|
try {
|
|
$array1[$key] = self::recursiveArrayMerge($array1[$key], $val);
|
|
} catch (TypeError $t) {
|
|
consoleLog(static::$res, CON_SYSTEM, CONFIG_XML_DUP_VAR . $key);
|
|
consoleLog(static::$res, CON_SYSTEM, $t->getMessage());
|
|
}
|
|
} elseif ((is_string($array1[$key]) or is_int($array1[$key])) && $overwrite) {
|
|
$array1[$key] = $val;
|
|
}
|
|
} else {
|
|
$array1[$key] = $val;
|
|
}
|
|
}
|
|
return $array1;
|
|
}
|
|
|
|
|
|
/**
|
|
* recursiveArrayPurge() -- local private method
|
|
*
|
|
* So, as it turns out, simplexml_load_file() does not ignore comments embedded into the XML... mostly... in truth,
|
|
* placeholder indices are created within the structure that are also arrays, albeit empty.
|
|
*
|
|
* So this function, which is called by the getConfig method, recursively loops through the existing $settings
|
|
* structure and removes any array with a key of "comment". This, of course, means that one cannot include this
|
|
* particular value as an index key within the xml file.
|
|
*
|
|
* The input parameter to the method is a call-by-reference value which allows us to traverse the settings
|
|
* structure recursively and retain changes to the overall array.
|
|
*
|
|
*
|
|
* @author mike@givingassistant.org
|
|
* @version 1.0
|
|
*
|
|
* @param array $ary -- initially, should be the static::$settings array, thereafter, sub-arrays from within
|
|
*
|
|
*
|
|
* HISTORY:
|
|
* ========
|
|
* 06-13-17 mks original coding
|
|
* 11-28-17 mks CORE-635: fixed bug in conditional that was causing 0th elements of XML sub-arrays
|
|
* to be discarded
|
|
*
|
|
*/
|
|
private static function recursiveArrayPurge(array &$ary)
|
|
{
|
|
foreach ($ary as $key => &$value) {
|
|
if ($key === STRING_COMMENT) {
|
|
unset($ary[$key]);
|
|
} elseif (is_array($value)) {
|
|
try {
|
|
self::recursiveArrayPurge($value);
|
|
} catch (TypeError $t) {
|
|
consoleLog(static::$res, CON_ERROR, $t->getMessage());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* objectToArray() - private static method
|
|
*
|
|
* recursive function to flatten objects to a single array.
|
|
* If the object contains embedded objects, then self-invoke.
|
|
*
|
|
* @author mike@givingassistant.org
|
|
* @version 1.0
|
|
*
|
|
* @param $_obj - a collection of either objects or arrays
|
|
* @return array - the flattened object
|
|
*
|
|
* HISTORY:
|
|
* ========
|
|
* 06-07-17 mks original coding
|
|
* 07-21-17 mks CORE-468: if the XML value is numeric (float or int) then return a float or int instead
|
|
* of a string. Do this by implicitly casting the value by adding 0 to the value.
|
|
*
|
|
*/
|
|
private static function objectToArray($_obj)
|
|
{
|
|
$ph = null; // placeholder
|
|
$ph = (is_object($_obj)) ? get_object_vars($_obj) : $_obj;
|
|
foreach ($ph as $key => $val) {
|
|
// todo - evaluate to see if is_array is ever true - if not, use strong typing (SimpleXMLElement)
|
|
$ph[$key] = ((is_array($val)) or (is_object($val))) ? self::objectToArray($val) : (is_numeric($val) ? $val + 0 : $val);
|
|
}
|
|
return ($ph);
|
|
}
|
|
|
|
|
|
/**
|
|
* __get() -- public method
|
|
*
|
|
* This is a magic function for accessing private data within the config class object.
|
|
* Note that magic functions are required to be public and not static.
|
|
*
|
|
* input parameter ($_section) is the string (tag) referencing the (sub)section of the configuration file
|
|
* to be returned. If $_section does not exist as an array key, or if self::$settings has yet to be set,
|
|
* then return Boolean(false) -- otherwise return the sub-scripted array as referenced by $_section.
|
|
*
|
|
* LIMITATIONS:
|
|
* ------------
|
|
* Sub-arrays cannot be deeper than one level.
|
|
*
|
|
* @author mike@givingassistant.org
|
|
* @version 1.0
|
|
*
|
|
* @param $_section
|
|
* @return bool
|
|
*
|
|
* HISTORY:
|
|
* ========
|
|
* 06-07-17 mks original coding
|
|
*
|
|
*/
|
|
public function __get($_section)
|
|
{
|
|
return (is_array(self::$settings) and (array_key_exists($_section, self::$settings)) ? self::$settings[$_section] : false);
|
|
}
|
|
|
|
|
|
/**
|
|
* getPedigree() -- public static function
|
|
*
|
|
* This function has no input parameters and returns an associative array.
|
|
*
|
|
* The function pulls the current configuration and returns selected parameters to the calling client. These values
|
|
* are going to be string values, except for the current version which is cast to float, and will indicate if a
|
|
* feature is enabled, disabled or, if there's a configuration error, set to an error message.
|
|
*
|
|
* It's the responsibility of the calling program to parse the return array and to compare/contrast the selected
|
|
* values.
|
|
*
|
|
*
|
|
* @author mike@givingassistant.org
|
|
* @version 1.0
|
|
*
|
|
* @return array
|
|
*
|
|
*
|
|
* HISTORY:
|
|
* ========
|
|
* 07-09-18 mks CORE-1017: original coding
|
|
*
|
|
*/
|
|
public static function getPedigree(): array
|
|
{
|
|
$retData[PEDIGREE_ENV] = isset(gasConfig::$settings[CONFIG_ID][CONFIG_ID_ENV]) ? gasConfig::$settings[CONFIG_ID][CONFIG_ID_ENV] : ERROR_STUB_NOTDEF;
|
|
$retData[PEDIGREE_VER] = isset(gasConfig::$settings[CONFIG_ID][CONFIG_ID_VER]) ? floatval(gasConfig::$settings[CONFIG_ID][CONFIG_ID_VER]) : ERROR_STUB_NOTDEF;
|
|
$retData[PEDIGREE_DEBUG] = isset(gasConfig::$settings[CONFIG_DEBUG]) ? ((intval(gasConfig::$settings[CONFIG_DEBUG]) == 1) ? STRING_ENABLED : STRING_DISABLED) : ERROR_STUB_NOTDEF;
|
|
$retData[PEDIGREE_SYSLOG] = STRING_ENABLED;
|
|
$retData[PEDIGREE_AUDIT] = isset(gasConfig::$settings[CONFIG_AUDIT_ON]) ? ((intval(gasConfig::$settings[CONFIG_AUDIT_ON]) == 1) ? STRING_ENABLED : STRING_DISABLED) : ERROR_STUB_NOTDEF;
|
|
$retData[PEDIGREE_JOURNAL] = isset(gasConfig::$settings[CONFIG_JOURNAL_ON]) ? ((intval(gasConfig::$settings[CONFIG_JOURNAL_ON]) == 1) ? STRING_ENABLED : STRING_DISABLED) : ERROR_STUB_NOTDEF;
|
|
$retData[PEDIGREE_SEGUNDO] = isset(gasConfig::$settings[CONFIG_BROKER_SEGUNDO]) ? ((intval(gasConfig::$settings[CONFIG_BROKER_SEGUNDO]) == 1) ? STRING_ENABLED : STRING_DISABLED) : ERROR_STUB_NOTDEF;
|
|
$retData[PEDIGREE_TERCERO] = isset(gasConfig::$settings[CONFIG_BROKER_TERCERO]) ? ((intval(gasConfig::$settings[CONFIG_BROKER_TERCERO]) == 1) ? STRING_ENABLED : STRING_DISABLED) : ERROR_STUB_NOTDEF;
|
|
$retData[PEDIGREE_QTAG] = isset(gasConfig::$settings[CONFIG_BROKER_SERVICES][CONFIG_BROKER_QUEUE_TAG]) ? gasConfig::$settings[CONFIG_BROKER_SERVICES][CONFIG_BROKER_QUEUE_TAG] : ERROR_STUB_NOTDEF;
|
|
$retData[PEDIGREE_VHOST] = isset(gasConfig::$settings[CONFIG_BROKER_SERVICES][CONFIG_BROKER_VHOST]) ? gasConfig::$settings[CONFIG_BROKER_SERVICES][CONFIG_BROKER_VHOST] : ERROR_STUB_NOTDEF;
|
|
return $retData;
|
|
}
|
|
|
|
|
|
/**
|
|
* singleton() -- public static function
|
|
*
|
|
* the problem with php is that it does not support true static classes. If you add a debug output to this method
|
|
* when it's entered, you'll see it proc every time this method is called.
|
|
*
|
|
* The input parameters are the name of the configuration file and the configuration file type - which defaults
|
|
* to the "auto" config type.
|
|
*
|
|
* The output is the array structure as defined by the config file itself since it's (more or less) read-in
|
|
* as the input.
|
|
*
|
|
* @author mike@givingassistant.org
|
|
* @version 1.0
|
|
*
|
|
* @param $_iniFile -- path/filename.ext to the config file
|
|
* @param string $_iniType -- extension type of the config file
|
|
* @return gasConfig -- returns an array or false
|
|
*
|
|
*
|
|
* HISTORY:
|
|
* ========
|
|
* 06-07-17 mks original coding
|
|
*
|
|
*/
|
|
public static function singleton(string $_iniFile, string $_iniType = gasConfig::AUTO): gasConfig
|
|
{
|
|
if (static::$instance === null) {
|
|
$c = __CLASS__;
|
|
static::$instance = new $c($_iniFile, $_iniType);
|
|
}
|
|
return static::$instance;
|
|
}
|
|
|
|
|
|
/**
|
|
* addConfig() -- public static method
|
|
*
|
|
* subsequent calls to read-in additional configuration are handled by this method which invokes the private
|
|
* method already used to load the 0th-case.
|
|
*
|
|
* @author mike@givingassistant.org
|
|
* @version 1.0
|
|
*
|
|
* @param $_file
|
|
* @param string $_type
|
|
*
|
|
* HISTORY:
|
|
* ========
|
|
* 06-07-17 mks original coding
|
|
* 06-08-18 mks CORE-1035: adding service environments to $settings
|
|
*
|
|
*/
|
|
public static function addConfig(string $_file, string $_type = gasConfig::AUTO)
|
|
{
|
|
self::getConfig($_file, $_type);
|
|
|
|
// now that both env files are loaded, register the service environments:
|
|
static::$status = false;
|
|
try {
|
|
if (!self::registerEnvironment()) {
|
|
consoleLog(static::$res, CON_ERROR, ERROR_CONFIG_RESOURCE_404 . STRING_SVC_ENV);
|
|
} else {
|
|
static::$status = true;
|
|
}
|
|
} catch (TypeError $t) {
|
|
consoleLog(static::$res, CON_ERROR, ERROR_TYPE_EXCEPTION . COLON . $t->getMessage());
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* __clone() -- public static method
|
|
*
|
|
* disallow cloning by return an explicit null on the request
|
|
*
|
|
* @author mshallop@pathway.com
|
|
*
|
|
* @return null
|
|
*
|
|
* HISTORY:
|
|
* ========
|
|
* 06-07-17 mks original coding
|
|
*
|
|
*/
|
|
public function __clone()
|
|
{
|
|
return(null); // disallow cloning of this class
|
|
}
|
|
|
|
}
|