1: <?php
2:
3: namespace GAubry\Logger;
4:
5: use GAubry\Helpers\Helpers;
6: use Psr\Log\LogLevel;
7:
8: /**
9: * PSR-3 logger for adding colors and indentation on PHP CLI output.
10: *
11: * Use tags and placeholder syntax to provide an easy way to color and indent PHP CLI output.
12: * PSR-3 compatibility allows graceful degradation when switching to another PSR-3 compliant logger.
13: * See README.md for more information.
14: *
15: * Copyright (c) 2013 Geoffroy Aubry <geoffroy.aubry@free.fr>
16: * Licensed under the GNU Lesser General Public License v3 (LGPL version 3).
17: *
18: * @copyright 2013 Geoffroy Aubry <geoffroy.aubry@free.fr>
19: * @license http://www.gnu.org/licenses/lgpl.html
20: * @see https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md
21: */
22: class ColoredIndentedLogger extends AbstractLogger
23: {
24: /**
25: * Length of the indent tag.
26: * @var int
27: * @see 'indent_tag' key of ColoredIndentedLogger::$aDefaultConfig
28: */
29: private $iIndentTagLength;
30:
31: /**
32: * Length of the unindent tag.
33: * @var int
34: * @see 'unindent_tag' key of ColoredIndentedLogger::$aDefaultConfig
35: */
36: private $iUnindentTagLength;
37:
38: /**
39: * Current zero-based indentation level.
40: * @var int
41: */
42: private $iIndentationLevel;
43:
44: /**
45: * Full length color tags combining prefix to user define colors.
46: * @var array
47: * @see ColoredIndentedLogger::buildColorTags()
48: * @see 'color_tag_prefix' key of ColoredIndentedLogger::$aDefaultConfig
49: */
50: private $aColorTags;
51:
52: /**
53: * Default configuration.
54: * – 'colors' => (array) Array of key/value pairs to associate bash color codes to color tags.
55: * Example: array(
56: * 'debug' => "\033[0;30m",
57: * 'warning' => "\033[0;33m",
58: * 'error' => "\033[1;31m"
59: * )
60: * – 'base_indentation' => (string) Describe what is a simple indentation, e.g. "\t".
61: * – 'indent_tag' => (string) Tag usable at the start or at the end of the message to add
62: * one or more indentation level.
63: * – 'unindent_tag' => (string) Tag usable at the start or at the end of the message to remove
64: * one or more indentation level.
65: * – 'min_message_level' => (string) Threshold required to log message, must be defined in \Psr\Log\LogLevel.
66: * – 'reset_color_sequence' => (string) Concatenated sequence at the end of message when colors are used.
67: * For example: "\033[0m".
68: * – 'color_tag_prefix' => (string) Prefix used in placeholders to distinguish standard context from colors.
69: *
70: * @var array
71: */
72: private static $aDefaultConfig = array(
73: 'colors' => array(),
74: 'base_indentation' => "\033[0;30m┆\033[0m ",
75: 'indent_tag' => '+++',
76: 'unindent_tag' => '---',
77: 'min_message_level' => LogLevel::DEBUG,
78: 'reset_color_sequence' => "\033[0m",
79: 'color_tag_prefix' => 'C.'
80: );
81:
82: /**
83: * Current configuration.
84: * @var array
85: * @see ColoredIndentedLogger::$aDefaultConfig
86: */
87: private $aConfig;
88:
89: /**
90: * Constructor.
91: *
92: * @param array $aConfig see self::$aDefaultConfig
93: * @throws \Psr\Log\InvalidArgumentException if calling this method with a level not defined in \Psr\Log\LogLevel
94: */
95: public function __construct (array $aConfig = array())
96: {
97: $this->aConfig = Helpers::arrayMergeRecursiveDistinct(self::$aDefaultConfig, $aConfig);
98: parent::__construct($this->aConfig['min_message_level']);
99:
100: $this->iIndentTagLength = strlen($this->aConfig['indent_tag']);
101: $this->iUnindentTagLength = strlen($this->aConfig['unindent_tag']);
102: $this->iIndentationLevel = 0;
103: $this->buildColorTags();
104: }
105:
106: /**
107: * Build full length color tags by adding prefix to user define colors.
108: * @see ColoredIndentedLogger::aColorTags
109: * @see 'color_tag_prefix' key of ColoredIndentedLogger::$aDefaultConfig
110: */
111: private function buildColorTags ()
112: {
113: $this->aColorTags = array();
114: foreach ($this->aConfig['colors'] as $sRawName => $sSequence) {
115: $sName = '{' . $this->aConfig['color_tag_prefix'] . $sRawName . '}';
116: $this->aColorTags[$sName] = $sSequence;
117: }
118: }
119:
120: /**
121: * Update indentation level according to leading indentation tags
122: * and remove them from the returned string.
123: *
124: * @param string $sMessage
125: * @return string specified message without any leading indentation tag
126: * @see ColoredIndentedLogger::iIndentationLevel
127: */
128: private function processLeadingIndentationTags ($sMessage)
129: {
130: $bTagFound = true;
131: while ($bTagFound && strlen($sMessage) > 0) {
132: if (substr($sMessage, 0, $this->iIndentTagLength) == $this->aConfig['indent_tag']) {
133: $this->iIndentationLevel++;
134: $sMessage = substr($sMessage, $this->iIndentTagLength);
135: } elseif (substr($sMessage, 0, $this->iUnindentTagLength) == $this->aConfig['unindent_tag']) {
136: $this->iIndentationLevel = max(0, $this->iIndentationLevel-1);
137: $sMessage = substr($sMessage, $this->iUnindentTagLength);
138: } else {
139: $bTagFound = false;
140: }
141: }
142: return $sMessage;
143: }
144:
145: /**
146: * Update indentation level according to trailing indentation tags
147: * and remove them from the returned string.
148: *
149: * @param string $sMessage
150: * @return string specified message without any trailing indentation tag
151: * @see ColoredIndentedLogger::iIndentationLevel
152: */
153: private function processTrailingIndentationTags ($sMessage)
154: {
155: $bTagFound = true;
156: while ($bTagFound && strlen($sMessage) > 0) {
157: if (substr($sMessage, -$this->iIndentTagLength) == $this->aConfig['indent_tag']) {
158: $this->iIndentationLevel++;
159: $sMessage = substr($sMessage, 0, -$this->iIndentTagLength);
160: } elseif (substr($sMessage, -$this->iUnindentTagLength) == $this->aConfig['unindent_tag']) {
161: $this->iIndentationLevel = max(0, $this->iIndentationLevel-1);
162: $sMessage = substr($sMessage, 0, -$this->iUnindentTagLength);
163: } else {
164: $bTagFound = false;
165: }
166: }
167: return $sMessage;
168: }
169:
170: /**
171: * Logs with an arbitrary level.
172: *
173: * Allows adjustment of the indentation whith multiple leading or trailing tags:
174: * see $this->sIndentTag and $this->sUnindentTag
175: *
176: * Allows insertion of bash colors via placeholders and context array.
177: *
178: * @param mixed $sMsgLevel message level, must be defined in \Psr\Log\LogLevel
179: * @param string $sMessage message with placeholders
180: * @param array $aContext context array
181: * @throws \Psr\Log\InvalidArgumentException if calling this method with a level not defined in \Psr\Log\LogLevel
182: */
183: public function log ($sMsgLevel, $sMessage, array $aContext = array())
184: {
185: $this->checkMsgLevel($sMsgLevel);
186: if (self::$aIntLevels[$sMsgLevel] >= $this->iMinMsgLevel) {
187: $sMessage = $this->processLeadingIndentationTags($sMessage);
188: $iCurrIndentationLvl = $this->iIndentationLevel;
189: $sMessage = $this->processTrailingIndentationTags($sMessage);
190:
191: if (strlen($sMessage) > 0) {
192: if (isset($this->aConfig['colors'][$sMsgLevel])
193: || isset($aContext[$this->aConfig['color_tag_prefix'] . $sMsgLevel])
194: ) {
195: $sImplicitColor = '{' . $this->aConfig['color_tag_prefix'] . $sMsgLevel . '}';
196: $sMessage = $sImplicitColor . $sMessage;
197: } else {
198: $iNbColorTags = preg_match_all('/{C.[A-Za-z0-9_.]+}/', $sMessage, $aMatches);
199: $sImplicitColor = '';
200: }
201: $sMessage = $this->interpolateContext($sMessage, $aContext);
202: $sIndent = str_repeat($this->aConfig['base_indentation'], $iCurrIndentationLvl);
203: $sMessage = $sIndent . str_replace("\n", "\n$sIndent$sImplicitColor", $sMessage);
204: $sMessage = strtr($sMessage, $this->aColorTags);
205: if ($sImplicitColor != ''
206: || (
207: $iNbColorTags > 0
208: && preg_match_all('/{C.[A-Za-z0-9_.]+}/', $sMessage, $aMatches) < $iNbColorTags
209: )
210: ) {
211: $sMessage .= $this->aConfig['reset_color_sequence'];
212: }
213:
214: echo $sMessage . PHP_EOL;
215: }
216: }
217: }
218: }
219: