*/ private $options = []; // page and host name /** @var string */ private $page_name; /** @var string */ private $host_name; /** @var int */ private $host_port; // internal error reporting vars /** @var array */ private $error_msg = []; // the "connection" to the outside errors // debug output prefix /** @var string */ private $error_msg_prefix = ''; // prefix to the error string (the class name) // debug flags/settings /** @var string */ private $running_uid = ''; // unique ID set on class init and used in logging as prefix // log file name /** @var string */ private $log_folder = ''; /** @var string */ private $log_file_name_ext = 'log'; // use this for date rotate /** @var string */ private $log_file_name = ''; /** @var int */ private $log_max_filesize = 0; // set in kilobytes /** @var string */ private $log_print_file = 'error_msg{LOGID}{LEVEL}{CLASS}{PAGENAME}{DATE_RUNID}'; /** @var string */ private $log_file_unique_id; // a unique ID set only once for call derived from this class /** @var string */ private $log_file_date = ''; // Y-m-d file in file name /** @var bool */ private $log_print_file_date = true; // if set add Y-m-d and do automatic daily rotation /** @var string */ private $log_file_id = ''; // a alphanumeric name that has to be set as global definition /** @var bool */ private $log_per_level = false; // set, it will split per level (first parameter in debug call) /** @var bool */ private $log_per_class = false; // set, will split log per class /** @var bool */ private $log_per_page = false; // set, will split log per called file /** @var bool */ private $log_per_run = false; // create a new log file per run (time stamp + unique ID) // script running time /** @var float */ private $script_starttime; /** @var string[] current log levels */ private $log_levels = ['debug', 'echo', 'print']; /** @var string[] log group per what for writing to file */ private $log_grouping = ['level', 'class', 'page', 'run']; // debug flags [they must exist or we get a warning] /** @var array */ private $debug_output = []; // if this is true, show debug on desconstructor /** @var array */ private $debug_output_not = []; /** @var bool */ private $debug_output_all = false; /** @var array */ private $echo_output = []; // errors: echo out, default is 1 /** @var array */ private $echo_output_not = []; /** @var bool */ private $echo_output_all = false; /** @var array */ private $print_output = []; // errors: print to file, default is 0 /** @var array */ private $print_output_not = []; /** @var bool */ private $print_output_all = false; /** * Init logger * * global vars that can be used * - BASE * - LOG * - LOG_FILE_ID * options array layout * - log_folder: * - file_id: * - unique_id: * - print_file_date: * - log_per_level: * - log_per_class: * - log_per_page: * - log_per_run: * - debug_all: * - echo_all: * - print_all: * - debug (array): * - echo (array): * - print (array): * - debug_not (array): * - echo_not (array): * - print_not (array): * * @param array $options Array with settings options */ public function __construct(array $options = []) { // copy the options over $this->options = $options; // set log folder from options $this->log_folder = $this->options['log_folder'] ?? ''; // legacy flow, check must set constants if (empty($this->log_folder) && defined('BASE') && defined('LOG')) { /** @deprecated Do not use this anymore, define path on class load */ trigger_error( 'options: log_folder must be set. Setting via BASE and LOG constants is deprecated', E_USER_DEPRECATED ); // make sure this is writeable, else skip $this->log_folder = BASE . LOG; } // fallback + notice if (empty($this->log_folder)) { /* trigger_error( 'options or constant not set or folder not writable. fallback to: ' . getcwd(), E_USER_NOTICE ); */ $this->log_folder = getcwd() . DIRECTORY_SEPARATOR; } // if folder is not writeable, abort if (!is_writeable($this->log_folder)) { trigger_error( 'Folder: ' . $this->log_folder . ' is not writeable for logging', E_USER_ERROR ); } // check if log_folder has a trailing / if (substr($this->log_folder, -1, 1) != DIRECTORY_SEPARATOR) { $this->log_folder .= DIRECTORY_SEPARATOR; } // running time start for script $this->script_starttime = microtime(true); // set per run UID for logging $this->running_uid = Uids::uniqIdShort(); // set the page name $this->page_name = System::getPageName(); // set host name list($this->host_name , $this->host_port) = System::getHostName(); // add port to host name if not port 80 if ($this->host_port != 80) { $this->host_name .= ':' . $this->host_port; } // can be overridden with basicSetLogFileId later if (!empty($this->options['file_id'])) { $this->setLogId($this->options['file_id']); } elseif (!empty($GLOBALS['LOG_FILE_ID'])) { /** @deprecated Do not use this anymore, define file_id on class load */ trigger_error( 'options: file_id must be set. Setting via LOG_FILE_ID global variable is deprecated', E_USER_DEPRECATED ); // legacy flow, should be removed and only set via options $this->setLogId($GLOBALS['LOG_FILE_ID']); // TODO trigger deprecation error // trigger_error( // 'Debug\Logging: Do not use globals LOG_FILE_ID to set log id for Logging', // E_USER_DEPRECATED // ); } elseif (defined('LOG_FILE_ID')) { /** @deprecated Do not use this anymore, define file_id on class load */ trigger_error( 'options: file_id must be set. Setting via LOG_FILE_ID constant is deprecated', E_USER_DEPRECATED ); // legacy flow, should be removed and only set via options $this->setLogId((string)LOG_FILE_ID); // trigger deprecation error // trigger_error( // 'Debug\Logging: Do not use constant LOG_FILE_ID to set log id for Logging', // E_USER_DEPRECATED // ); } // init the log levels $this->initLogLevels(); } // *** PRIVATE *** /** * init the basic log levels based on global set variables * * @return void */ private function initLogLevels(): void { // if given via parameters, only for all // globals overrule given settings, for one (array), eg $ECHO['db'] = 1; foreach ($this->log_levels as $type) { // include or exclude (off) from output foreach (['on', 'off'] as $flag) { $in_type = $type; if ($flag == 'off') { $in_type .= '_not'; } $up_type = strtoupper($in_type); if ( isset($this->options[$in_type]) && is_array($this->options[$in_type]) ) { $this->setLogLevel($type, $flag, $this->options[$in_type]); } elseif ( isset($GLOBALS[$up_type]) && is_array($GLOBALS[$up_type]) ) { // TODO trigger deprecation error $this->setLogLevel($type, $flag, $GLOBALS[$up_type]); } } } // TODO remove all $GLOBALS call and only use options // all overrule $this->setLogLevelAll( 'debug', $this->options['debug_all'] ?? // for user login, should be handled outside like globals $_SESSION['DEBUG_ALL'] ?? // DEPRECATED $GLOBALS['DEBUG_ALL'] ?? // DEPRECATED false ); $this->setLogLevelAll( 'print', $this->options['print_all'] ?? // for user login, should be handled outside like globals $_SESSION['DEBUG_ALL'] ?? // DEPRECATED $GLOBALS['PRINT_ALL'] ?? // DEPRECATED false ); $this->setLogLevelAll( 'echo', $this->options['echo_all'] ?? $GLOBALS['ECHO_ALL'] ?? // DEPRECATED false ); // GLOBAL rules for log writing // add file date is default on $this->setGetLogPrintFileDate( $this->options['print_file_date'] ?? $GLOBALS['LOG_PRINT_FILE_DATE'] ?? // DEPRECATED true ); // all other logging file name flags are off $this->setLogPer( 'level', $this->options['per_level'] ?? $GLOBALS['LOG_PER_LEVEL'] ?? // DEPRECATED false ); $this->setLogPer( 'class', $this->options['per_class'] ?? $GLOBALS['LOG_PER_CLASS'] ?? // DEPRECATED false ); $this->setLogPer( 'page', $this->options['per_page'] ?? $GLOBALS['LOG_PER_PAGE'] ?? // DEPRECATED false ); $this->setLogPer( 'run', $this->options['per_run'] ?? $GLOBALS['LOG_PER_RUN'] ?? // DEPRECATED false ); // set log per date if ($this->setGetLogPrintFileDate()) { $this->log_file_date = date('Y-m-d'); } // set per run ID if ($this->log_per_run) { $this->setLogUniqueId(); } } /** * checks if we have a need to work on certain debug output * Needs debug/echo/print ad target for which of the debug flag groups we check * also needs level string to check in the per level output flag check. * In case we have invalid target it will return false * * @param string $target target group to check debug/echo/print * @param string $level level to check in detailed level flag * @return bool true on access allowed or false on no access */ private function doDebugTrigger(string $target, string $level): bool { $access = false; // check if we do debug, echo or print if ( ( $this->getLogLevel($target, 'on', $level) || $this->getLogLevelAll($target) ) && !$this->getLogLevel($target, 'off', $level) ) { $access = true; } return $access; } /** * writes error msg data to file for current level * * @param string $level the level to write * @param string $error_string error string to write * @return bool True if message written, False if not */ private function writeErrorMsg(string $level, string $error_string): bool { // only write if write is requested if ( !($this->doDebugTrigger('debug', $level) && $this->doDebugTrigger('print', $level)) ) { return false; } // init base file path $fn = $this->log_folder . $this->log_print_file . '.' . $this->log_file_name_ext; // log ID prefix settings, if not valid, replace with empty if (!empty($this->log_file_id)) { $rpl_string = '_' . $this->log_file_id; } else { $rpl_string = ''; } $fn = str_replace('{LOGID}', $rpl_string, $fn); // log id (like a log file prefix) // if run id, we auto add ymd, so we ignore the log file date if ($this->log_per_run) { $rpl_string = '_' . $this->log_file_unique_id; // add 8 char unique string } elseif ($this->setGetLogPrintFileDate()) { $rpl_string = '_' . $this->log_file_date; // add date to file } else { $rpl_string = ''; } $fn = str_replace('{DATE_RUNID}', $rpl_string, $fn); // create output filename // write per level $rpl_string = !$this->log_per_level ? '' : // normalize level, replace all non alphanumeric characters with - '_' . ( // if return is only - then set error string preg_match( "/^-+$/", $level_string = preg_replace("/[^A-Za-z0-9-_]/", '-', $level) ?? '' ) ? 'INVALID-LEVEL-STRING' : $level_string ); $fn = str_replace('{LEVEL}', $rpl_string, $fn); // create output filename // set per class, but don't use get_class as we will only get self $rpl_string = !$this->log_per_class ? '' : '_' // set sub class settings . str_replace('\\', '-', Support::getCallerTopLevelClass()); $fn = str_replace('{CLASS}', $rpl_string, $fn); // create output filename // if request to write to one file $rpl_string = !$this->log_per_page ? '' : '_' . System::getPageName(System::NO_EXTENSION); $fn = str_replace('{PAGENAME}', $rpl_string, $fn); // create output filename // write to file // first check if max file size is is set and file is bigger if ( $this->log_max_filesize > 0 && ((filesize($fn) / 1024) > $this->log_max_filesize) ) { // for easy purpose, rename file only to attach timestamp, nur sequence numbering rename($fn, $fn . '.' . date("YmdHis")); } $this->log_file_name = $fn; $fp = fopen($this->log_file_name, 'a'); if ($fp !== false) { fwrite($fp, $error_string); fclose($fp); return true; } else { echo ""; return false; } } // *** PUBLIC *** /** * Temporary method to read all class variables for testing purpose * * @param string $name what variable to return * @return mixed can be anything, bool, string, int, array */ public function getSetting(string $name): mixed { // for debug purpose only return $this->{$name}; } /** * sets the internal log file prefix id * string must be a alphanumeric string * if non valid string is given it returns the previous set one only * * @param string $string log file id string value * @return string returns the set log file id string * @deprecated Use $log->setLogId() */ public function basicSetLogId(string $string): string { return $this->setLogId($string); } /** * sets the internal log file prefix id * string must be a alphanumeric string * if non valid string is given it returns the previous set one only * * @param string $string log file id string value * @return string returns the set log file id string */ public function setLogId(string $string): string { if (preg_match("/^[\w\-]+$/", $string)) { $this->log_file_id = $string; } return $this->log_file_id; } /** * return current set log file id * @return string */ public function getLogId(): string { return $this->log_file_id; } /** * old name for setLogLevel * * @param string $type debug, echo, print * @param string $flag on/off * array $array of levels to turn on/off debug * @return bool Return false if type or flag is invalid * @deprecated Use setLogLevel */ public function debugFor(string $type, string $flag): bool { /** @phan-suppress-next-line PhanTypeMismatchArgumentReal, PhanParamTooFew @phpstan-ignore-next-line */ return $this->setLogLevel(...[func_get_args()]); } /** * set log level settings for All types * if invalid type, skip * * @param string $type Type to get: debug, echo, print * @param bool $set True or False * @return bool Return false if type invalid */ public function setLogLevelAll(string $type, bool $set): bool { // skip set if not valid if (!in_array($type, $this->log_levels)) { return false; } $this->{$type . '_output_all'} = $set; return true; } /** * get the current log level setting for All level blocks * * @param string $type Type to get: debug, echo, print * @return bool False on failure, or the boolean flag from the all var */ public function getLogLevelAll(string $type): bool { // type check for debug/echo/print if (!in_array($type, $this->log_levels)) { return false; } return $this->{$type . '_output_all'}; } /** * passes list of level names, to turn on debug * eg $foo->debugFor('print', 'on', ['LOG', 'DEBUG', 'INFO']); * * @param string $type debug, echo, print * @param string $flag on/off * @param array $debug_on Array of levels to turn on/off debug * To turn off a level set 'Level' => false, * If not set, switches to on * @return bool Return false if type or flag invalid * also false if debug array is empty */ public function setLogLevel(string $type, string $flag, array $debug_on): bool { // abort if not valid type if (!in_array($type, $this->log_levels)) { return false; } // invalid flag type if (!in_array($flag, ['on', 'off'])) { return false; } if (count($debug_on) >= 1) { foreach ($debug_on as $level => $set) { $switch = $type . '_output' . ($flag == 'off' ? '_not' : ''); if (!is_bool($set)) { $level = $set; $set = true; } $this->{$switch}[$level] = $set; } } else { return false; } return true; } /** * return the log level for the array type normal and not (disable) * * @param string $type debug, echo, print * @param string $flag on/off * @param string|null $level if not null then check if this array entry is set * else return false * @return array|bool if $level is null, return array, else boolean true/false */ public function getLogLevel(string $type, string $flag, ?string $level = null): array|bool { // abort if not valid type if (!in_array($type, $this->log_levels)) { return false; } // invalid flag type if (!in_array($flag, ['on', 'off'])) { return false; } $switch = $type . '_output' . ($flag == 'off' ? '_not' : ''); // log level direct check must be not null or not empty string if (!empty($level)) { return $this->{$switch}[$level] ?? false; } // array return $this->{$switch}; } /** * set flags for per log level type * - level: set per sub group level * - class: split by class * - page: split per page called * - run: for each run * * @param string $type Type to get: level, class, page, run * @param bool $set True or False * @return bool Return false if type invalid */ public function setLogPer(string $type, bool $set): bool { if (!in_array($type, $this->log_grouping)) { return false; } $this->{'log_per_' . $type} = $set; // if per run set unique id if ($type == 'run' && $set == true) { $this->setLogUniqueId(); } return true; } /** * return current set log per flag in bool * * @param string $type Type to get: level, class, page, run * @return bool True of false for turned on or off */ public function getLogPer(string $type): bool { if (!in_array($type, $this->log_grouping)) { return false; } return $this->{'log_per_' . $type}; } /** * Sets a unique id based on current date (y/m/d, h:i:s) and a unique id (8 chars) * if override is set to true it will be newly set, else if already set nothing changes * * @param bool $override True to force new set * @return void */ public function setLogUniqueId(bool $override = false): void { if (!$this->log_file_unique_id || $override == true) { $this->log_file_unique_id = date('Y-m-d_His') . '_U_' . substr(hash('sha1', uniqid((string)mt_rand(), true)), 0, 8); } } /** * Return current set log file unique id, * empty string for not set * * @return string */ public function getLogUniqueId(): string { return $this->log_file_unique_id; } /** * Set or get the log file date extension flag * if null or empty parameter gets current flag * * @param boolean|null $set Set the date suffix for log files * If set to null return current set * @return boolean Current set flag */ public function setGetLogPrintFileDate(?bool $set = null): bool { if ($set !== null) { $this->log_print_file_date = $set; } return $this->log_print_file_date; } /** * Return current set log file name * * @return string Filename set set after the last time debug was called */ public function getLogFileName(): string { return $this->log_file_name; } /** * A replacement for the \CoreLibs\Debug\Support::printAr * But this does not wrap it in

	 * It uses some special code sets so we can convert that to pre flags
	 * for echo output {##HTMLPRE##} ... {##/HTMLPRE##}
	 * Do not use this without using it in a string in debug function
	 *
	 * @param  array $a Array to format
	 * @return string          print_r formated
	 */
	public function prAr(array $a): string
	{
		return '##HTMLPRE##' . print_r($a, true) . '##/HTMLPRE##';
	}

	/**
	 * Convert bool value to string value
	 *
	 * @param  bool   $bool  Bool value to be transformed
	 * @param  string $true  Override default string 'true'
	 * @param  string $false Override default string 'false'
	 * @return string        $true or $false string for true/false bool
	 */
	public function prBl(
		bool $bool,
		string $true = 'true',
		string $false = 'false'
	): string {
		return $bool ? $true : $false;
	}

	/**
	 * write debug data to error_msg array
	 *
	 * @param  string $level  id for error message, groups messages together
	 * @param  string $string the actual error message
	 * @param  bool   $strip  default on false, if set to true,
	 *                        all html tags will be stripped and 
changed to \n * this is only used for debug output * @param string $prefix Attach some block before $string. * Will not be stripped even * when strip is true * if strip is false, recommended to add that to $string * @return bool True if logged, false if not logged */ public function debug( string $level, string $string, bool $strip = false, string $prefix = '' ): bool { $status = false; // must be debug on and either echo or print on if ( !$this->doDebugTrigger('debug', $level) || ( // if debug is on, either print or echo must be set to on !$this->doDebugTrigger('print', $level) && !$this->doDebugTrigger('echo', $level) ) ) { return $status; } // get the last class entry and wrie that $class = Support::getCallerTopLevelClass(); // get timestamp $timestamp = Support::printTime(); // same string put for print (no html data inside) // write to file if set $status = $this->writeErrorMsg( $level, '[' . $timestamp . '] ' . '[' . $this->host_name . '] ' . '[' . System::getPageName(System::FULL_PATH) . '] ' . '[' . $this->running_uid . '] ' . '{' . $class . '} ' . '<' . $level . '> - ' // strip the htmlpre special tags if exist . str_replace( ['##HTMLPRE##', '##/HTMLPRE##'], '', // if stripping all html, etc is requested, only for write error msg ($strip ? // find any
and replace them with \n // strip rest of html elements (base only) preg_replace( "/(<\/?)(\w+)([^>]*>)/", '', str_replace('
', "\n", $prefix . $string) ) : $prefix . $string ) ?: '' ) . "\n" ); // write to error level msg array if there is an echo request if ($this->doDebugTrigger('echo', $level)) { // init if not set if (!isset($this->error_msg[$level])) { $this->error_msg[$level] = []; } // HTML string $this->error_msg[$level][] = '
' . '[' . $timestamp . '] ' . '[' . $level . '] ' . '[' . $this->host_name . '] ' . '[' . $this->page_name . '] ' . '[' . $this->running_uid . '] ' . '{' . $class . '} - ' // as is prefix, allow HTML . $prefix // we replace special HTMLPRE with
 entries
				. str_replace(
					['##HTMLPRE##', '##/HTMLPRE##'],
					['
', '
'], Html::htmlent($string) ) . "
"; $status = true; } return $status; } /** * for ECHO ON only * returns error data as string so it can be echoed out * * @param string $header_prefix prefix string for header * @return string error msg for all levels */ public function printErrorMsg(string $header_prefix = ''): string { $string_output = ''; // if not debug && echo on, do not return anything if ( !$this->getLogLevelAll('debug') || !$this->getLogLevelAll('echo') ) { return $string_output; } if ($this->error_msg_prefix) { $header_prefix = $this->error_msg_prefix; } $script_end = microtime(true) - $this->script_starttime; foreach ($this->error_msg as $level => $temp_debug_output) { if ($this->doDebugTrigger('debug', $level)) { if ($this->doDebugTrigger('echo', $level)) { $string_output .= '
' . '[' . $level . '] ' . ($header_prefix ? "**** " . Html::htmlent($header_prefix) . " ****
\n" : '') . '
' . join('', $temp_debug_output); } // echo it out } // do printout } // for each level // create the output wrapper around // so we have a nice formated output per class if ($string_output) { $string_prefix = '
' . '
{' . Support::getCallerTopLevelClass() . '}
'; $string_output = $string_prefix . $string_output . '
Script Run Time: ' . $script_end . '
' . '
'; } // } return $string_output; } /** * for ECHO ON only * unsests the error message array * can be used if writing is primary to file * if no level given resets all * * @param string $level optional level * @return void has no return */ public function resetErrorMsg(string $level = ''): void { if (!$level) { $this->error_msg = []; } elseif (isset($this->error_msg[$level])) { unset($this->error_msg[$level]); } } /** * for ECHO ON only * Get current error message array * * @return array error messages collected */ public function getErrorMsg(): array { return $this->error_msg; } /** * for ECHO ON only * merges the given error array with the one from this class * only merges visible ones * * @param array $error_msg error array * @return void has no return */ public function mergeErrors(array $error_msg = []): void { array_push($this->error_msg, ...$error_msg); } } // __END__