diff --git a/composer.json b/composer.json index 0d66da1..a2b3913 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,8 @@ "ext-json": "*", "ext-simplexml": "*", "symfony/console": "~2.8|~3.4", - "psr/container": "^1.0" + "psr/container": "^1.0", + "psr/log": "^1.0 || ^2.0" }, "require-dev": { "yoast/phpunit-polyfills": "^0.2.0" diff --git a/src/xPDO/xPDO.php b/src/xPDO/xPDO.php index a5bfd22..f3a4774 100644 --- a/src/xPDO/xPDO.php +++ b/src/xPDO/xPDO.php @@ -20,6 +20,9 @@ use Composer\Autoload\ClassLoader; use Psr\Container\ContainerInterface; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; +use Psr\Log\LoggerInterface; use xPDO\Om\xPDOCriteria; use xPDO\Om\xPDOQuery; @@ -55,7 +58,10 @@ * * @package xpdo */ -class xPDO { +class xPDO implements LoggerAwareInterface +{ + use LoggerAwareTrait; + /**#@+ * Constants */ @@ -96,10 +102,25 @@ class xPDO { const OPT_VALIDATE_ON_SAVE = 'validate_on_save'; const OPT_VALIDATOR_CLASS = 'validator_class'; + /** + * @deprecated Prefer Psr\Log\LogLevel::EMERGENCY + */ const LOG_LEVEL_FATAL = 0; + /** + * @deprecated Prefer Psr\Log\LogLevel::ERROR + */ const LOG_LEVEL_ERROR = 1; + /** + * @deprecated Prefer Psr\Log\LogLevel::WARNING + */ const LOG_LEVEL_WARN = 2; + /** + * @deprecated Prefer Psr\Log\LogLevel::INFO + */ const LOG_LEVEL_INFO = 3; + /** + * @deprecated Prefer Psr\Log\LogLevel::DEBUG + */ const LOG_LEVEL_DEBUG = 4; const SCHEMA_VERSION = '3.0'; @@ -277,8 +298,20 @@ public function __construct($dsn, $username= '', $password= '', $options= array( if ($this->services === null) { $this->services = new xPDOContainer(); } - $this->setLogLevel($this->getOption('log_level', null, xPDO::LOG_LEVEL_FATAL, true)); - $this->setLogTarget($this->getOption('log_target', null, php_sapi_name() === 'cli' ? 'ECHO' : 'HTML', true)); + $this->logLevel = $this->getOption('log_level', null, xPDO::LOG_LEVEL_FATAL, true); + $this->logTarget = $this->getOption('log_target', null, php_sapi_name() === 'cli' ? 'ECHO' : 'HTML', true); + + if (!$this->services->has(LoggerInterface::class)) { + $this->services->add(LoggerInterface::class, function() { + return new xPDOLogger( + $this->getCacheManager(), + $this->logTarget, + $this->logLevel + ); + }); + } + $this->logger = $this->services->get(LoggerInterface::class); + if (!empty($dsn)) { $this->addConnection($dsn, $username, $password, $this->config, $driverOptions); } @@ -1964,15 +1997,23 @@ public function setDebug($v= true) { * * @param integer $level The logging level to switch to. * @return integer The previous log level. + * @deprecated To change the log settings, provide a new logger via setLogger(LoggerInterface $logger). Will be removed in v4. */ public function setLogLevel($level= xPDO::LOG_LEVEL_FATAL) { $oldLevel = $this->logLevel; - $this->logLevel= intval($level); + if ($this->logger instanceof xPDOLogger) { + $this->logger->setLogLevel($level); + $this->logLevel= intval($level); + } + else { + $this->logger->error('Trying to change the log level on a custom logger implementation of type ' . get_class($this->logger)); + } return $oldLevel; } /** * @return integer The current log level. + * @deprecated The log level is only available with the native xPDOLogger, not with custom loggers. Will be removed in v4. */ public function getLogLevel() { return $this->logLevel; @@ -1983,27 +2024,34 @@ public function getLogLevel() { * * Valid target values include: *
' . "\n" . print_r(debug_backtrace(), true) . "\n" . '' : '')); + + // If the target is different, adjust it + $previousLogger = $this->logger; + $oldTarget = $this->logTarget; + if (!empty($target) && $this->logger instanceof xPDOLogger) { + $this->logger->setLogTarget($target); } - if ($this->_debug === true || $level <= $this->logLevel) { - @ob_start(); - if (!empty ($def)) { - $def= " in {$def}"; - } - if (!empty ($file)) { - $file= " @ {$file}"; - } - if (!empty ($line)) { - $line= " : {$line}"; - } - switch ($target) { - case 'HTML' : - echo '
' . $msg . '' . "\n"; - break; - default : - echo '[' . strftime('%Y-%m-%d %H:%M:%S') . '] (' . $this->_getLogLevel($level) . $def . $file . $line . ') ' . $msg . "\n"; - } - $content= @ob_get_contents(); - @ob_end_clean(); - if ($target=='FILE' && $this->getCacheManager()) { - $filename = isset($targetOptions['filename']) ? $targetOptions['filename'] : 'error.log'; - $filepath = isset($targetOptions['filepath']) ? $targetOptions['filepath'] : $this->getCachePath() . Cache\xPDOCacheManager::LOG_DIR; - $this->cacheManager->writeFile($filepath . $filename, $content, 'a'); - } elseif ($target=='ARRAY' && isset($targetOptions['var']) && is_array($targetOptions['var'])) { - $targetOptions['var'][] = $content; - } elseif ($target=='ARRAY_EXTENDED' && isset($targetOptions['var']) && is_array($targetOptions['var'])) { - $targetOptions['var'][] = array( - 'content' => $content, - 'level' => $this->_getLogLevel($level), - 'msg' => $msg, - 'def' => $def, - 'file' => $file, - 'line' => $line - ); - } else { - echo $content; - } + + // Pass the message on to the PSR-3 logger + $this->logger->log($level, $msg, $context); + + // Restore the target if it was changed + if (!empty($oldTarget) && $this->logger instanceof xPDOLogger) { + $this->logger->setLogTarget($oldTarget); } } @@ -2770,4 +2782,9 @@ protected function sanitizePKCriteria($className, &$criteria) { } } } + + public function getLogger(): LoggerInterface + { + return $this->logger; + } } diff --git a/src/xPDO/xPDOLogger.php b/src/xPDO/xPDOLogger.php new file mode 100644 index 0000000..8633433 --- /dev/null +++ b/src/xPDO/xPDOLogger.php @@ -0,0 +1,329 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace xPDO; + +use Psr\Log\LoggerInterface; +use Psr\Log\LogLevel; + +/** + * Implements a minimal PSR-3 compatible logger if none are provided. + * + * @package xPDO + */ +class xPDOLogger implements LoggerInterface +{ + /** + * @var \xPDO\Cache\xPDOCacheManager + */ + protected $cacheManager; + /** + * @var array|string + */ + protected $target; + /** + * @var int|string + */ + protected $level; + + /** + * + * Valid target values include: + *
' . "\n" . print_r(debug_backtrace(), true) . "\n" . '' : '')); + } + + // Process into format: [timestamp] (SEVERITY) msg {context} + $content = ($target === 'HTML') + ? '
' . $message . "\n" . json_encode($context, JSON_PRETTY_PRINT) . '' . "\n" + : '[' . strftime('%Y-%m-%d %H:%M:%S') . '] (' . $def . ') ' . $message . ' ' . json_encode($context) . "\n"; + + if ($target === 'FILE') { + $filename = isset($targetOptions['filename']) ? $targetOptions['filename'] : 'error.log'; + $filepath = isset($targetOptions['filepath']) ? $targetOptions['filepath'] : $this->cacheManager->getCachePath() . Cache\xPDOCacheManager::LOG_DIR; + $this->cacheManager->writeFile($filepath . $filename, $content, 'a'); + } + elseif ($target === 'ARRAY' && isset($targetOptions['var']) && is_array($targetOptions['var'])) { + $targetOptions['var'][] = $content; + } + elseif ($target === 'ARRAY_EXTENDED' && isset($targetOptions['var']) && is_array($targetOptions['var'])) { + $targetOptions['var'][] = [ + 'content' => $content, + 'level' => strtoupper($level), + 'msg' => $message, + 'def' => $def, + ] + $context; + } + else { + echo $content; + } + } + + /** + * System is unusable. + * + * @param string|\Stringable $message + * @param mixed[] $context + * + * @return void + */ + public function emergency($message, array $context = []): void + { + $this->log(LogLevel::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|\Stringable $message + * @param mixed[] $context + * + * @return void + */ + public function alert($message, array $context = []): void + { + $this->log(LogLevel::ALERT, $message, $context); + } + + /** + * Critical conditions. + * + * Example: Application component unavailable, unexpected exception. + * + * @param string|\Stringable $message + * @param mixed[] $context + * + * @return void + */ + public function critical($message, array $context = []): void + { + $this->log(LogLevel::CRITICAL, $message, $context); + } + + /** + * Runtime errors that do not require immediate action but should typically + * be logged and monitored. + * + * @param string|\Stringable $message + * @param mixed[] $context + * + * @return void + */ + public function error($message, array $context = []): void + { + $this->log(LogLevel::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|\Stringable $message + * @param mixed[] $context + * + * @return void + */ + public function warning($message, array $context = []): void + { + $this->log(LogLevel::WARNING, $message, $context); + } + + /** + * Normal but significant events. + * + * @param string|\Stringable $message + * @param mixed[] $context + * + * @return void + */ + public function notice($message, array $context = []): void + { + $this->log(LogLevel::NOTICE, $message, $context); + } + + /** + * Interesting events. + * + * Example: User logs in, SQL logs. + * + * @param string|\Stringable $message + * @param mixed[] $context + * + * @return void + */ + public function info($message, array $context = []): void + { + $this->log(LogLevel::INFO, $message, $context); + } + + /** + * Detailed debug information. + * + * @param string|\Stringable $message + * @param mixed[] $context + * + * @return void + */ + public function debug($message, array $context = []): void + { + $this->log(LogLevel::DEBUG, $message, $context); + } + + private function translateLevel(int $level) + { + switch ($level) { + case xPDO::LOG_LEVEL_FATAL: + return LogLevel::EMERGENCY; + case xPDO::LOG_LEVEL_ERROR: + return LogLevel::ERROR; + case xPDO::LOG_LEVEL_WARN: + return LogLevel::WARNING; + case xPDO::LOG_LEVEL_INFO: + return LogLevel::INFO; + case xPDO::LOG_LEVEL_DEBUG: + return LogLevel::DEBUG; + } + + return $level; + } + + private function isHigherThanMinimum($level): bool + { + switch ($this->level) { + case LogLevel::EMERGENCY: + return $level === LogLevel::EMERGENCY; + case LogLevel::ALERT: + return in_array($level, [LogLevel::EMERGENCY, LogLevel::ALERT], true); + case LogLevel::CRITICAL: + return in_array($level, [LogLevel::EMERGENCY, LogLevel::ALERT, LogLevel::CRITICAL], true); + case LogLevel::ERROR: + return in_array($level, [LogLevel::EMERGENCY, LogLevel::ALERT, LogLevel::CRITICAL, LogLevel::ERROR], true); + case LogLevel::WARNING: + return in_array($level, [LogLevel::EMERGENCY, LogLevel::ALERT, LogLevel::CRITICAL, LogLevel::ERROR, LogLevel::WARNING], true); + case LogLevel::NOTICE: + return in_array($level, [LogLevel::EMERGENCY, LogLevel::ALERT, LogLevel::CRITICAL, LogLevel::ERROR, LogLevel::WARNING, LogLevel::NOTICE], true); + case LogLevel::INFO: + return in_array($level, [LogLevel::EMERGENCY, LogLevel::ALERT, LogLevel::CRITICAL, LogLevel::ERROR, LogLevel::WARNING, LogLevel::NOTICE, LogLevel::INFO], true); + case LogLevel::DEBUG: + return in_array($level, [LogLevel::EMERGENCY, LogLevel::ALERT, LogLevel::CRITICAL, LogLevel::ERROR, LogLevel::WARNING, LogLevel::NOTICE, LogLevel::INFO, LogLevel::DEBUG], true); + default: + // If the level is unrecognised, always log everything. + return true; + } + } + + public function getLogTarget() + { + return $this->target; + } + + public function setLogTarget($target) + { + $this->target = $target; + } + + public function getLogLevel() + { + return $this->target; + } + + public function setLogLevel($level) + { + $this->level = is_int($level) ? $this->translateLevel($level) : $level; + } +}