PHPConsoleHandler.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. <?php declare(strict_types=1);
  2. /*
  3. * This file is part of the Monolog package.
  4. *
  5. * (c) Jordi Boggiano <j.boggiano@seld.be>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Monolog\Handler;
  11. use Monolog\Formatter\LineFormatter;
  12. use Monolog\Formatter\FormatterInterface;
  13. use Monolog\Level;
  14. use Monolog\Utils;
  15. use PhpConsole\Connector;
  16. use PhpConsole\Handler as VendorPhpConsoleHandler;
  17. use PhpConsole\Helper;
  18. use Monolog\LogRecord;
  19. use PhpConsole\Storage;
  20. /**
  21. * Monolog handler for Google Chrome extension "PHP Console"
  22. *
  23. * Display PHP error/debug log messages in Google Chrome console and notification popups, executes PHP code remotely
  24. *
  25. * Usage:
  26. * 1. Install Google Chrome extension [now dead and removed from the chrome store]
  27. * 2. See overview https://github.com/barbushin/php-console#overview
  28. * 3. Install PHP Console library https://github.com/barbushin/php-console#installation
  29. * 4. Example (result will looks like http://i.hizliresim.com/vg3Pz4.png)
  30. *
  31. * $logger = new \Monolog\Logger('all', array(new \Monolog\Handler\PHPConsoleHandler()));
  32. * \Monolog\ErrorHandler::register($logger);
  33. * echo $undefinedVar;
  34. * $logger->debug('SELECT * FROM users', array('db', 'time' => 0.012));
  35. * PC::debug($_SERVER); // PHP Console debugger for any type of vars
  36. *
  37. * @author Sergey Barbushin https://www.linkedin.com/in/barbushin
  38. * @phpstan-type Options array{
  39. * enabled: bool,
  40. * classesPartialsTraceIgnore: string[],
  41. * debugTagsKeysInContext: array<int|string>,
  42. * useOwnErrorsHandler: bool,
  43. * useOwnExceptionsHandler: bool,
  44. * sourcesBasePath: string|null,
  45. * registerHelper: bool,
  46. * serverEncoding: string|null,
  47. * headersLimit: int|null,
  48. * password: string|null,
  49. * enableSslOnlyMode: bool,
  50. * ipMasks: string[],
  51. * enableEvalListener: bool,
  52. * dumperDetectCallbacks: bool,
  53. * dumperLevelLimit: int,
  54. * dumperItemsCountLimit: int,
  55. * dumperItemSizeLimit: int,
  56. * dumperDumpSizeLimit: int,
  57. * detectDumpTraceAndSource: bool,
  58. * dataStorage: Storage|null
  59. * }
  60. * @phpstan-type InputOptions array{
  61. * enabled?: bool,
  62. * classesPartialsTraceIgnore?: string[],
  63. * debugTagsKeysInContext?: array<int|string>,
  64. * useOwnErrorsHandler?: bool,
  65. * useOwnExceptionsHandler?: bool,
  66. * sourcesBasePath?: string|null,
  67. * registerHelper?: bool,
  68. * serverEncoding?: string|null,
  69. * headersLimit?: int|null,
  70. * password?: string|null,
  71. * enableSslOnlyMode?: bool,
  72. * ipMasks?: string[],
  73. * enableEvalListener?: bool,
  74. * dumperDetectCallbacks?: bool,
  75. * dumperLevelLimit?: int,
  76. * dumperItemsCountLimit?: int,
  77. * dumperItemSizeLimit?: int,
  78. * dumperDumpSizeLimit?: int,
  79. * detectDumpTraceAndSource?: bool,
  80. * dataStorage?: Storage|null
  81. * }
  82. *
  83. * @deprecated Since 2.8.0 and 3.2.0, PHPConsole is abandoned and thus we will drop this handler in Monolog 4
  84. */
  85. class PHPConsoleHandler extends AbstractProcessingHandler
  86. {
  87. /**
  88. * @phpstan-var Options
  89. */
  90. private array $options = [
  91. 'enabled' => true, // bool Is PHP Console server enabled
  92. 'classesPartialsTraceIgnore' => ['Monolog\\'], // array Hide calls of classes started with...
  93. 'debugTagsKeysInContext' => [0, 'tag'], // bool Is PHP Console server enabled
  94. 'useOwnErrorsHandler' => false, // bool Enable errors handling
  95. 'useOwnExceptionsHandler' => false, // bool Enable exceptions handling
  96. 'sourcesBasePath' => null, // string Base path of all project sources to strip in errors source paths
  97. 'registerHelper' => true, // bool Register PhpConsole\Helper that allows short debug calls like PC::debug($var, 'ta.g.s')
  98. 'serverEncoding' => null, // string|null Server internal encoding
  99. 'headersLimit' => null, // int|null Set headers size limit for your web-server
  100. 'password' => null, // string|null Protect PHP Console connection by password
  101. 'enableSslOnlyMode' => false, // bool Force connection by SSL for clients with PHP Console installed
  102. 'ipMasks' => [], // array Set IP masks of clients that will be allowed to connect to PHP Console: array('192.168.*.*', '127.0.0.1')
  103. 'enableEvalListener' => false, // bool Enable eval request to be handled by eval dispatcher(if enabled, 'password' option is also required)
  104. 'dumperDetectCallbacks' => false, // bool Convert callback items in dumper vars to (callback SomeClass::someMethod) strings
  105. 'dumperLevelLimit' => 5, // int Maximum dumped vars array or object nested dump level
  106. 'dumperItemsCountLimit' => 100, // int Maximum dumped var same level array items or object properties number
  107. 'dumperItemSizeLimit' => 5000, // int Maximum length of any string or dumped array item
  108. 'dumperDumpSizeLimit' => 500000, // int Maximum approximate size of dumped vars result formatted in JSON
  109. 'detectDumpTraceAndSource' => false, // bool Autodetect and append trace data to debug
  110. 'dataStorage' => null, // \PhpConsole\Storage|null Fixes problem with custom $_SESSION handler(see http://goo.gl/Ne8juJ)
  111. ];
  112. private Connector $connector;
  113. /**
  114. * @param array<string, mixed> $options See \Monolog\Handler\PHPConsoleHandler::$options for more details
  115. * @param Connector|null $connector Instance of \PhpConsole\Connector class (optional)
  116. * @throws \RuntimeException
  117. * @phpstan-param InputOptions $options
  118. */
  119. public function __construct(array $options = [], ?Connector $connector = null, int|string|Level $level = Level::Debug, bool $bubble = true)
  120. {
  121. if (!class_exists('PhpConsole\Connector')) {
  122. throw new \RuntimeException('PHP Console library not found. See https://github.com/barbushin/php-console#installation');
  123. }
  124. parent::__construct($level, $bubble);
  125. $this->options = $this->initOptions($options);
  126. $this->connector = $this->initConnector($connector);
  127. }
  128. /**
  129. * @param array<string, mixed> $options
  130. * @return array<string, mixed>
  131. *
  132. * @phpstan-param InputOptions $options
  133. * @phpstan-return Options
  134. */
  135. private function initOptions(array $options): array
  136. {
  137. $wrongOptions = array_diff(array_keys($options), array_keys($this->options));
  138. if (\count($wrongOptions) > 0) {
  139. throw new \RuntimeException('Unknown options: ' . implode(', ', $wrongOptions));
  140. }
  141. return array_replace($this->options, $options);
  142. }
  143. private function initConnector(?Connector $connector = null): Connector
  144. {
  145. if (null === $connector) {
  146. if ($this->options['dataStorage'] instanceof Storage) {
  147. Connector::setPostponeStorage($this->options['dataStorage']);
  148. }
  149. $connector = Connector::getInstance();
  150. }
  151. if ($this->options['registerHelper'] && !Helper::isRegistered()) {
  152. Helper::register();
  153. }
  154. if ($this->options['enabled'] && $connector->isActiveClient()) {
  155. if ($this->options['useOwnErrorsHandler'] || $this->options['useOwnExceptionsHandler']) {
  156. $handler = VendorPhpConsoleHandler::getInstance();
  157. $handler->setHandleErrors($this->options['useOwnErrorsHandler']);
  158. $handler->setHandleExceptions($this->options['useOwnExceptionsHandler']);
  159. $handler->start();
  160. }
  161. if (null !== $this->options['sourcesBasePath']) {
  162. $connector->setSourcesBasePath($this->options['sourcesBasePath']);
  163. }
  164. if (null !== $this->options['serverEncoding']) {
  165. $connector->setServerEncoding($this->options['serverEncoding']);
  166. }
  167. if (null !== $this->options['password']) {
  168. $connector->setPassword($this->options['password']);
  169. }
  170. if ($this->options['enableSslOnlyMode']) {
  171. $connector->enableSslOnlyMode();
  172. }
  173. if (\count($this->options['ipMasks']) > 0) {
  174. $connector->setAllowedIpMasks($this->options['ipMasks']);
  175. }
  176. if (null !== $this->options['headersLimit'] && $this->options['headersLimit'] > 0) {
  177. $connector->setHeadersLimit($this->options['headersLimit']);
  178. }
  179. if ($this->options['detectDumpTraceAndSource']) {
  180. $connector->getDebugDispatcher()->detectTraceAndSource = true;
  181. }
  182. $dumper = $connector->getDumper();
  183. $dumper->levelLimit = $this->options['dumperLevelLimit'];
  184. $dumper->itemsCountLimit = $this->options['dumperItemsCountLimit'];
  185. $dumper->itemSizeLimit = $this->options['dumperItemSizeLimit'];
  186. $dumper->dumpSizeLimit = $this->options['dumperDumpSizeLimit'];
  187. $dumper->detectCallbacks = $this->options['dumperDetectCallbacks'];
  188. if ($this->options['enableEvalListener']) {
  189. $connector->startEvalRequestsListener();
  190. }
  191. }
  192. return $connector;
  193. }
  194. public function getConnector(): Connector
  195. {
  196. return $this->connector;
  197. }
  198. /**
  199. * @return array<string, mixed>
  200. */
  201. public function getOptions(): array
  202. {
  203. return $this->options;
  204. }
  205. public function handle(LogRecord $record): bool
  206. {
  207. if ($this->options['enabled'] && $this->connector->isActiveClient()) {
  208. return parent::handle($record);
  209. }
  210. return !$this->bubble;
  211. }
  212. /**
  213. * Writes the record down to the log of the implementing handler
  214. */
  215. protected function write(LogRecord $record): void
  216. {
  217. if ($record->level->isLowerThan(Level::Notice)) {
  218. $this->handleDebugRecord($record);
  219. } elseif (isset($record->context['exception']) && $record->context['exception'] instanceof \Throwable) {
  220. $this->handleExceptionRecord($record);
  221. } else {
  222. $this->handleErrorRecord($record);
  223. }
  224. }
  225. private function handleDebugRecord(LogRecord $record): void
  226. {
  227. [$tags, $filteredContext] = $this->getRecordTags($record);
  228. $message = $record->message;
  229. if (\count($filteredContext) > 0) {
  230. $message .= ' ' . Utils::jsonEncode($this->connector->getDumper()->dump(array_filter($filteredContext)), null, true);
  231. }
  232. $this->connector->getDebugDispatcher()->dispatchDebug($message, $tags, $this->options['classesPartialsTraceIgnore']);
  233. }
  234. private function handleExceptionRecord(LogRecord $record): void
  235. {
  236. $this->connector->getErrorsDispatcher()->dispatchException($record->context['exception']);
  237. }
  238. private function handleErrorRecord(LogRecord $record): void
  239. {
  240. $context = $record->context;
  241. $this->connector->getErrorsDispatcher()->dispatchError(
  242. $context['code'] ?? null,
  243. $context['message'] ?? $record->message,
  244. $context['file'] ?? null,
  245. $context['line'] ?? null,
  246. $this->options['classesPartialsTraceIgnore']
  247. );
  248. }
  249. /**
  250. * @return array{string, mixed[]}
  251. */
  252. private function getRecordTags(LogRecord $record): array
  253. {
  254. $tags = null;
  255. $filteredContext = [];
  256. if ($record->context !== []) {
  257. $filteredContext = $record->context;
  258. foreach ($this->options['debugTagsKeysInContext'] as $key) {
  259. if (isset($filteredContext[$key])) {
  260. $tags = $filteredContext[$key];
  261. if ($key === 0) {
  262. array_shift($filteredContext);
  263. } else {
  264. unset($filteredContext[$key]);
  265. }
  266. break;
  267. }
  268. }
  269. }
  270. return [$tags ?? $record->level->toPsrLogLevel(), $filteredContext];
  271. }
  272. /**
  273. * @inheritDoc
  274. */
  275. protected function getDefaultFormatter(): FormatterInterface
  276. {
  277. return new LineFormatter('%message%');
  278. }
  279. }