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