<?php
/**
 * Zend Framework (http://framework.zend.com/)
 *
 * @link      http://github.com/zendframework/zf2 for the canonical source repository
 * @copyright Copyright (c) 2005-2013 Zend Technologies USA Inc. (http://www.zend.com)
 * @license   http://framework.zend.com/license/new-bsd New BSD License
 */

namespace Zend\Log;

use 
DateTime;
use 
ErrorException;
use 
Traversable;
use 
Zend\Stdlib\ArrayUtils;
use 
Zend\Stdlib\SplPriorityQueue;

/**
 * Logging messages with a stack of backends
 */
class Logger implements LoggerInterface
{
    
/**
     * @const int defined from the BSD Syslog message severities
     * @link http://tools.ietf.org/html/rfc3164
     */
    
const EMERG  0;
    const 
ALERT  1;
    const 
CRIT   2;
    const 
ERR    3;
    const 
WARN   4;
    const 
NOTICE 5;
    const 
INFO   6;
    const 
DEBUG  7;

    
/**
     * Map native PHP errors to priority
     *
     * @var array
     */
    
public static $errorPriorityMap = array(
        
E_NOTICE            => self::NOTICE,
        
E_USER_NOTICE       => self::NOTICE,
        
E_WARNING           => self::WARN,
        
E_CORE_WARNING      => self::WARN,
        
E_USER_WARNING      => self::WARN,
        
E_ERROR             => self::ERR,
        
E_USER_ERROR        => self::ERR,
        
E_CORE_ERROR        => self::ERR,
        
E_RECOVERABLE_ERROR => self::ERR,
        
E_STRICT            => self::DEBUG,
        
E_DEPRECATED        => self::DEBUG,
        
E_USER_DEPRECATED   => self::DEBUG,
    );

    
/**
     * Registered error handler
     *
     * @var bool
     */
    
protected static $registeredErrorHandler false;

    
/**
     * Registered exception handler
     *
     * @var bool
     */
    
protected static $registeredExceptionHandler false;

    
/**
     * List of priority code => priority (short) name
     *
     * @var array
     */
    
protected $priorities = array(
        
self::EMERG  => 'EMERG',
        
self::ALERT  => 'ALERT',
        
self::CRIT   => 'CRIT',
        
self::ERR    => 'ERR',
        
self::WARN   => 'WARN',
        
self::NOTICE => 'NOTICE',
        
self::INFO   => 'INFO',
        
self::DEBUG  => 'DEBUG',
    );

    
/**
     * Writers
     *
     * @var SplPriorityQueue
     */
    
protected $writers;

    
/**
     * Processors
     *
     * @var SplPriorityQueue
     */
    
protected $processors;

    
/**
     * Writer plugins
     *
     * @var WriterPluginManager
     */
    
protected $writerPlugins;

    
/**
     * Processor plugins
     *
     * @var ProcessorPluginManager
     */
    
protected $processorPlugins;

    
/**
     * Constructor
     *
     * Set options for an logger. Accepted options are:
     * - writers: array of writers to add to this logger
     * - exceptionhandler: if true register this logger as exceptionhandler
     * - errorhandler: if true register this logger as errorhandler
     *
     * @param  array|Traversable $options
     * @return Logger
     * @throws Exception\InvalidArgumentException
     */
    
public function __construct(array $options null)
    {
        
$this->writers = new SplPriorityQueue();

        if (
$options instanceof Traversable) {
            
$options ArrayUtils::iteratorToArray($options);
        }

        if (
is_array($options)) {
            if (isset(
$options['writers']) && is_array($options['writers'])) {
                foreach (
$options['writers'] as $writer) {

                    if (!isset(
$writer['name'])) {
                        throw new 
Exception\InvalidArgumentException('Options must contain a name for the writer');
                    }

                    
$priority      = (isset($writer['priority'])) ? $writer['priority'] : null;
                    
$writerOptions = (isset($writer['options'])) ? $writer['options'] : null;

                    
$this->addWriter($writer['name'], $priority$writerOptions);
                }
            }

            if (isset(
$options['exceptionhandler']) && $options['exceptionhandler'] === true) {
                
self::registerExceptionHandler($this);
            }

            if (isset(
$options['errorhandler']) && $options['errorhandler'] === true) {
                
self::registerErrorHandler($this);
            }

        }

        
$this->processors = new SplPriorityQueue();
    }

    
/**
     * Shutdown all writers
     *
     * @return void
     */
    
public function __destruct()
    {
        foreach (
$this->writers as $writer) {
            try {
                
$writer->shutdown();
            } catch (\
Exception $e) {}
        }
    }

    
/**
     * Get writer plugin manager
     *
     * @return WriterPluginManager
     */
    
public function getWriterPluginManager()
    {
        if (
null === $this->writerPlugins) {
            
$this->setWriterPluginManager(new WriterPluginManager());
        }
        return 
$this->writerPlugins;
    }

    
/**
     * Set writer plugin manager
     *
     * @param  string|WriterPluginManager $plugins
     * @return Logger
     * @throws Exception\InvalidArgumentException
     */
    
public function setWriterPluginManager($plugins)
    {
        if (
is_string($plugins)) {
            
$plugins = new $plugins;
        }
        if (!
$plugins instanceof WriterPluginManager) {
            throw new 
Exception\InvalidArgumentException(sprintf(
                
'Writer plugin manager must extend %s\WriterPluginManager; received %s',
                
__NAMESPACE__,
                
is_object($plugins) ? get_class($plugins) : gettype($plugins)
            ));
        }

        
$this->writerPlugins $plugins;
        return 
$this;
    }

    
/**
     * Get writer instance
     *
     * @param string $name
     * @param array|null $options
     * @return Writer\WriterInterface
     */
    
public function writerPlugin($name, array $options null)
    {
        return 
$this->getWriterPluginManager()->get($name$options);
    }

    
/**
     * Add a writer to a logger
     *
     * @param  string|Writer\WriterInterface $writer
     * @param  int $priority
     * @param  array|null $options
     * @return Logger
     * @throws Exception\InvalidArgumentException
     */
    
public function addWriter($writer$priority 1, array $options null)
    {
        if (
is_string($writer)) {
            
$writer $this->writerPlugin($writer$options);
        } elseif (!
$writer instanceof Writer\WriterInterface) {
            throw new 
Exception\InvalidArgumentException(sprintf(
                
'Writer must implement %s\Writer\WriterInterface; received "%s"',
                
__NAMESPACE__,
                
is_object($writer) ? get_class($writer) : gettype($writer)
            ));
        }
        
$this->writers->insert($writer$priority);

        return 
$this;
    }

    
/**
     * Get writers
     *
     * @return SplPriorityQueue
     */
    
public function getWriters()
    {
        return 
$this->writers;
    }

    
/**
     * Set the writers
     *
     * @param  SplPriorityQueue $writers
     * @return Logger
     * @throws Exception\InvalidArgumentException
     */
    
public function setWriters(SplPriorityQueue $writers)
    {
        foreach (
$writers->toArray() as $writer) {
            if (!
$writer instanceof Writer\WriterInterface) {
                throw new 
Exception\InvalidArgumentException('Writers must be a SplPriorityQueue of Zend\Log\Writer');
            }
        }
        
$this->writers $writers;
        return 
$this;
    }

    
/**
     * Get processor plugin manager
     *
     * @return ProcessorPluginManager
     */
    
public function getProcessorPluginManager()
    {
        if (
null === $this->processorPlugins) {
            
$this->setProcessorPluginManager(new ProcessorPluginManager());
        }
        return 
$this->processorPlugins;
    }

    
/**
     * Set processor plugin manager
     *
     * @param  string|ProcessorPluginManager $plugins
     * @return Logger
     * @throws Exception\InvalidArgumentException
     */
    
public function setProcessorPluginManager($plugins)
    {
        if (
is_string($plugins)) {
            
$plugins = new $plugins;
        }
        if (!
$plugins instanceof ProcessorPluginManager) {
            throw new 
Exception\InvalidArgumentException(sprintf(
                    
'processor plugin manager must extend %s\ProcessorPluginManager; received %s',
                    
__NAMESPACE__,
                    
is_object($plugins) ? get_class($plugins) : gettype($plugins)
            ));
        }

        
$this->processorPlugins $plugins;
        return 
$this;
    }

    
/**
     * Get processor instance
     *
     * @param string $name
     * @param array|null $options
     * @return Processor\ProcessorInterface
     */
    
public function processorPlugin($name, array $options null)
    {
        return 
$this->getProcessorPluginManager()->get($name$options);
    }

    
/**
     * Add a processor to a logger
     *
     * @param  string|Processor\ProcessorInterface $processor
     * @param  int $priority
     * @param  array|null $options
     * @return Logger
     * @throws Exception\InvalidArgumentException
     */
    
public function addProcessor($processor$priority 1, array $options null)
    {
        if (
is_string($processor)) {
            
$processor $this->processorPlugin($processor$options);
        } elseif (!
$processor instanceof Processor\ProcessorInterface) {
            throw new 
Exception\InvalidArgumentException(sprintf(
                    
'Processor must implement Zend\Log\ProcessorInterface; received "%s"',
                    
is_object($processor) ? get_class($processor) : gettype($processor)
            ));
        }
        
$this->processors->insert($processor$priority);

        return 
$this;
    }

    
/**
     * Get processors
     *
     * @return SplPriorityQueue
     */
    
public function getProcessors()
    {
        return 
$this->processors;
    }

    
/**
     * Add a message as a log entry
     *
     * @param  int $priority
     * @param  mixed $message
     * @param  array|Traversable $extra
     * @return Logger
     * @throws Exception\InvalidArgumentException if message can't be cast to string
     * @throws Exception\InvalidArgumentException if extra can't be iterated over
     * @throws Exception\RuntimeException if no log writer specified
     */
    
public function log($priority$message$extra = array())
    {
        if (!
is_int($priority) || ($priority<0) || ($priority>=count($this->priorities))) {
            throw new 
Exception\InvalidArgumentException(sprintf(
                
'$priority must be an integer > 0 and < %d; received %s',
                
count($this->priorities),
                
var_export($priority1)
            ));
        }
        if (
is_object($message) && !method_exists($message'__toString')) {
            throw new 
Exception\InvalidArgumentException(
                
'$message must implement magic __toString() method'
            
);
        }

        if (!
is_array($extra) && !$extra instanceof Traversable) {
            throw new 
Exception\InvalidArgumentException(
                
'$extra must be an array or implement Traversable'
            
);
        } elseif (
$extra instanceof Traversable) {
            
$extra ArrayUtils::iteratorToArray($extra);
        }

        if (
$this->writers->count() === 0) {
            throw new 
Exception\RuntimeException('No log writer specified');
        }

        
$timestamp = new DateTime();

        if (
is_array($message)) {
            
$message var_export($messagetrue);
        }

        
$event = array(
            
'timestamp'    => $timestamp,
            
'priority'     => (int) $priority,
            
'priorityName' => $this->priorities[$priority],
            
'message'      => (string) $message,
            
'extra'        => $extra
        
);

        foreach (
$this->processors->toArray() as $processor) {
            
$event $processor->process($event);
        }

        foreach (
$this->writers->toArray() as $writer) {
            
$writer->write($event);
        }

        return 
$this;
    }

    
/**
     * @param string $message
     * @param array|Traversable $extra
     * @return Logger
     */
    
public function emerg($message$extra = array())
    {
        return 
$this->log(self::EMERG$message$extra);
    }

    
/**
     * @param string $message
     * @param array|Traversable $extra
     * @return Logger
     */
    
public function alert($message$extra = array())
    {
        return 
$this->log(self::ALERT$message$extra);
    }

    
/**
     * @param string $message
     * @param array|Traversable $extra
     * @return Logger
     */
    
public function crit($message$extra = array())
    {
        return 
$this->log(self::CRIT$message$extra);
    }

    
/**
     * @param string $message
     * @param array|Traversable $extra
     * @return Logger
     */
    
public function err($message$extra = array())
    {
        return 
$this->log(self::ERR$message$extra);
    }

    
/**
     * @param string $message
     * @param array|Traversable $extra
     * @return Logger
     */
    
public function warn($message$extra = array())
    {
        return 
$this->log(self::WARN$message$extra);
    }

    
/**
     * @param string $message
     * @param array|Traversable $extra
     * @return Logger
     */
    
public function notice($message$extra = array())
    {
        return 
$this->log(self::NOTICE$message$extra);
    }

    
/**
     * @param string $message
     * @param array|Traversable $extra
     * @return Logger
     */
    
public function info($message$extra = array())
    {
        return 
$this->log(self::INFO$message$extra);
    }

    
/**
     * @param string $message
     * @param array|Traversable $extra
     * @return Logger
     */
    
public function debug($message$extra = array())
    {
        return 
$this->log(self::DEBUG$message$extra);
    }

    
/**
     * Register logging system as an error handler to log PHP errors
     *
     * @link http://www.php.net/manual/function.set-error-handler.php
     * @param  Logger $logger
     * @param  bool   $continueNativeHandler
     * @return mixed  Returns result of set_error_handler
     * @throws Exception\InvalidArgumentException if logger is null
     */
    
public static function registerErrorHandler(Logger $logger$continueNativeHandler false)
    {
        
// Only register once per instance
        
if (static::$registeredErrorHandler) {
            return 
false;
        }

        
$errorHandlerMap = static::$errorPriorityMap;

        
$previous set_error_handler(function ($level$message$file$line$context)
            use (
$logger$errorHandlerMap$continueNativeHandler)
        {
            
$iniLevel error_reporting();

            if (
$iniLevel $level) {
                if (isset(
Logger::$errorPriorityMap[$level])) {
                    
$priority $errorHandlerMap[$level];
                } else {
                    
$priority Logger::INFO;
                }
                
$logger->log($priority$message, array(
                    
'errno'   => $level,
                    
'file'    => $file,
                    
'line'    => $line,
                    
'context' => $context,
                ));
            }

            return !
$continueNativeHandler;
        });

        static::
$registeredErrorHandler true;
        return 
$previous;
    }

    
/**
     * Unregister error handler
     *
     */
    
public static function unregisterErrorHandler()
    {
        
restore_error_handler();
        static::
$registeredErrorHandler false;
    }

    
/**
     * Register logging system as an exception handler to log PHP exceptions
     *
     * @link http://www.php.net/manual/en/function.set-exception-handler.php
     * @param Logger $logger
     * @return bool
     * @throws Exception\InvalidArgumentException if logger is null
     */
    
public static function registerExceptionHandler(Logger $logger)
    {
        
// Only register once per instance
        
if (static::$registeredExceptionHandler) {
            return 
false;
        }

        if (
$logger === null) {
            throw new 
Exception\InvalidArgumentException('Invalid Logger specified');
        }

        
$errorPriorityMap = static::$errorPriorityMap;

        
set_exception_handler(function ($exception) use ($logger$errorPriorityMap) {
            
$logMessages = array();

            do {
                
$priority Logger::ERR;
                if (
$exception instanceof ErrorException && isset($errorPriorityMap[$exception->getSeverity()])) {
                    
$priority $errorPriorityMap[$exception->getSeverity()];
                }

                
$extra = array(
                    
'file'  => $exception->getFile(),
                    
'line'  => $exception->getLine(),
                    
'trace' => $exception->getTrace(),
                );
                if (isset(
$exception->xdebug_message)) {
                    
$extra['xdebug'] = $exception->xdebug_message;
                }

                
$logMessages[] = array(
                    
'priority' => $priority,
                    
'message'  => $exception->getMessage(),
                    
'extra'    => $extra,
                );
                
$exception $exception->getPrevious();
            } while (
$exception);

            foreach (
array_reverse($logMessages) as $logMessage) {
                
$logger->log($logMessage['priority'], $logMessage['message'], $logMessage['extra']);
            }
        });

        static::
$registeredExceptionHandler true;
        return 
true;
    }

    
/**
     * Unregister exception handler
     */
    
public static function unregisterExceptionHandler()
    {
        
restore_exception_handler();
        static::
$registeredExceptionHandler false;
    }
}