A super easy PHP Framework for web development!
				https://github.com/exacti/phacil-framework
			
			
		
			You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							524 lines
						
					
					
						
							12 KiB
						
					
					
				
			
		
		
	
	
							524 lines
						
					
					
						
							12 KiB
						
					
					
				<?php
 | 
						|
/*
 | 
						|
 * Copyright © 2021 ExacTI Technology Solutions. All rights reserved.
 | 
						|
 * GPLv3 General License.
 | 
						|
 * https://exacti.com.br
 | 
						|
 * Phacil PHP Framework - https://github.com/exacti/phacil-framework
 | 
						|
 */
 | 
						|
 | 
						|
namespace Phacil\Framework;
 | 
						|
 | 
						|
/** 
 | 
						|
 * The principal log class for this framework
 | 
						|
 * 
 | 
						|
 * @param string $filename (optional) The name of log file. The path is automatic defined in the DIR_LOGS constant in config file. Isn't not possible to change the path. The default name is error.log.
 | 
						|
 * @package Phacil\Framework 
 | 
						|
 */
 | 
						|
class Log implements \Phacil\Framework\Api\Log {
 | 
						|
	
 | 
						|
	/**
 | 
						|
	 * Storage the object of log file
 | 
						|
	 * 
 | 
						|
	 * @var resource|false
 | 
						|
	 */
 | 
						|
	private $fileobj;
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Storage the filename
 | 
						|
	 * 
 | 
						|
	 * @var string
 | 
						|
	 */
 | 
						|
	private $filename;
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Storage the path
 | 
						|
	 * 
 | 
						|
	 * @var string
 | 
						|
	 */
 | 
						|
	private $filepath;
 | 
						|
 | 
						|
	/**
 | 
						|
	 * 
 | 
						|
	 * @var \Phacil\Framework\Api\Log[]
 | 
						|
	 */
 | 
						|
	private $extraLoggers = [];
 | 
						|
 | 
						|
	/** @var bool */
 | 
						|
	protected $removeUsedContextFields = false;
 | 
						|
 | 
						|
	protected $allowInlineLineBreaks = true;
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Log output format
 | 
						|
	 * @var string
 | 
						|
	 */
 | 
						|
	protected $format = "[%datetime%] %channel%.%level_name%: %message% %context% %extra% | %route%";
 | 
						|
 | 
						|
	/**
 | 
						|
	 * @param string $filename (optional) The name of log file. The path is automatic defined in the DIR_LOGS constant in config file. Isn't not possible to change the path. The default name is error.log.
 | 
						|
	 * @return void 
 | 
						|
	 */
 | 
						|
	public function __construct($filename = \Phacil\Framework\Api\Log::DEFAULT_FILE_NAME) {
 | 
						|
		if(!defined(self::CONFIGURATION_LOGS_CONST)){
 | 
						|
			if(!defined('DIR_APPLICATION'))
 | 
						|
				trigger_error('The '.self::CONFIGURATION_LOGS_CONST.' folder configuration is required!', E_USER_ERROR);
 | 
						|
 | 
						|
			define(self::CONFIGURATION_LOGS_CONST, DIR_APPLICATION . 'logs/');
 | 
						|
		}
 | 
						|
		$this->filename = $filename;
 | 
						|
		$this->filepath = constant(self::CONFIGURATION_LOGS_CONST) . $filename;
 | 
						|
		if(!is_dir(constant(self::CONFIGURATION_LOGS_CONST))){
 | 
						|
			$old = umask(0);
 | 
						|
			mkdir(constant(self::CONFIGURATION_LOGS_CONST), self::DIR_LOGS_PERMISSIONS, true);
 | 
						|
			umask($old);
 | 
						|
		}
 | 
						|
		if(!is_writable(constant(self::CONFIGURATION_LOGS_CONST))){
 | 
						|
			trigger_error('The '. constant(self::CONFIGURATION_LOGS_CONST).' folder must to be writeable!', E_USER_ERROR);
 | 
						|
		}
 | 
						|
		//$this->fileobj = fopen($this->filepath, 'a+');
 | 
						|
		return;
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * {@inheritdoc}
 | 
						|
	 */
 | 
						|
	public function setLog($name) {
 | 
						|
		if(($name.self::LOG_FILE_EXTENSION) == $this->filename){
 | 
						|
			return $this;
 | 
						|
		}
 | 
						|
 | 
						|
		if(isset($this->extraLoggers[$name])){ 
 | 
						|
			return $this->extraLoggers[$name];
 | 
						|
		}
 | 
						|
 | 
						|
		return $this->extraLoggers[$name] = new self($name . self::LOG_FILE_EXTENSION);
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Write the error message in the log file
 | 
						|
	 * 
 | 
						|
	 * @param string $message 
 | 
						|
	 * @return int|false
 | 
						|
	 * @since 1.0.0
 | 
						|
	 */
 | 
						|
	public function write($message) {
 | 
						|
		return $this->writeToFile('[' . $this->getDateTime() . '] - ' . print_r($message, true)." | ".$_SERVER['REQUEST_URI'] . " | " . \Phacil\Framework\startEngineExacTI::getRoute());
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * @param string $message 
 | 
						|
	 * @return int|false 
 | 
						|
	 */
 | 
						|
	protected function writeToFile($message) {
 | 
						|
		if(!$this->fileobj){
 | 
						|
			$this->fileobj = fopen($this->filepath, 'a+');
 | 
						|
		}
 | 
						|
		return fwrite($this->fileobj, $message . PHP_EOL);
 | 
						|
	}
 | 
						|
 | 
						|
	/** @return string  */
 | 
						|
	protected function getDateTime() {
 | 
						|
		if (version_compare(phpversion(), "7.2.0", "<")) {
 | 
						|
			// Obtém o timestamp atual com precisão de microssegundos
 | 
						|
			$microtime = microtime(true);
 | 
						|
 | 
						|
			// Arredonda os milissegundos para três casas decimais
 | 
						|
			$milissegundos = round(($microtime - floor($microtime)) * 1000);
 | 
						|
 | 
						|
			return sprintf(date(self::DATE_TIME_FORMAT_PHP5), (string)$milissegundos);
 | 
						|
		}
 | 
						|
 | 
						|
		return (new \DateTimeImmutable())->format(self::DATE_TIME_FORMAT);
 | 
						|
	}
 | 
						|
	
 | 
						|
	/** 
 | 
						|
	 * Remove object from memory
 | 
						|
	 * 
 | 
						|
	 * @since 1.0.0
 | 
						|
	 * @return void  
 | 
						|
	 */
 | 
						|
	public function __destruct() {
 | 
						|
		if ($this->fileobj) {
 | 
						|
			fclose($this->fileobj);
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Return the log file name
 | 
						|
	 * 
 | 
						|
	 * @since 2.0.0
 | 
						|
	 * @return string 
 | 
						|
	 */
 | 
						|
	public function getFileName()
 | 
						|
	{
 | 
						|
		return $this->filename;
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Return the log file path
 | 
						|
	 * 
 | 
						|
	 * @since 2.0.0
 | 
						|
	 * @return string 
 | 
						|
	 */
 | 
						|
	public function getFilePath()
 | 
						|
	{
 | 
						|
		return $this->filepath;
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Return last lines of log archive
 | 
						|
	 * 
 | 
						|
	 * @param string|null $filepath (optional) Path of file. Default is the log file.
 | 
						|
	 * @param int $lines (optional) Lines to be readed. Default value is 10.
 | 
						|
	 * @param bool $adaptive (optional) Default value is true.
 | 
						|
	 * @return false|string 
 | 
						|
	 * @since 2.0.0
 | 
						|
	 */
 | 
						|
	public function tail($filepath = null, $lines = 10, $adaptive = true)
 | 
						|
	{
 | 
						|
 | 
						|
		// Open file
 | 
						|
		$f = @fopen(($filepath ?: $this->filepath), "rb");
 | 
						|
		if ($f === false) return false;
 | 
						|
 | 
						|
		// Sets buffer size
 | 
						|
		if (!$adaptive) 
 | 
						|
			$buffer = 4096;
 | 
						|
		else 
 | 
						|
			$buffer = ($lines < 2 ? 64 : ($lines < 10 ? 512 : 4096));
 | 
						|
 | 
						|
		// Jump to last character
 | 
						|
		fseek($f, -1, SEEK_END);
 | 
						|
 | 
						|
		// Read it and adjust line number if necessary
 | 
						|
		// (Otherwise the result would be wrong if file doesn't end with a blank line)
 | 
						|
		if (fread($f, 1) != "\n") $lines -= 1;
 | 
						|
 | 
						|
		// Start reading
 | 
						|
		$output = '';
 | 
						|
		$chunk = '';
 | 
						|
 | 
						|
		// While we would like more
 | 
						|
		while (ftell($f) > 0 && $lines >= 0) {
 | 
						|
 | 
						|
			// Figure out how far back we should jump
 | 
						|
			$seek = min(ftell($f), $buffer);
 | 
						|
 | 
						|
			// Do the jump (backwards, relative to where we are)
 | 
						|
			fseek($f, -$seek, SEEK_CUR);
 | 
						|
 | 
						|
			// Read a chunk and prepend it to our output
 | 
						|
			$output = ($chunk = fread($f, $seek)) . $output;
 | 
						|
 | 
						|
			// Jump back to where we started reading
 | 
						|
			fseek($f, -mb_strlen($chunk, '8bit'), SEEK_CUR);
 | 
						|
 | 
						|
			// Decrease our line counter
 | 
						|
			$lines -= substr_count($chunk, "\n");
 | 
						|
		}
 | 
						|
 | 
						|
		// While we have too many lines
 | 
						|
		// (Because of buffer size we might have read too many)
 | 
						|
		while ($lines++ < 0) {
 | 
						|
 | 
						|
			// Find first newline and remove all text before that
 | 
						|
			$output = substr($output, strpos($output, "\n") + 1);
 | 
						|
		}
 | 
						|
 | 
						|
		// Close file and return
 | 
						|
		fclose($f);
 | 
						|
		return trim($output);
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Return first lines of log archive
 | 
						|
	 * 
 | 
						|
	 * @param string|null $filepath (optional) Path of file. Default is the log file.
 | 
						|
	 * @param int $lines (optional) Lines to be readed. Default value is 10.
 | 
						|
	 * @param bool $adaptive (optional) Default value is true.
 | 
						|
	 * @return false|string 
 | 
						|
	 * @since 2.0.0
 | 
						|
	 */
 | 
						|
	public function head($filepath = null, $lines = 10, $adaptive = true)
 | 
						|
	{
 | 
						|
 | 
						|
		// Open file
 | 
						|
		$f = @fopen(($filepath ?: $this->filepath), "rb");
 | 
						|
		if ($f === false) return false;
 | 
						|
 | 
						|
		// Sets buffer size
 | 
						|
		/* if (!$adaptive) 
 | 
						|
			$buffer = 4096;
 | 
						|
		else 
 | 
						|
			$buffer = ($lines < 2 ? 64 : ($lines < 10 ? 512 : 4096)); */
 | 
						|
 | 
						|
		// Start reading
 | 
						|
		$output = '';
 | 
						|
		while (($line = fgets($f)) !== false) {
 | 
						|
			if (feof($f)) break;
 | 
						|
			if ($lines-- == 0) break;
 | 
						|
			$output .= $line . "";
 | 
						|
		}
 | 
						|
 | 
						|
		// Close file and return
 | 
						|
		fclose($f);
 | 
						|
		return trim($output);
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * System is unusable.
 | 
						|
	 *
 | 
						|
	 * @param string $message
 | 
						|
	 * @param array  $context
 | 
						|
	 *
 | 
						|
	 * @return void
 | 
						|
	 */
 | 
						|
	public function emergency($message, array $context = array())
 | 
						|
	{
 | 
						|
		$this->log(self::EMERGENCY, $message, $context);
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Action must be taken immediately.
 | 
						|
	 *
 | 
						|
	 * Example: Entire website down, database unavailable, etc. This should
 | 
						|
	 * trigger the SMS alerts and wake you up.
 | 
						|
	 *
 | 
						|
	 * @param string $message
 | 
						|
	 * @param array  $context
 | 
						|
	 *
 | 
						|
	 * @return void
 | 
						|
	 */
 | 
						|
	public function alert($message, array $context = array())
 | 
						|
	{
 | 
						|
		$this->log(self::ALERT, $message, $context);
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Critical conditions.
 | 
						|
	 *
 | 
						|
	 * Example: Application component unavailable, unexpected exception.
 | 
						|
	 *
 | 
						|
	 * @param string $message
 | 
						|
	 * @param array  $context
 | 
						|
	 *
 | 
						|
	 * @return void
 | 
						|
	 */
 | 
						|
	public function critical($message, array $context = array())
 | 
						|
	{
 | 
						|
		$this->log(self::CRITICAL, $message, $context);
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Runtime errors that do not require immediate action but should typically
 | 
						|
	 * be logged and monitored.
 | 
						|
	 *
 | 
						|
	 * @param string $message
 | 
						|
	 * @param array  $context
 | 
						|
	 *
 | 
						|
	 * @return void
 | 
						|
	 */
 | 
						|
	public function error($message, array $context = array())
 | 
						|
	{
 | 
						|
		$this->log(self::ERROR, $message, $context);
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Exceptional occurrences that are not errors.
 | 
						|
	 *
 | 
						|
	 * Example: Use of deprecated APIs, poor use of an API, undesirable things
 | 
						|
	 * that are not necessarily wrong.
 | 
						|
	 *
 | 
						|
	 * @param string $message
 | 
						|
	 * @param array  $context
 | 
						|
	 *
 | 
						|
	 * @return void
 | 
						|
	 */
 | 
						|
	public function warning($message, array $context = array())
 | 
						|
	{
 | 
						|
		$this->log(self::WARNING, $message, $context);
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Normal but significant events.
 | 
						|
	 *
 | 
						|
	 * @param string $message
 | 
						|
	 * @param array  $context
 | 
						|
	 *
 | 
						|
	 * @return void
 | 
						|
	 */
 | 
						|
	public function notice($message, array $context = array())
 | 
						|
	{
 | 
						|
		$this->log(self::NOTICE, $message, $context);
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Interesting events.
 | 
						|
	 *
 | 
						|
	 * Example: User logs in, SQL logs.
 | 
						|
	 *
 | 
						|
	 * @param string $message
 | 
						|
	 * @param array  $context
 | 
						|
	 *
 | 
						|
	 * @return void
 | 
						|
	 */
 | 
						|
	public function info($message, array $context = array())
 | 
						|
	{
 | 
						|
		$this->log(self::INFO, $message, $context);
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Detailed debug information.
 | 
						|
	 *
 | 
						|
	 * @param string $message
 | 
						|
	 * @param array  $context
 | 
						|
	 *
 | 
						|
	 * @return void
 | 
						|
	 */
 | 
						|
	public function debug($message, array $context = array())
 | 
						|
	{
 | 
						|
		$this->log(self::DEBUG, $message, $context);
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * {@inheritdoc}
 | 
						|
	 */
 | 
						|
	public function log($level = self::INFO, $message = null, array $context = array()) {
 | 
						|
		$record = [
 | 
						|
			'message' => $message,
 | 
						|
			'context' => $context,
 | 
						|
			'level' => $level,
 | 
						|
			'level_name' => $level,
 | 
						|
			'channel' => str_replace(self::LOG_FILE_EXTENSION, '', $this->filename),
 | 
						|
			'datetime' => $this->getDateTime(),
 | 
						|
			'extra' => [],
 | 
						|
			'route' => \Phacil\Framework\startEngineExacTI::getRoute(),
 | 
						|
		];
 | 
						|
 | 
						|
		$trated = $this->logTreatment($record);
 | 
						|
		
 | 
						|
		$log = $this->interpolate($this->format, $trated);
 | 
						|
 | 
						|
		return $this->writeToFile($log);
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * @param array $record 
 | 
						|
	 * @return array 
 | 
						|
	 * @throws \RuntimeException 
 | 
						|
	 */
 | 
						|
	protected function logTreatment(array $record)
 | 
						|
	{
 | 
						|
		if (false === strpos($record['message'], '{')) {
 | 
						|
			return $record;
 | 
						|
		}
 | 
						|
 | 
						|
		try {
 | 
						|
			$replacements = [];
 | 
						|
			foreach ($record['context'] as $key => $val) {
 | 
						|
				$placeholder = '{' . $key . '}';
 | 
						|
				if (strpos($record['message'], $placeholder) === false) {
 | 
						|
					continue;
 | 
						|
				}
 | 
						|
 | 
						|
				if (is_null($val) || is_scalar($val) || (is_object($val) && method_exists($val, "__toString"))) {
 | 
						|
					$replacements[$placeholder] = $val;
 | 
						|
				} elseif ($val instanceof \DateTimeInterface) {
 | 
						|
					$replacements[$placeholder] = $val->format(self::DATE_TIME_FORMAT);
 | 
						|
				} elseif ($val instanceof \UnitEnum) {
 | 
						|
					$replacements[$placeholder] = $val instanceof \BackedEnum ? $val->value : $val->name;
 | 
						|
				} elseif (is_object($val)) {
 | 
						|
					$replacements[$placeholder] = '[object ' . \get_class($val) . ']';
 | 
						|
				} elseif (is_array($val)) {
 | 
						|
					$replacements[$placeholder] = 'array' . $this->toJson($val);
 | 
						|
				} else {
 | 
						|
					$replacements[$placeholder] = '[' . gettype($val) . ']';
 | 
						|
				}
 | 
						|
 | 
						|
				if ($this->removeUsedContextFields) {
 | 
						|
					unset($record['context'][$key]);
 | 
						|
				}
 | 
						|
			}
 | 
						|
 | 
						|
			$record['message'] = strtr($record['message'], $replacements);
 | 
						|
 | 
						|
		} catch (\Exception $th) {
 | 
						|
			//throw $th;
 | 
						|
			$this->warning($th->getMessage());
 | 
						|
		}
 | 
						|
		
 | 
						|
		return $record;
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * @param mixed $data
 | 
						|
	 */
 | 
						|
	protected function convertToString($data)
 | 
						|
	{
 | 
						|
		if (null === $data || is_bool($data)) {
 | 
						|
			return var_export($data, true);
 | 
						|
		}
 | 
						|
 | 
						|
		if (is_scalar($data)) {
 | 
						|
			return (string) $data;
 | 
						|
		}
 | 
						|
 | 
						|
		return $this->toJson($data);
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Return the JSON representation of a value
 | 
						|
	 *
 | 
						|
	 * @param  mixed             $data
 | 
						|
	 * @throws \RuntimeException if encoding fails and errors are not ignored
 | 
						|
	 * @return string            if encoding fails and ignoreErrors is true 'null' is returned
 | 
						|
	 */
 | 
						|
	protected function toJson($data)
 | 
						|
	{
 | 
						|
		return json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION | JSON_PARTIAL_OUTPUT_ON_ERROR);;
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * @param string $str 
 | 
						|
	 * @return string 
 | 
						|
	 * @throws \Phacil\Framework\Exception\RuntimeException 
 | 
						|
	 */
 | 
						|
	protected function replaceNewlines($str)
 | 
						|
	{
 | 
						|
		if ($this->allowInlineLineBreaks) {
 | 
						|
			if (0 === strpos($str, '{')) {
 | 
						|
				$str = preg_replace('/(?<!\\\\)\\\\[rn]/', "\n", $str);
 | 
						|
				if (null === $str) {
 | 
						|
					$pcreErrorCode = preg_last_error();
 | 
						|
					throw new \Phacil\Framework\Exception\RuntimeException('Failed to run preg_replace: ' . $pcreErrorCode );
 | 
						|
				}
 | 
						|
			}
 | 
						|
 | 
						|
			return $str;
 | 
						|
		}
 | 
						|
 | 
						|
		return str_replace(["\r\n", "\r", "\n"], ' ', $str);
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * @param mixed $value
 | 
						|
	 */
 | 
						|
	protected function stringify($value)
 | 
						|
	{
 | 
						|
		return $this->replaceNewlines($this->convertToString($value));
 | 
						|
	}
 | 
						|
 | 
						|
	protected function interpolate($format, $record)
 | 
						|
	{
 | 
						|
		$replace = [];
 | 
						|
 | 
						|
		foreach ($record as $key => $value) {
 | 
						|
			if (is_array($value)) {
 | 
						|
				$value = $this->convertToString($value);
 | 
						|
			}
 | 
						|
			$replace['%' . $key . '%'] = $value;
 | 
						|
		}
 | 
						|
 | 
						|
		return strtr($format, $replace);
 | 
						|
	}
 | 
						|
}
 | 
						|
 |