1 <?php
2
3 namespace AloFramework\Log;
4
5 use Psr\Log\LoggerInterface;
6 use Psr\Log\LogLevel;
7 use Psr\Log\LoggerTrait;
8 use Psr\Log\InvalidArgumentException;
9
10 /**
11 * AloFramework logger
12 * @author Art <a.molcanovas@gmail.com>
13 */
14 class Log extends LogLevel implements LoggerInterface {
15
16 use LoggerTrait;
17
18 /**
19 * Log identifier
20 * @var string
21 */
22 protected $label;
23
24 /**
25 * Where the logs are stored
26 * @var string
27 */
28 protected $savePath;
29
30 /**
31 * Log level set
32 * @var string
33 */
34 protected $level;
35
36 /**
37 * Log element separator
38 * @var string
39 */
40 private static $SEPARATOR = '|';
41
42 /**
43 * Log levels and their priorities
44 * @var array
45 */
46 private static $priority = [self::DEBUG => 1,
47 self::INFO => 2,
48 self::NOTICE => 3,
49 self::WARNING => 4,
50 self::ERROR => 5,
51 self::CRITICAL => 6,
52 self::ALERT => 7,
53 self::EMERGENCY => 8];
54
55 /**
56 * Constructor
57 * @author Art <a.molcanovas@gmail.com>
58 *
59 * @param string $logLevel Minimum log level to log. See class constants.
60 * @param string $label Log label, e.g. if you specify 'System' log entries will be prepended with SYSTEM
61 * @param string $savePath The file to save the logs in. If omitted, the logs directory in this package will
62 * be used with the filenames being today's date in YYYY-mm-dd format.
63 *
64 * @throws LogException When a save path is specified, but the directory does not exist
65 * @throws InvalidArgumentException When $log isn't scalar or $logLevel is invalid
66 */
67 function __construct($logLevel = self::DEBUG, $label = ALO_LOG_LABEL, $savePath = ALO_LOG_SAVE_PATH) {
68 $this->logLabel($label)->level($logLevel)->savePath($savePath);
69 }
70
71 /**
72 * Returns a string representation of the class
73 * @author Art <a.molcanovas@gmail.com>
74 * @return string
75 */
76 function __toString() {
77 ob_start();
78 d($this);
79
80 return ob_get_clean();
81 }
82
83 /**
84 * Gets or sets the log file path
85 * @author Art <a.molcanovas@gmail.com>
86 *
87 * @throws LogException When the directory doesn't exist
88 * @throws InvalidArgumentException When the path isn't a valid string
89 *
90 * @param string|null $path Omit if using as a getter; The path to the log file otherwise
91 *
92 * @return Log|string
93 */
94 function savePath($path = null) {
95 if ($path === null) {
96 return $this->savePath;
97 } elseif (!is_string($path)) {
98 throw new InvalidArgumentException('The path must be a string');
99 } else {
100 $dir = dirname($path);
101
102 if (!file_exists($dir)) {
103 throw new LogException('The directory does not exist: ' . $dir, LogException::E_INVALID_PATH);
104 } else {
105 $this->savePath = $path;
106 }
107 }
108
109 return $this;
110 }
111
112 /**
113 * Gets or sets the log label
114 * @author Art <a.molcanovas@gmail.com>
115 *
116 * @param string|null $set Omit if using as a getter, the new log label if using as a setter.
117 *
118 * @throws InvalidArgumentException If attempting to set an invalid log level
119 *
120 * @return Log|string
121 */
122 function logLabel($set = null) {
123 if ($set === null) {
124 return $this->label;
125 } elseif (!is_scalar($set)) {
126 throw new InvalidArgumentException('The log label must be scalar. You tried to set (serialised): ' .
127 serialize($set));
128 } else {
129 $this->label = strtoupper($set);
130 }
131
132 return $this;
133 }
134
135 /**
136 * Gets or sets the log level
137 * @author Art <a.molcanovas@gmail.com>
138 *
139 * @param string|null $set Omit if using as a getter, the log level if using as a setter - see class constants
140 * or refer to the PSR-3 standards.
141 *
142 * @throws InvalidArgumentException If attempting to set an invalid log level
143 *
144 * @return Log|string
145 */
146 function level($set = null) {
147 if ($set === null) {
148 return $this->level;
149 } elseif (!array_key_exists($set, self::$priority)) {
150 throw new InvalidArgumentException('An invalid log level was passed: ' .
151 (is_scalar($set) ? $set : serialize($set) . ' (serialised)'));
152 } else {
153 $this->level = $set;
154 }
155
156 return $this;
157 }
158
159 /**
160 * Logs with an arbitrary level.
161 *
162 * @param string $level The message's log level
163 * @param string $message The message
164 * @param array $context The message context/placeholder asociative array
165 *
166 * @throws InvalidArgumentException When the log level is invalid
167 *
168 * @return bool Whether the message has been written
169 */
170 function log($level, $message, array $context = []) {
171 if (!array_key_exists($level, self::$priority)) {
172 throw new InvalidArgumentException('Invalid log level supplied: ' .
173 (is_scalar($level) ? $level : serialize($level) . ' (serialised)'));
174 } elseif (self::$priority[$level] < self::$priority[$this->level]) {
175 return false;
176 } else {
177 return $this->doWrite($level, $this->replaceContext($message, $context));
178 }
179 }
180
181 /**
182 * Performs the actual log operation
183 * @author Art <a.molcanovas@gmail.com>
184 *
185 * @param string $level Log level
186 * @param string $message Raw message
187 *
188 * @return bool
189 */
190 private function doWrite($level, $message) {
191 $fp = fopen($this->savePath, 'ab');
192
193 if ($fp) {
194 $trace = debug_backtrace();
195 $trace = isset($trace[1]) ? $trace[1] : [];
196
197 $file = isset($trace['file']) ? implode(DIRECTORY_SEPARATOR,
198 array_slice(explode(DIRECTORY_SEPARATOR,
199 $trace['file']),
200 -2)) : '<<unknown file>>';
201 $line = isset($trace['line']) ? implode(DIRECTORY_SEPARATOR,
202 array_slice(explode(DIRECTORY_SEPARATOR,
203 $trace['line']),
204 -2)) : '<<unknown line>>';
205 $message =
206 $level .
207 ' ' .
208 self::$SEPARATOR .
209 ' ' .
210 date('Y-m-d H:i:s') .
211 ' ' .
212 self::$SEPARATOR .
213 ' ' .
214 $this->label .
215 ' ' .
216 self::$SEPARATOR .
217 ' ' .
218 str_replace(self::$SEPARATOR, '\\' . self::$SEPARATOR, $message) .
219 ' ' .
220 self::$SEPARATOR .
221 ' ' .
222 $file .
223 ' ' .
224 self::$SEPARATOR .
225 ' ' .
226 $line .
227 PHP_EOL;
228
229 $ok = [flock($fp, LOCK_EX),
230 fwrite($fp, $message),
231 flock($fp, LOCK_UN),
232 fclose($fp)];
233
234 return !in_array(false, $ok, true);
235 } else {
236 trigger_error('Failed to open log file ' . $this->savePath, E_USER_WARNING);
237 }
238
239 return false;
240 }
241
242 /**
243 * Replaces the placeholders in the message
244 *
245 * @param string $message The raw message
246 * @param array $context The context/placeholder assoc array
247 *
248 * @return string
249 */
250 protected static function replaceContext($message, array $context = []) {
251 if (empty($context)) {
252 return $message;
253 } else {
254 $search = $replace = [];
255
256 foreach ($context as $k => $v) {
257 $search[] = '{' . $k . '}';
258 $replace[] = $v;
259 }
260
261 return str_ireplace($search, $replace, $message);
262 }
263 }
264 }
265