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\Formatter; 13 14use Monolog\DateTimeImmutable; 15use Monolog\Utils; 16use Throwable; 17 18/** 19 * Normalizes incoming records to remove objects/resources so it's easier to dump to various targets 20 * 21 * @author Jordi Boggiano <j.boggiano@seld.be> 22 */ 23class NormalizerFormatter implements FormatterInterface 24{ 25 public const SIMPLE_DATE = "Y-m-d\TH:i:sP"; 26 27 /** @var string */ 28 protected $dateFormat; 29 /** @var int */ 30 protected $maxNormalizeDepth = 9; 31 /** @var int */ 32 protected $maxNormalizeItemCount = 1000; 33 34 /** @var int */ 35 private $jsonEncodeOptions = Utils::DEFAULT_JSON_FLAGS; 36 37 /** 38 * @param string|null $dateFormat The format of the timestamp: one supported by DateTime::format 39 */ 40 public function __construct(?string $dateFormat = null) 41 { 42 $this->dateFormat = null === $dateFormat ? static::SIMPLE_DATE : $dateFormat; 43 if (!function_exists('json_encode')) { 44 throw new \RuntimeException('PHP\'s json extension is required to use Monolog\'s NormalizerFormatter'); 45 } 46 } 47 48 /** 49 * {@inheritDoc} 50 * 51 * @param mixed[] $record 52 */ 53 public function format(array $record) 54 { 55 return $this->normalize($record); 56 } 57 58 /** 59 * {@inheritDoc} 60 */ 61 public function formatBatch(array $records) 62 { 63 foreach ($records as $key => $record) { 64 $records[$key] = $this->format($record); 65 } 66 67 return $records; 68 } 69 70 public function getDateFormat(): string 71 { 72 return $this->dateFormat; 73 } 74 75 public function setDateFormat(string $dateFormat): self 76 { 77 $this->dateFormat = $dateFormat; 78 79 return $this; 80 } 81 82 /** 83 * The maximum number of normalization levels to go through 84 */ 85 public function getMaxNormalizeDepth(): int 86 { 87 return $this->maxNormalizeDepth; 88 } 89 90 public function setMaxNormalizeDepth(int $maxNormalizeDepth): self 91 { 92 $this->maxNormalizeDepth = $maxNormalizeDepth; 93 94 return $this; 95 } 96 97 /** 98 * The maximum number of items to normalize per level 99 */ 100 public function getMaxNormalizeItemCount(): int 101 { 102 return $this->maxNormalizeItemCount; 103 } 104 105 public function setMaxNormalizeItemCount(int $maxNormalizeItemCount): self 106 { 107 $this->maxNormalizeItemCount = $maxNormalizeItemCount; 108 109 return $this; 110 } 111 112 /** 113 * Enables `json_encode` pretty print. 114 */ 115 public function setJsonPrettyPrint(bool $enable): self 116 { 117 if ($enable) { 118 $this->jsonEncodeOptions |= JSON_PRETTY_PRINT; 119 } else { 120 $this->jsonEncodeOptions &= ~JSON_PRETTY_PRINT; 121 } 122 123 return $this; 124 } 125 126 /** 127 * @param mixed $data 128 * @return null|scalar|array<array|scalar|null> 129 */ 130 protected function normalize($data, int $depth = 0) 131 { 132 if ($depth > $this->maxNormalizeDepth) { 133 return 'Over ' . $this->maxNormalizeDepth . ' levels deep, aborting normalization'; 134 } 135 136 if (null === $data || is_scalar($data)) { 137 if (is_float($data)) { 138 if (is_infinite($data)) { 139 return ($data > 0 ? '' : '-') . 'INF'; 140 } 141 if (is_nan($data)) { 142 return 'NaN'; 143 } 144 } 145 146 return $data; 147 } 148 149 if (is_array($data)) { 150 $normalized = []; 151 152 $count = 1; 153 foreach ($data as $key => $value) { 154 if ($count++ > $this->maxNormalizeItemCount) { 155 $normalized['...'] = 'Over ' . $this->maxNormalizeItemCount . ' items ('.count($data).' total), aborting normalization'; 156 break; 157 } 158 159 $normalized[$key] = $this->normalize($value, $depth + 1); 160 } 161 162 return $normalized; 163 } 164 165 if ($data instanceof \DateTimeInterface) { 166 return $this->formatDate($data); 167 } 168 169 if (is_object($data)) { 170 if ($data instanceof Throwable) { 171 return $this->normalizeException($data, $depth); 172 } 173 174 if ($data instanceof \JsonSerializable) { 175 /** @var null|scalar|array<array|scalar|null> $value */ 176 $value = $data->jsonSerialize(); 177 } elseif (method_exists($data, '__toString')) { 178 /** @var string $value */ 179 $value = $data->__toString(); 180 } else { 181 // the rest is normalized by json encoding and decoding it 182 /** @var null|scalar|array<array|scalar|null> $value */ 183 $value = json_decode($this->toJson($data, true), true); 184 } 185 186 return [Utils::getClass($data) => $value]; 187 } 188 189 if (is_resource($data)) { 190 return sprintf('[resource(%s)]', get_resource_type($data)); 191 } 192 193 return '[unknown('.gettype($data).')]'; 194 } 195 196 /** 197 * @return mixed[] 198 */ 199 protected function normalizeException(Throwable $e, int $depth = 0) 200 { 201 if ($e instanceof \JsonSerializable) { 202 return (array) $e->jsonSerialize(); 203 } 204 205 $data = [ 206 'class' => Utils::getClass($e), 207 'message' => $e->getMessage(), 208 'code' => (int) $e->getCode(), 209 'file' => $e->getFile().':'.$e->getLine(), 210 ]; 211 212 if ($e instanceof \SoapFault) { 213 if (isset($e->faultcode)) { 214 $data['faultcode'] = $e->faultcode; 215 } 216 217 if (isset($e->faultactor)) { 218 $data['faultactor'] = $e->faultactor; 219 } 220 221 if (isset($e->detail)) { 222 if (is_string($e->detail)) { 223 $data['detail'] = $e->detail; 224 } elseif (is_object($e->detail) || is_array($e->detail)) { 225 $data['detail'] = $this->toJson($e->detail, true); 226 } 227 } 228 } 229 230 $trace = $e->getTrace(); 231 foreach ($trace as $frame) { 232 if (isset($frame['file'])) { 233 $data['trace'][] = $frame['file'].':'.$frame['line']; 234 } 235 } 236 237 if ($previous = $e->getPrevious()) { 238 $data['previous'] = $this->normalizeException($previous, $depth + 1); 239 } 240 241 return $data; 242 } 243 244 /** 245 * Return the JSON representation of a value 246 * 247 * @param mixed $data 248 * @throws \RuntimeException if encoding fails and errors are not ignored 249 * @return string if encoding fails and ignoreErrors is true 'null' is returned 250 */ 251 protected function toJson($data, bool $ignoreErrors = false): string 252 { 253 return Utils::jsonEncode($data, $this->jsonEncodeOptions, $ignoreErrors); 254 } 255 256 /** 257 * @return string 258 */ 259 protected function formatDate(\DateTimeInterface $date) 260 { 261 // in case the date format isn't custom then we defer to the custom DateTimeImmutable 262 // formatting logic, which will pick the right format based on whether useMicroseconds is on 263 if ($this->dateFormat === self::SIMPLE_DATE && $date instanceof DateTimeImmutable) { 264 return (string) $date; 265 } 266 267 return $date->format($this->dateFormat); 268 } 269 270 public function addJsonEncodeOption(int $option): self 271 { 272 $this->jsonEncodeOptions |= $option; 273 274 return $this; 275 } 276 277 public function removeJsonEncodeOption(int $option): self 278 { 279 $this->jsonEncodeOptions &= ~$option; 280 281 return $this; 282 } 283} 284