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 MongoDB\BSON\Type;
15use MongoDB\BSON\UTCDateTime;
16use Monolog\Utils;
17
18/**
19 * Formats a record for use with the MongoDBHandler.
20 *
21 * @author Florian Plattner <me@florianplattner.de>
22 */
23class MongoDBFormatter implements FormatterInterface
24{
25    /** @var bool */
26    private $exceptionTraceAsString;
27    /** @var int */
28    private $maxNestingLevel;
29    /** @var bool */
30    private $isLegacyMongoExt;
31
32    /**
33     * @param int  $maxNestingLevel        0 means infinite nesting, the $record itself is level 1, $record['context'] is 2
34     * @param bool $exceptionTraceAsString set to false to log exception traces as a sub documents instead of strings
35     */
36    public function __construct(int $maxNestingLevel = 3, bool $exceptionTraceAsString = true)
37    {
38        $this->maxNestingLevel = max($maxNestingLevel, 0);
39        $this->exceptionTraceAsString = $exceptionTraceAsString;
40
41        $this->isLegacyMongoExt = extension_loaded('mongodb') && version_compare((string) phpversion('mongodb'), '1.1.9', '<=');
42    }
43
44    /**
45     * {@inheritDoc}
46     *
47     * @return mixed[]
48     */
49    public function format(array $record): array
50    {
51        /** @var mixed[] $res */
52        $res = $this->formatArray($record);
53
54        return $res;
55    }
56
57    /**
58     * {@inheritDoc}
59     *
60     * @return array<mixed[]>
61     */
62    public function formatBatch(array $records): array
63    {
64        $formatted = [];
65        foreach ($records as $key => $record) {
66            $formatted[$key] = $this->format($record);
67        }
68
69        return $formatted;
70    }
71
72    /**
73     * @param  mixed[]        $array
74     * @return mixed[]|string Array except when max nesting level is reached then a string "[...]"
75     */
76    protected function formatArray(array $array, int $nestingLevel = 0)
77    {
78        if ($this->maxNestingLevel > 0 && $nestingLevel > $this->maxNestingLevel) {
79            return '[...]';
80        }
81
82        foreach ($array as $name => $value) {
83            if ($value instanceof \DateTimeInterface) {
84                $array[$name] = $this->formatDate($value, $nestingLevel + 1);
85            } elseif ($value instanceof \Throwable) {
86                $array[$name] = $this->formatException($value, $nestingLevel + 1);
87            } elseif (is_array($value)) {
88                $array[$name] = $this->formatArray($value, $nestingLevel + 1);
89            } elseif (is_object($value) && !$value instanceof Type) {
90                $array[$name] = $this->formatObject($value, $nestingLevel + 1);
91            }
92        }
93
94        return $array;
95    }
96
97    /**
98     * @param  mixed          $value
99     * @return mixed[]|string
100     */
101    protected function formatObject($value, int $nestingLevel)
102    {
103        $objectVars = get_object_vars($value);
104        $objectVars['class'] = Utils::getClass($value);
105
106        return $this->formatArray($objectVars, $nestingLevel);
107    }
108
109    /**
110     * @return mixed[]|string
111     */
112    protected function formatException(\Throwable $exception, int $nestingLevel)
113    {
114        $formattedException = [
115            'class' => Utils::getClass($exception),
116            'message' => $exception->getMessage(),
117            'code' => (int) $exception->getCode(),
118            'file' => $exception->getFile() . ':' . $exception->getLine(),
119        ];
120
121        if ($this->exceptionTraceAsString === true) {
122            $formattedException['trace'] = $exception->getTraceAsString();
123        } else {
124            $formattedException['trace'] = $exception->getTrace();
125        }
126
127        return $this->formatArray($formattedException, $nestingLevel);
128    }
129
130    protected function formatDate(\DateTimeInterface $value, int $nestingLevel): UTCDateTime
131    {
132        if ($this->isLegacyMongoExt) {
133            return $this->legacyGetMongoDbDateTime($value);
134        }
135
136        return $this->getMongoDbDateTime($value);
137    }
138
139    private function getMongoDbDateTime(\DateTimeInterface $value): UTCDateTime
140    {
141        return new UTCDateTime((int) floor(((float) $value->format('U.u')) * 1000));
142    }
143
144    /**
145     * This is needed to support MongoDB Driver v1.19 and below
146     *
147     * See https://github.com/mongodb/mongo-php-driver/issues/426
148     *
149     * It can probably be removed in 2.1 or later once MongoDB's 1.2 is released and widely adopted
150     */
151    private function legacyGetMongoDbDateTime(\DateTimeInterface $value): UTCDateTime
152    {
153        $milliseconds = floor(((float) $value->format('U.u')) * 1000);
154
155        $milliseconds = (PHP_INT_SIZE == 8) //64-bit OS?
156            ? (int) $milliseconds
157            : (string) $milliseconds;
158
159        // @phpstan-ignore-next-line
160        return new UTCDateTime($milliseconds);
161    }
162}
163