1<?php
2
3/*
4 * This file is part of Mustache.php.
5 *
6 * (c) 2010-2017 Justin Hileman
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12/**
13 * A Mustache Stream Logger.
14 *
15 * The Stream Logger wraps a file resource instance (such as a stream) or a
16 * stream URL. All log messages over the threshold level will be appended to
17 * this stream.
18 *
19 * Hint: Try `php://stderr` for your stream URL.
20 */
21class Mustache_Logger_StreamLogger extends Mustache_Logger_AbstractLogger
22{
23    protected static $levels = array(
24        self::DEBUG     => 100,
25        self::INFO      => 200,
26        self::NOTICE    => 250,
27        self::WARNING   => 300,
28        self::ERROR     => 400,
29        self::CRITICAL  => 500,
30        self::ALERT     => 550,
31        self::EMERGENCY => 600,
32    );
33
34    protected $level;
35    protected $stream = null;
36    protected $url    = null;
37
38    /**
39     * @throws InvalidArgumentException if the logging level is unknown
40     *
41     * @param resource|string $stream Resource instance or URL
42     * @param int             $level  The minimum logging level at which this handler will be triggered
43     */
44    public function __construct($stream, $level = Mustache_Logger::ERROR)
45    {
46        $this->setLevel($level);
47
48        if (is_resource($stream)) {
49            $this->stream = $stream;
50        } else {
51            $this->url = $stream;
52        }
53    }
54
55    /**
56     * Close stream resources.
57     */
58    public function __destruct()
59    {
60        if (is_resource($this->stream)) {
61            fclose($this->stream);
62        }
63    }
64
65    /**
66     * Set the minimum logging level.
67     *
68     * @throws Mustache_Exception_InvalidArgumentException if the logging level is unknown
69     *
70     * @param int $level The minimum logging level which will be written
71     */
72    public function setLevel($level)
73    {
74        if (!array_key_exists($level, self::$levels)) {
75            throw new Mustache_Exception_InvalidArgumentException(sprintf('Unexpected logging level: %s', $level));
76        }
77
78        $this->level = $level;
79    }
80
81    /**
82     * Get the current minimum logging level.
83     *
84     * @return int
85     */
86    public function getLevel()
87    {
88        return $this->level;
89    }
90
91    /**
92     * Logs with an arbitrary level.
93     *
94     * @throws Mustache_Exception_InvalidArgumentException if the logging level is unknown
95     *
96     * @param mixed  $level
97     * @param string $message
98     * @param array  $context
99     */
100    public function log($level, $message, array $context = array())
101    {
102        if (!array_key_exists($level, self::$levels)) {
103            throw new Mustache_Exception_InvalidArgumentException(sprintf('Unexpected logging level: %s', $level));
104        }
105
106        if (self::$levels[$level] >= self::$levels[$this->level]) {
107            $this->writeLog($level, $message, $context);
108        }
109    }
110
111    /**
112     * Write a record to the log.
113     *
114     * @throws Mustache_Exception_LogicException   If neither a stream resource nor url is present
115     * @throws Mustache_Exception_RuntimeException If the stream url cannot be opened
116     *
117     * @param int    $level   The logging level
118     * @param string $message The log message
119     * @param array  $context The log context
120     */
121    protected function writeLog($level, $message, array $context = array())
122    {
123        if (!is_resource($this->stream)) {
124            if (!isset($this->url)) {
125                throw new Mustache_Exception_LogicException('Missing stream url, the stream can not be opened. This may be caused by a premature call to close().');
126            }
127
128            $this->stream = fopen($this->url, 'a');
129            if (!is_resource($this->stream)) {
130                // @codeCoverageIgnoreStart
131                throw new Mustache_Exception_RuntimeException(sprintf('The stream or file "%s" could not be opened.', $this->url));
132                // @codeCoverageIgnoreEnd
133            }
134        }
135
136        fwrite($this->stream, self::formatLine($level, $message, $context));
137    }
138
139    /**
140     * Gets the name of the logging level.
141     *
142     * @throws InvalidArgumentException if the logging level is unknown
143     *
144     * @param int $level
145     *
146     * @return string
147     */
148    protected static function getLevelName($level)
149    {
150        return strtoupper($level);
151    }
152
153    /**
154     * Format a log line for output.
155     *
156     * @param int    $level   The logging level
157     * @param string $message The log message
158     * @param array  $context The log context
159     *
160     * @return string
161     */
162    protected static function formatLine($level, $message, array $context = array())
163    {
164        return sprintf(
165            "%s: %s\n",
166            self::getLevelName($level),
167            self::interpolateMessage($message, $context)
168        );
169    }
170
171    /**
172     * Interpolate context values into the message placeholders.
173     *
174     * @param string $message
175     * @param array  $context
176     *
177     * @return string
178     */
179    protected static function interpolateMessage($message, array $context = array())
180    {
181        if (strpos($message, '{') === false) {
182            return $message;
183        }
184
185        // build a replacement array with braces around the context keys
186        $replace = array();
187        foreach ($context as $key => $val) {
188            $replace['{' . $key . '}'] = $val;
189        }
190
191        // interpolate replacement values into the the message and return
192        return strtr($message, $replace);
193    }
194}
195