diff --git a/resources/classes/command_option.php b/resources/classes/command_option.php new file mode 100644 index 0000000000..622e3b8974 --- /dev/null +++ b/resources/classes/command_option.php @@ -0,0 +1,241 @@ + + * Portions created by the Initial Developer are Copyright (C) 2008-2024 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Mark J Crane + * Tim Fry + */ + +/** + * Container object for creating command line options when creating a service + * @author Tim Fry + */ +class command_option { + + private $short_option; + private $long_option; + private $description; + private $short_description; + private $long_description; + private $functions; + + /** + * Constructs an empty command_option + */ + public function __construct() { + $this->short_option = ''; + $this->long_option = ''; + $this->description = ''; + $this->short_description = ''; + $this->long_description = ''; + $this->functions = []; + } + + /** + * A factory method to create a new command_option + * @param type $options + * @return command_option + */ + public static function new(...$options): command_option { + $obj = new command_option(); + + //automatically assign properties to the object that were passed in key/value pairs + self::parse_options($obj, $options); + + //return the command_option with all properties filled in that were passed + return $obj; + } + + // used to parse object values when created + private static function parse_options($obj, $options) { + foreach ($options as $key => $value) { + if (is_array($value)) { + self::parse_options($obj, $value); + } + //call the method with the name of $key and pass it $value + if (method_exists($obj, $key)) { + $obj->{$key}($value); + } elseif (property_exists($obj, $key)) { + $obj->{$key} = $value; + } + } + } + + /** + * Sets or returns the short option value + * @param string|null $short_option + * @return $this + */ + public function short_option(?string $short_option = null) { + if (!empty($short_option)) { + $this->short_option = $short_option; + return $this; + } + return $this->short_option; + } + + /** + * Sets or returns the long option value + * @param string|null $long_option + * @return $this + */ + public function long_option(?string $long_option = null) { + if (!empty($long_option)) { + $this->long_option = $long_option; + return $this; + } + return $this->long_option; + } + + /** + * Set the general description + * @param string|null $description + * @return $this + */ + public function description(?string $description = null) { + if (!empty($description)) { + $this->description = $description; + return $this; + } + return $this->description; + } + + /** + * Sets or returns the short_description. If short_description is empty then the short_option is used as a default. + * @param string|null $short_description When parameter is null, it returns the currently set value. When not null the short description is set to the passed value. + * @return $this + */ + public function short_description(?string $short_description = null) { + if (!empty($short_description)) { + $this->short_description = $short_description; + return $this; + } + if (empty($this->short_description)) { + if (str_ends_with($this->short_option, ':')) { + $short = rtrim($this->short_option, ':'); + $short_description = "-$short "; + } else { + $short_description = '-' . $this->short_option; + } + } else { + $short_description = $this->short_description; + } + return $short_description; + } + + /** + * Sets or returns the long_description. If long_description is empty then the long_option is used as a default. + * @param string|null $long_description When parameter is null, it returns the currently set value. When not null the long description is set to the passed value. + * @return $this + */ + public function long_description(?string $long_description = null) { + if ($long_description !== null) { + $this->long_description = $long_description; + return $this; + } + if (empty($this->long_description)) { + if (str_ends_with($this->long_option, ':')) { + $long = rtrim($this->long_option, ':'); + $long_description = "--$long "; + } else { + $long_description = '--' . $this->long_option; + } + } else { + $long_description = $this->long_description; + } + return $long_description; + } + + /** + * Adds an array of callback functions replacing the existing callback functions + * @param array|null $functions + * @return $this + */ + public function functions(?array $functions = null) { + if ($functions !== null) { + $this->functions = $functions; + return $this; + } + return $this->functions; + } + + /** + * Appends the callback function to the array of existing callback functions + * @param string|null $function When function param is set, the callback function will be appended to the list of functions. When called without a param, the array will be returned of current callbacks. + * @return $this|array Returns the array of callbacks if no parameters passed or this object when appending a callback + */ + public function callback(?string $function = null) { + if ($function !== null) { + $this->functions += [$function]; + return $this; + } + return $this->functions; + } + + /** + * Appends the callback function to the array of existing callback functions + * @param string|null $function + * @return $this + */ + public function function_append(?string $function = null) { + if ($function !== null) { + $this->functions += [$function]; + return $this; + } + return $this->functions; + } + + /** + * Returns the array structure required for service + * @return array + */ + public function to_array(): array { + $array['short_option'] = $this->short_option(); + $array['long_option'] = $this->long_option(); + $array['description'] = $this->description(); + $array['short_description'] = $this->short_description(); + $array['long_description'] = $this->long_description(); + $array['functions'] = $this->functions(); + return $array; + } +} + +/* Examples +$command_option = command_option::new([ + 'short_option'=>'m', + 'long_option' =>'my-option', + 'description' =>'Create an option that uses -m or --my-option command-line parameter' +]); + +$command_option = command_option::new() + ->short_option('m') + ->long_option('my-option') + ->description('Create an option that uses -m or --my-option command-line parameter'); + +echo $command_option->description(); + +$command_parsing_array = $command_option->to_array(); +print_r($command_parsing_array); + + + //*/ diff --git a/resources/classes/service.php b/resources/classes/service.php new file mode 100644 index 0000000000..14843a082b --- /dev/null +++ b/resources/classes/service.php @@ -0,0 +1,1042 @@ + + * Portions created by the Initial Developer are Copyright (C) 2008-2024 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Mark J Crane + * Tim Fry + */ + +/** + * Service class + * @version 1.00 + * @author Tim Fry + */ +abstract class service { + + const VERSION = "1.00"; + + /** + * Track the internal loop. It is recommended to use this variable to control the loop inside the run function. See the example + * below the class for a more complete explanation + * @var bool + */ + protected $running; + + /** + * current debugging level for output to syslog + * @var int Syslog level + */ + protected static $log_level = LOG_INFO; + + /** + * config object + * @var config config object + */ + protected static $config; + + /** + * Holds the parsed options from the command line + * @var array + */ + protected static $parsed_command_options; + + /** + * Operating System process identification file + * @var string + */ + private static $pid_file = ""; + + /** + * Cli Options Array + * @var array + */ + protected static $available_command_options = []; + + /** + * Holds the configuration file location + * @var string + */ + protected static $config_file = ""; + + /** + * Child classes must provide a mechanism to reload settings + */ + abstract protected function reload_settings(): void; + + /** + * Method to start the child class internal loop + */ + abstract public function run(): int; + + /** + * Display version notice + */ + abstract protected static function display_version(): void; + + /** + * Called when the display_help_message is run in the base class for extra command line parameter explanation + */ + abstract protected static function set_command_options(); + + /** + * Open a log when created. + *

NOTE:
+ * This is a protected function so it can not be called using the keyword 'new' outside of this class or a child + * class. This is due to the requirement to set signal handlers for the POSIX system outside of the constructor. + * PHP seems to have an issue on some versions where setting a signal handler while in the constructor (even + * calling another method from the constructor) will fail to register the signal handlers.

+ */ + protected function __construct() { + openlog('[php][' . self::class . ']', LOG_CONS | LOG_NDELAY | LOG_PID, LOG_DAEMON); + } + + public function __destruct() { + //ensure we unlink the correct PID file if needed + if (self::is_running()) { + unlink(self::$pid_file); + self::log("Initiating Shutdown...", LOG_NOTICE); + $this->running = false; + } + //this should remain the last statement to execute before exit + closelog(); + } + + /** + * Shutdown process gracefully + */ + public static function shutdown() { + exit(); + } + + public static function send_shutdown() { + if (self::is_any_running()) { + self::send_signal(SIGTERM); + } else { + die("Service Not Started\n"); + } + } + + // register signal handlers + private function register_signal_handlers() { + // Allow the calls to be made while the main loop is running + pcntl_async_signals(true); + + // A signal listener to reload the service for any config changes in the database + pcntl_signal(SIGUSR1, [$this, 'reload_settings']); + pcntl_signal(SIGHUP, [$this, 'reload_settings']); + + // A signal listener to stop the service + pcntl_signal(SIGUSR2, [self::class, 'shutdown']); + pcntl_signal(SIGTERM, [self::class, 'shutdown']); + } + + /** + * Extracts the short options from the cli options array and returns a string. The resulting string must + * return a single string with all options in the string such as 'rxc:'. + * This can be overridden by the child class. + * @return string + */ + protected static function get_short_options(): string { + return implode('' , array_map(function ($option) { return $option['short_option']; }, self::$available_command_options)); + } + + /** + * Extracts the long options from the cli options array and returns an array. The resulting array must + * return a single dimension array with an integer indexed key but does not have to be sequential order. + * This can be overridden by the child class. + * @return array + */ + protected static function get_long_options(): array { + return array_map(function ($option) { return $option['long_option']; }, self::$available_command_options); + } + + /** + * Method that will retrieve the callbacks from the cli options array + * @param string $set_option + * @return array + */ + protected static function get_user_callbacks_from_available_options(string $set_option): array { + //match the available option to the set option and return the callback function that needs to be called + foreach(self::$available_command_options as $option) { + $short_option = $option['short_option'] ?? ''; + if (str_ends_with($short_option, ':')) { + $short_option = rtrim($short_option, ':'); + } + $long_option = $option['long_option'] ?? ''; + if (str_ends_with($long_option, ':')) { + $long_option = rtrim($long_option, ':'); + } + if ($short_option === $set_option || + $long_option === $set_option) { + return $option['functions'] ?? [$option['function']] ?? []; + } + } + return []; + } + + /** + * Parse CLI options using getopt() + * @return void + */ + protected static function parse_service_command_options(): void { + //base class short options + self::$available_command_options = self::base_command_options(); + + //get the options from the child class + static::set_command_options(); + + //collapse short options to a string + $short_options = self::get_short_options(); + + //isolate long options + $long_options = self::get_long_options(); + + //parse the short and long options + $options = getopt($short_options, $long_options); + + //make the options available to the child object + if ($options !== false) { + self::$parsed_command_options = $options; + } else { + //make sure the command_options are reset + self::$parsed_command_options = []; + //if the options are empty there is nothing left to do + return; + } + + //notify user + self::log("CLI Options detected: " . implode(",", self::$parsed_command_options), LOG_DEBUG); + + //loop through the parsed options given on the command line + foreach ($options as $option_key => $option_value) { + + //get the function responsible for handling the cli option + $funcs = self::get_user_callbacks_from_available_options($option_key); + + //ensure it was found before we take action + if (!empty($funcs)) { + //check for more than one function to be called is permitted + if (is_array($funcs)) { + //call each one + foreach($funcs as $func) { + //use the best method to call the function + self::call_function($func, $option_value); + } + } else { + //single function call + self::call_function($func, $option_value); + } + } + } + } + + // + // Calls a function using the best suited PHP method + // + private static function call_function($function, $args) { + if ($function === 'exit') { + //check for exit + exit($args); + } elseif ($function instanceof Closure || function_exists($function)) { + //globally available function or closure + $function($args); + } else { + static::$function($args); + } + } + + /** + * Checks the file system for a pid file that matches the process ID from this running instance + * @return bool true if pid exists and false if not + */ + public static function is_running(): bool { + return posix_getpid() === self::get_service_pid(); + } + + public static function is_any_running(): bool { + return self::get_service_pid() !== false; + } + + /** + * Returns the operating system service PID or false if it is not yet running + * @return bool|int PID or false if not running + */ + protected static function get_service_pid() { + if (file_exists(self::$pid_file)) { + $pid = file_get_contents(self::$pid_file); + if (function_exists('posix_getsid')) { + if (posix_getsid($pid) !== false) { + //return the pid for reloading configuration + return $pid; + } + } else { + if (file_exists('/proc/' . $pid)) { + //return the pid for reloading configuration + return $pid; + } + } + } + return false; + } + + /** + * Create an operating system PID file removing any existing PID file + */ + private function create_service_pid() { + // Set the pid filename + $basename = basename(self::$pid_file, '.pid'); + $pid = getmypid(); + + // Remove the old pid file + if (file_exists(self::$pid_file)) { + unlink(self::$pid_file); + } + + // Show the details to the user + self::log("Service : $basename", LOG_INFO); + self::log("Process ID: $pid", LOG_INFO); + self::log("PID File : " . self::$pid_file, LOG_INFO); + + // Save the pid file + file_put_contents(self::$pid_file, $pid); + } + + /** + * Creates the service directory to store the PID + * @throws Exception thrown when the service directory is unable to be created + */ + private function create_service_directory() { + //make sure the /var/run/fusionpbx directory exists + if (!file_exists('/var/run/fusionpbx')) { + $result = mkdir('/var/run/fusionpbx', 0777, true); + if (!$result) { + throw new Exception('Failed to create /var/run/fusionpbx'); + } + } + } + + /** + * Parses the debug level to an integer and stores it in the class for syslog use + * @param string $debug_level Debug level with any of the Linux system log levels + */ + protected static function set_debug_level(string $debug_level) { + // Map user input log level to syslog constant + switch ($debug_level) { + case '0': + case 'emergency': + self::$log_level = LOG_EMERG; // Hardware failures + break; + case '1': + case 'alert': + self::$log_level = LOG_ALERT; // Loss of network connection or a condition that should be corrected immediately + break; + case '2': + case 'critical': + self::$log_level = LOG_CRIT; // Condition like low disk space + break; + case '3': + case 'error': + self::$log_level = LOG_ERR; // Database query failure, file not found + break; + case '4': + case 'warning': + self::$log_level = LOG_WARNING; // Deprecated function usage, approaching resource limits + break; + case '5': + case 'notice': + self::$log_level = LOG_NOTICE; // Normal conditions + break; + case '6': + case 'info': + self::$log_level = LOG_INFO; // Informational + break; + case '7': + case 'debug': + self::$log_level = LOG_DEBUG; // Debugging + break; + default: + self::$log_level = LOG_NOTICE; // Default to NOTICE if invalid level + } + } + + /** + * Show memory usage to the user + */ + protected static function show_mem_usage() { + //current memory + $memory_usage = memory_get_usage(); + //peak memory + $memory_peak = memory_get_peak_usage(); + self::log('Current memory: ' . round($memory_usage / 1024) . " KB", LOG_INFO); + self::log('Peak memory: ' . round($memory_peak / 1024) . " KB", LOG_INFO); + } + + /** + * Logs to the system log + * @param string $message + * @param int $level + */ + protected static function log(string $message, int $level = null) { + // Use default log level if not provided + if ($level === null) { + $level = self::$log_level; + } + + // Log the message to syslog + syslog($level, 'fusionpbx[' . posix_getpid() . ']: ['.self::class.'] '.$message); + } + + /** + * Returns a file safe class name with \ from namespaces converted to _ + * @return string file safe name + */ + protected static function base_file_name(): string { + return str_replace('\\', "_", static::class); + } + + /** + * Returns only the name of the class without namespace + * @return string base class name + */ + protected static function base_class_name(): string { + $class_and_namespace = explode('\\', static::class); + return array_pop($class_and_namespace); + } + + /** + * Write a standard copyright notice to the console + * @return void + */ + public static function display_copyright(): void { + echo "FusionPBX\n"; + echo "Version: MPL 1.1\n"; + echo "\n"; + echo "The contents of this file are subject to the Mozilla Public License Version\n"; + echo "1.1 (the \"License\"); you may not use this file except in compliance with\n"; + echo "the License. You may obtain a copy of the License at\n"; + echo "http://www.mozilla.org/MPL/\n"; + echo "\n"; + echo "Software distributed under the License is distributed on an \"AS IS\" basis,\n"; + echo "WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License\n"; + echo "for the specific language governing rights and limitations under the\n"; + echo "License.\n"; + echo "\n"; + echo "The Original Code is FusionPBX\n"; + echo "\n"; + echo "The Initial Developer of the Original Code is\n"; + echo "Mark J Crane \n"; + echo "Portions created by the Initial Developer are Copyright (C) 2008-2023\n"; + echo "the Initial Developer. All Rights Reserved.\n"; + echo "\n"; + echo "Contributor(s):\n"; + echo "Mark J Crane \n"; + echo "Tim Fry \n"; + echo "\n"; + } + + /** + * Sends the shutdown signal to the service using a posix signal. + *

NOTE:
+ * The signal will not be received from the service if the + * command is sent from a user that has less privileges then + * the running service. For example, if the service is started + * by user root and then the command line option '-r' is given + * as user www-data, the service will not receive this signal + * because the OS will not allow the signal to be passed to a + * more privileged user due to security concerns. This would + * be the main reason why you must run a 'systemctl' or a + * 'service' command as root user. It is possible to start the + * service with user www-data and then the web UI would in fact + * be able to send the reload signal to the running service.

+ */ + public static function send_signal($posix_signal) { + $signal_name = ""; + switch ($posix_signal) { + case SIGHUP: + case SIGUSR1: + $signal_name = "Reload"; + break; + case SIGTERM: + case SIGUSR2: + $signal_name = "Shutdown"; + break; + } + $pid = self::get_service_pid(); + if ($pid === false) { + self::log("service not running", LOG_EMERG); + } else { + if (posix_kill((int) $pid, $posix_signal) ) { + echo "Sent $signal_name\n"; + } else { + $err = posix_strerror(posix_get_last_error()); + echo "Failed to send $signal_name: $err\n"; + } + } + } + + /** + * Display a basic help message to the user for using service + */ + protected static function display_help_message(): void { + //get the classname of the child class + $class_name = self::base_class_name(); + + //get the widest options for proper alignment + $width_short = max(array_map(function ($arr) { return strlen($arr['short_description'] ?? ''); }, self::$available_command_options)); + $width_long = max(array_map(function ($arr) { return strlen($arr['long_description' ] ?? ''); }, self::$available_command_options)); + + //display usage help using the class name of child + echo "Usage: php $class_name [options]\n"; + + //display the options aligned to the widest short and long options + echo "Options:\n"; + foreach (self::$available_command_options as $option) { + printf("%-{$width_short}s %-{$width_long}s %s\n", + $option['short_description'], + $option['long_description'], + $option['description'] + ); + } + } + + public static function send_reload() { + if (self::is_any_running()) { + self::send_signal(SIGUSR1); + } else { + die("Service Not Started\n"); + } + exit(); + } + + // + // Options built-in to the base service class. These can be overridden with the child class + // or they can be extended using the array + // + private static function base_command_options(): array { + //put the display for help in an array so we can calculate width + $help_options = []; + $index = 0; + $help_options[$index]['short_option'] = 'v'; + $help_options[$index]['long_option'] = 'version'; + $help_options[$index]['description'] = 'Show the version information'; + $help_options[$index]['short_description'] = '-v'; + $help_options[$index]['long_description'] = '--version'; + $help_options[$index]['functions'][] = 'display_version'; + $help_options[$index]['functions'][] = 'shutdown'; + $index++; + $help_options[$index]['short_option'] = 'h'; + $help_options[$index]['long_option'] = 'help'; + $help_options[$index]['description'] = 'Show the version and help message'; + $help_options[$index]['short_description'] = '-h'; + $help_options[$index]['long_description'] = '--help'; + $help_options[$index]['functions'][] = 'display_version'; + $help_options[$index]['functions'][] = 'display_help_message'; + $help_options[$index]['functions'][] = 'shutdown'; + $index++; + $help_options[$index]['short_option'] = 'a'; + $help_options[$index]['long_option'] = 'about'; + $help_options[$index]['description'] = 'Show the version and copyright information'; + $help_options[$index]['short_description'] = '-a'; + $help_options[$index]['long_description'] = '--about'; + $help_options[$index]['functions'][] = 'display_version'; + $help_options[$index]['functions'][] = 'display_copyright'; + $help_options[$index]['functions'][] = 'shutdown'; + $index++; + $help_options[$index]['short_option'] = 'r'; + $help_options[$index]['long_option'] = 'reload'; + $help_options[$index]['description'] = 'Reload settings for an already running service'; + $help_options[$index]['short_description'] = '-r'; + $help_options[$index]['long_description'] = '--reload'; + $help_options[$index]['functions'][] = 'send_reload'; + $index++; + $help_options[$index]['short_option'] = 'd:'; + $help_options[$index]['long_option'] = 'debug:'; + $help_options[$index]['description'] = 'Set the syslog level between 0 (EMERG) and 7 (DEBUG). 5 (INFO) is default'; + $help_options[$index]['short_description'] = '-d '; + $help_options[$index]['long_description'] = '--debug '; + $help_options[$index]['functions'][] = 'set_debug_level'; + $index++; + $help_options[$index]['short_option'] = 'c:'; + $help_options[$index]['long_option'] = 'config:'; + $help_options[$index]['description'] = 'Full path and file name of the configuration file to use. /etc/fusionpbx/config.conf or /usr/local/etc/fusionpbx/config.conf on FreeBSD is default'; + $help_options[$index]['short_description'] = '-c '; + $help_options[$index]['long_description'] = '--config '; + $help_options[$index]['functions'][] = 'set_config_file'; + $index++; + $help_options[$index]['short_option'] = 'x'; + $help_options[$index]['long_option'] = 'exit'; + $help_options[$index]['description'] = 'Exit the service gracefully'; + $help_options[$index]['short_description'] = '-x'; + $help_options[$index]['long_description'] = '--exit'; + $help_options[$index]['functions'][] = 'send_shutdown'; + $help_options[$index]['functions'][] = 'shutdown'; + return $help_options; + } + + /** + * Set the configuration file location to use for a config object + */ + public static function set_config_file(string $file = '/etc/fusionpbx/config.conf') { + if (empty(self::$config_file)) { + self::$config_file = $file; + } + self::$config = new config(self::$config_file); + } + + /** + * Appends the CLI option to the list given to the user as a command line argument. + * @param command_option $option + * @return int The index of the item added + */ + public static function append_command_option(command_option $option): int { + $index = count(self::$available_command_options); + self::$available_command_options[$index] = $option->to_array(); + return $index; + } + + /** + * Adds an option to the command line parameters + * @param string $short_option + * @param string $long_option + * @param string $description + * @param string $short_description + * @param string $long_description + * @param string $callback + * @return int The index of the item added + */ + public static function add_command_option(string $short_option, string $long_option, string $description, string $short_description = '', string $long_description = '', ...$callback): int { + //use the option as the description if not filled in + if (empty($short_description)) { + $short_description = '-' . $short_option; + if (str_ends_with($short_option, ':')) { + $short_description .= " "; + } + } + if (empty($long_description)) { + $long_description = '-' . $long_option; + if (str_ends_with($long_option, ':')) { + $long_description .= " "; + } + } + $index = count(self::$available_command_options); + self::$available_command_options[$index]['short_option'] = $short_option; + self::$available_command_options[$index]['long_option'] = $long_option; + self::$available_command_options[$index]['description'] = $description; + self::$available_command_options[$index]['short_description'] = $short_description; + self::$available_command_options[$index]['long_description'] = $long_description; + self::$available_command_options[$index]['functions'] = $callback; + return $index; + } + + /** + * Returns the process ID filename used for a service + * @return string file name used for the process identifier + */ + public static function get_pid_filename(): string { + return '/var/run/fusionpbx/' . self::base_file_name() . '.pid'; + } + + /** + * Sets the following: + * - execution time to unlimited + * - location for PID file + * - parses CLI options + * - ensures folder structure exists + * - registers signal handlers + */ + private function init() { + + // Increase limits + set_time_limit(0); + ini_set('max_execution_time', 0); + ini_set('memory_limit', '512M'); + + //set the PID file + self::$pid_file = self::get_pid_filename(); + + //register the shutdown function + register_shutdown_function([$this, 'shutdown']); + + // Ensure we have only one instance + if (self::is_any_running()) { + self::log("Service already running", LOG_ERR); + exit(); + } + + // Ensure directory creation for pid location + $this->create_service_directory(); + + // Create a process identifier file + $this->create_service_pid(); + + // Set the signal handlers for reloading + $this->register_signal_handlers(); + + // We are now considered running + $this->running = true; + } + + /** + * Creates a system service that will run in the background + * @return self + */ + public static function create(): self { + //can only start from command line + defined('STDIN') or die('Unauthorized'); + + //force launching in a seperate process + if ($pid = pcntl_fork()) { + exit; + } + + if ($cid = pcntl_fork()) { + exit; + } + + //set the PID file we will use + self::$pid_file = self::get_pid_filename(); + + //TODO remove updated settings object after merge + if (file_exists( __DIR__ . '/settings.php')) { + require_once __DIR__ . '/settings.php'; + } + + //TODO remove global functions after merge + if (file_exists(dirname(__DIR__).'/functions.php')) { + require_once dirname(__DIR__).'/functions.php'; + } + + //parse the cli options and store them statically + self::parse_service_command_options(); + + //create the config object if not already created + if (self::$config === null) { + self::$config = new config(self::$config_file); + } + + //get the name of child object + $class = self::base_class_name(); + + //create the child object + $service = new $class(); + + //initialize the service + $service->init(); + + //return the initialized object + return $service; + } + +} + +/* + * Example + * + * The child_service class must be used to demonstrate the base_service because base_service is abstract. This means that you + * cannot use the syntax of: + * $service = new service(); //throws fatal error + * $service->run(); //never reaches this statement + * + * Instead, you must use a class that will extend the service class like this: + * $service = child_service::create(); + * $service->run(); + * (make the code below more readable by putting) + * ( in the '/' line below to complete the comment section ) + * + +// +// A class that extends base_service must implement 4 functions: +// - run() This is the entry point called from an external source after the create method is called +// - reload_settings This is called when the CLI option -r or --reload is used +// - display_version +// - command_options +// +// Using the class below use the commands +// $simple_example = simple_example::create(); +// $simple_example->run(); +// +// This will create the class and then run it once and exit with a success code. +// +// +class simple_example extends service { + + protected function reload_settings(): void { + + } + + protected static function display_version(): void { + echo "Version 1.00\n"; + } + + protected static function set_command_options() { + + } + + public function run(): int { + echo "Successfully ran child service\n"; + echo "Try command line options -h or -v\n"; + return 0; + } +} + +//*/ +/* +// +// This class is more complex in that it will continue to run with a connection to a database +// +// The service class is divided between static and non-static methods. The static methods are +// used and called before the service is run allowing the CLI options to be read and parsed +// before the object is initialized. This allows for configuration options to be available +// when the child class is first started up. Keep in mind that these are called statically +// so that all callback functions declared in the cli options must be static. +// +class child_service extends service { + + // + // Using a version constant is ideal for tracking and reporting + // + const CHILD_SERVICE_VERSION = '1.00'; + + // + // The parent service does not create a database connection as the child service may not need it. This example + // demonstrates how the config object is passed from the parent and then used in the child service to connect + // to other resources or use other settings the base class loaded so the child class automatically inherits. + // + private $database; + + // This example uses a settings object to demonstrate how the config is passed through to the child class + // and is then used again in the reload_settings to demonstrate how the settings could be reloaded + // with changes in the configuration, database connection, and default settings without the need to create + // new instances of the config object. + private $settings; + + // + // This function is required from the base service class because it is used when the reload command line option is used + // + protected function reload_settings(): void { + //informing the user in this example is simple but can use the parent class log functions + echo "Reloading settings\n"; + + // + // Reload the configuration file + // + self::$config->read(); + + // + // If services have their own configuration file that was passed in using the -c or --config option, the options + // would be available here as well to the child class + // By allowing the config file to be specified, it is possible for services to have a configuration specific to them + // while it could still be possible to allow access to the original making it very flexible with a wide degree of + // choices. + // + // For example, specifying a configuration file that could be used for an archive or backup server would allow + // the backup service to connect to another system remotely. + // + // It could also be used to separate the web configuration from system services to keep them organized and allow for + // configuration settings to be available should the database fail. One possible scenario where this could be useful + // is to send an email if the database stops responding. Currently, this is not possible as the database class uses + // the 'die' command to immediately exit. I think it would be good to remove that and instead set the error message + // to be something that would reflect the error allowing a system service to detect and even possibly correct that. + // + $alert_email = self::$config->get('alert_email', ''); + $smtp_host = self::$config->get('smtp_host', ''); + $smtp_port = self::$config->get('smtp_port', ''); + + // + // Ensure the database is connected with the new configuration parameters + // + $this->database->connect(); + + // + // The reload settings here completes the chain + // + $this->settings->reload(); + + } + + // + // This run function is required as it is called to launch child_service. This + // is the entry point for the child class. + // + public function run(): int { + + // + // Create the database object once passing a reference to the config object + // + $this->database = new database(['config' => self::$config]); + + // + // Create the settings object using the database connection + // + $this->settings = new settings(['database' => $this->database]); + + // + // In this example I have used the reload_settings because it is required by the parent class + // whenever the '-r' or '--reload' option is given on the CLI. The base class is responsible for + // parsing the information given on the CLI. Whenever the base class detects a '-r' option, the + // reload_settings method in the child class is called. This gives the responsibility to the the + // child class to reload any settings that might be needed during long execution of the service + // without stopping and starting the service. The method is called here to initialize any and all + // objects within the child service. + // + $this->reload_settings(); + + // + // The $running property is declared in the base service class as a boolean and it is responsible + // to enable this so that the child class can run. The base service class will set this to false + // if it receives a shutdown command from either the OS, PHP, or a posix signal allowing the child + // class to respond or clean up after the while loop. + // + while($this->running) { + // + // This is where the actual heart of the code for the new service will be created + // + echo "Doing something..." . date("Y-m-d H:i:s") . "\n"; + sleep(1); + } + + + // + // Returning a non-zero value would indicate there was an issue. Here we return zero to indicate graceful shutdown. + // + return 0; + } + + // + // This is the version that will be displayed when the option '-v' or '--version' is used on the command line. + // This run function is required + // + protected static function display_version(): void { + echo "Child service example version " . self::CHILD_SERVICE_VERSION . "\n"; + } + + // + // set_command_options can either add to or replace options. Replacing the base options would allow an override for default behaviour. + // This run function is required + // + protected static function set_command_options() { + + // + // The options below are added to the CLI options and displayed whenever the -h or --help option is used. + // There are multiple methods are used to suite the style of the creator + // + + // + // The callbacks set here are used to demonstrate multiple calls can be used + // + + //using the parameter in the function + self::add_command_option( + 't:' + , 'template:' + , 'Full path and file name of the template file to use' + , '-t ' + , '--template ' + , ['set_template_path'] + ); + //using a container object + self::append_command_option(command_option::new() + ->short_option('n') + ->long_option('null') + ->description('This option is to demonstrate using a cli object to create cli options') + ->functions(['null_function_method']) + ); + //using an array of key/value pairs + self::append_command_option(command_option::new([ + 'short_option' => 'z:' + ,'long_option' => 'zero:' + ,'description' => 'This has zero effect on behavior' + ,'function' => 'call_single_function' + ])); + + // + // These options are here but are commented out to allow the functionality to still exist in the parent + // +// +// //replace cli options in the parent class using array +// $index = 0; +// $arr_options = []; +// $arr_options[$index]['short_option'] = 'z'; +// $arr_options[$index]['long_option'] = 'zero'; +// $arr_options[$index]['description'] = 'This has zero effect on behavior'; +// $arr_options[$index]['short_description'] = '-z'; +// $arr_options[$index]['long_description'] = '--zero'; +// $arr_options[$index]['function'][] = 'call_single_function'; +// self::$available_command_options = $arr_options; +// +// //replace all cli options using container object +// $arr_options = []; +// self::$available_command_options = []; +// $arr_options[0] = command_option::new() +// ->short_option('z') +// ->short_description('-z') +// ->function('call_a_function') +// ->function('call_another_function_after_first') +// ->description('This option does nothing') +// ->to_array(); +// +// $arr_options[1] = command_option::new([ +// 'short_option' => 'z' +// ,'long_option' => '--zero' +// ,'description' => 'This option does nothing' +// ,'functions' => ['call_a_function', 'call_another_function'] +// ])->to_array(); + //self::$available_command_options = $arr_options; + } +} // class child_service + +//*/ + +/* +// +// Standard includes do not apply for the base class because the require.php has included many other php files. These other files +// or objects may not be required for some services. Thus, only the config is required for base_service. Child services may then +// create a database class and use it by passing the config object to the database constructor. This is why the 'require.php' is +// left out of the initial setup class. +// + +// Use the auto_loader to find any classes needed so we don't have a lot of include statements +// In this example, the auto_loader should not be using the PROJECT_ROOT or any other defined constants +// because they are not needed in the initial stage of loading +require_once __DIR__ . '/auto_loader.php'; + +// We don't need to ever reference the object so don't assign a variable. It +// would be a good idea to remove the auto_loader as a class declaration so +// that there would only need to be one line. It seems illogical to have an +// object that never needs to be referenced. +new auto_loader(); + +// The base_service class has a 'protected' constructor, meaning you are not able to use "new" to create the object. Instead, you +// must use the 'create' static method to create an object. This technique is employed because some PHP versions have an issue with +// registering signal listeners in the constructor. See the link https://www.php.net/manual/en/function.pcntl-signal.php in the user +// comments section. +// The child_service class does not override the parent constructor so parent constructor is used. If the child_service class does +// have a constructor then the child class must call: +// parent::__construct($config); +// as the first line of the child constructor. This is because the parent constructor uses the config class. This also means +// that the child class must receive the config object in the constructor as a minimum. +$service = child_service::create(); + +// The run class is declared as abstract in the parent. So the child class must have one. +$service->run(); +//*/