PHPConsoleHandler.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  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\Logger;
  14. use Monolog\Utils;
  15. use PhpConsole\Connector;
  16. use PhpConsole\Handler as VendorPhpConsoleHandler;
  17. use PhpConsole\Helper;
  18. /**
  19. * Monolog handler for Google Chrome extension "PHP Console"
  20. *
  21. * Display PHP error/debug log messages in Google Chrome console and notification popups, executes PHP code remotely
  22. *
  23. * Usage:
  24. * 1. Install Google Chrome extension https://chrome.google.com/webstore/detail/php-console/nfhmhhlpfleoednkpnnnkolmclajemef
  25. * 2. See overview https://github.com/barbushin/php-console#overview
  26. * 3. Install PHP Console library https://github.com/barbushin/php-console#installation
  27. * 4. Example (result will looks like http://i.hizliresim.com/vg3Pz4.png)
  28. *
  29. * $logger = new \Monolog\Logger('all', array(new \Monolog\Handler\PHPConsoleHandler()));
  30. * \Monolog\ErrorHandler::register($logger);
  31. * echo $undefinedVar;
  32. * $logger->debug('SELECT * FROM users', array('db', 'time' => 0.012));
  33. * PC::debug($_SERVER); // PHP Console debugger for any type of vars
  34. *
  35. * @author Sergey Barbushin https://www.linkedin.com/in/barbushin
  36. */
  37. class PHPConsoleHandler extends AbstractProcessingHandler
  38. {
  39. private $options = [
  40. 'enabled' => true, // bool Is PHP Console server enabled
  41. 'classesPartialsTraceIgnore' => ['Monolog\\'], // array Hide calls of classes started with...
  42. 'debugTagsKeysInContext' => [0, 'tag'], // bool Is PHP Console server enabled
  43. 'useOwnErrorsHandler' => false, // bool Enable errors handling
  44. 'useOwnExceptionsHandler' => false, // bool Enable exceptions handling
  45. 'sourcesBasePath' => null, // string Base path of all project sources to strip in errors source paths
  46. 'registerHelper' => true, // bool Register PhpConsole\Helper that allows short debug calls like PC::debug($var, 'ta.g.s')
  47. 'serverEncoding' => null, // string|null Server internal encoding
  48. 'headersLimit' => null, // int|null Set headers size limit for your web-server
  49. 'password' => null, // string|null Protect PHP Console connection by password
  50. 'enableSslOnlyMode' => false, // bool Force connection by SSL for clients with PHP Console installed
  51. 'ipMasks' => [], // array Set IP masks of clients that will be allowed to connect to PHP Console: array('192.168.*.*', '127.0.0.1')
  52. 'enableEvalListener' => false, // bool Enable eval request to be handled by eval dispatcher(if enabled, 'password' option is also required)
  53. 'dumperDetectCallbacks' => false, // bool Convert callback items in dumper vars to (callback SomeClass::someMethod) strings
  54. 'dumperLevelLimit' => 5, // int Maximum dumped vars array or object nested dump level
  55. 'dumperItemsCountLimit' => 100, // int Maximum dumped var same level array items or object properties number
  56. 'dumperItemSizeLimit' => 5000, // int Maximum length of any string or dumped array item
  57. 'dumperDumpSizeLimit' => 500000, // int Maximum approximate size of dumped vars result formatted in JSON
  58. 'detectDumpTraceAndSource' => false, // bool Autodetect and append trace data to debug
  59. 'dataStorage' => null, // \PhpConsole\Storage|null Fixes problem with custom $_SESSION handler(see http://goo.gl/Ne8juJ)
  60. ];
  61. /** @var Connector */
  62. private $connector;
  63. /**
  64. * @param array $options See \Monolog\Handler\PHPConsoleHandler::$options for more details
  65. * @param Connector|null $connector Instance of \PhpConsole\Connector class (optional)
  66. * @param string|int $level The minimum logging level at which this handler will be triggered.
  67. * @param bool $bubble Whether the messages that are handled can bubble up the stack or not.
  68. * @throws \RuntimeException
  69. */
  70. public function __construct(array $options = [], ?Connector $connector = null, $level = Logger::DEBUG, bool $bubble = true)
  71. {
  72. if (!class_exists('PhpConsole\Connector')) {
  73. throw new \RuntimeException('PHP Console library not found. See https://github.com/barbushin/php-console#installation');
  74. }
  75. parent::__construct($level, $bubble);
  76. $this->options = $this->initOptions($options);
  77. $this->connector = $this->initConnector($connector);
  78. }
  79. private function initOptions(array $options): array
  80. {
  81. $wrongOptions = array_diff(array_keys($options), array_keys($this->options));
  82. if ($wrongOptions) {
  83. throw new \RuntimeException('Unknown options: ' . implode(', ', $wrongOptions));
  84. }
  85. return array_replace($this->options, $options);
  86. }
  87. /**
  88. * @suppress PhanTypeMismatchArgument
  89. */
  90. private function initConnector(?Connector $connector = null): Connector
  91. {
  92. if (!$connector) {
  93. if ($this->options['dataStorage']) {
  94. Connector::setPostponeStorage($this->options['dataStorage']);
  95. }
  96. $connector = Connector::getInstance();
  97. }
  98. if ($this->options['registerHelper'] && !Helper::isRegistered()) {
  99. Helper::register();
  100. }
  101. if ($this->options['enabled'] && $connector->isActiveClient()) {
  102. if ($this->options['useOwnErrorsHandler'] || $this->options['useOwnExceptionsHandler']) {
  103. $handler = VendorPhpConsoleHandler::getInstance();
  104. $handler->setHandleErrors($this->options['useOwnErrorsHandler']);
  105. $handler->setHandleExceptions($this->options['useOwnExceptionsHandler']);
  106. $handler->start();
  107. }
  108. if ($this->options['sourcesBasePath']) {
  109. $connector->setSourcesBasePath($this->options['sourcesBasePath']);
  110. }
  111. if ($this->options['serverEncoding']) {
  112. $connector->setServerEncoding($this->options['serverEncoding']);
  113. }
  114. if ($this->options['password']) {
  115. $connector->setPassword($this->options['password']);
  116. }
  117. if ($this->options['enableSslOnlyMode']) {
  118. $connector->enableSslOnlyMode();
  119. }
  120. if ($this->options['ipMasks']) {
  121. $connector->setAllowedIpMasks($this->options['ipMasks']);
  122. }
  123. if ($this->options['headersLimit']) {
  124. $connector->setHeadersLimit($this->options['headersLimit']);
  125. }
  126. if ($this->options['detectDumpTraceAndSource']) {
  127. $connector->getDebugDispatcher()->detectTraceAndSource = true;
  128. }
  129. $dumper = $connector->getDumper();
  130. $dumper->levelLimit = $this->options['dumperLevelLimit'];
  131. $dumper->itemsCountLimit = $this->options['dumperItemsCountLimit'];
  132. $dumper->itemSizeLimit = $this->options['dumperItemSizeLimit'];
  133. $dumper->dumpSizeLimit = $this->options['dumperDumpSizeLimit'];
  134. $dumper->detectCallbacks = $this->options['dumperDetectCallbacks'];
  135. if ($this->options['enableEvalListener']) {
  136. $connector->startEvalRequestsListener();
  137. }
  138. }
  139. return $connector;
  140. }
  141. public function getConnector(): Connector
  142. {
  143. return $this->connector;
  144. }
  145. public function getOptions(): array
  146. {
  147. return $this->options;
  148. }
  149. public function handle(array $record): bool
  150. {
  151. if ($this->options['enabled'] && $this->connector->isActiveClient()) {
  152. return parent::handle($record);
  153. }
  154. return !$this->bubble;
  155. }
  156. /**
  157. * Writes the record down to the log of the implementing handler
  158. */
  159. protected function write(array $record): void
  160. {
  161. if ($record['level'] < Logger::NOTICE) {
  162. $this->handleDebugRecord($record);
  163. } elseif (isset($record['context']['exception']) && $record['context']['exception'] instanceof \Throwable) {
  164. $this->handleExceptionRecord($record);
  165. } else {
  166. $this->handleErrorRecord($record);
  167. }
  168. }
  169. private function handleDebugRecord(array $record): void
  170. {
  171. $tags = $this->getRecordTags($record);
  172. $message = $record['message'];
  173. if ($record['context']) {
  174. $message .= ' ' . Utils::jsonEncode($this->connector->getDumper()->dump(array_filter($record['context'])), null, true);
  175. }
  176. $this->connector->getDebugDispatcher()->dispatchDebug($message, $tags, $this->options['classesPartialsTraceIgnore']);
  177. }
  178. private function handleExceptionRecord(array $record): void
  179. {
  180. $this->connector->getErrorsDispatcher()->dispatchException($record['context']['exception']);
  181. }
  182. private function handleErrorRecord(array $record): void
  183. {
  184. $context = $record['context'];
  185. $this->connector->getErrorsDispatcher()->dispatchError(
  186. $context['code'] ?? null,
  187. $context['message'] ?? $record['message'],
  188. $context['file'] ?? null,
  189. $context['line'] ?? null,
  190. $this->options['classesPartialsTraceIgnore']
  191. );
  192. }
  193. private function getRecordTags(array &$record)
  194. {
  195. $tags = null;
  196. if (!empty($record['context'])) {
  197. $context = & $record['context'];
  198. foreach ($this->options['debugTagsKeysInContext'] as $key) {
  199. if (!empty($context[$key])) {
  200. $tags = $context[$key];
  201. if ($key === 0) {
  202. array_shift($context);
  203. } else {
  204. unset($context[$key]);
  205. }
  206. break;
  207. }
  208. }
  209. }
  210. return $tags ?: strtolower($record['level_name']);
  211. }
  212. /**
  213. * {@inheritDoc}
  214. */
  215. protected function getDefaultFormatter(): FormatterInterface
  216. {
  217. return new LineFormatter('%message%');
  218. }
  219. }