1<?php
2namespace GuzzleHttp;
3
4use Psr\Http\Message\MessageInterface;
5use Psr\Http\Message\RequestInterface;
6use Psr\Http\Message\ResponseInterface;
7
8/**
9 * Formats log messages using variable substitutions for requests, responses,
10 * and other transactional data.
11 *
12 * The following variable substitutions are supported:
13 *
14 * - {request}:        Full HTTP request message
15 * - {response}:       Full HTTP response message
16 * - {ts}:             ISO 8601 date in GMT
17 * - {date_iso_8601}   ISO 8601 date in GMT
18 * - {date_common_log} Apache common log date using the configured timezone.
19 * - {host}:           Host of the request
20 * - {method}:         Method of the request
21 * - {uri}:            URI of the request
22 * - {version}:        Protocol version
23 * - {target}:         Request target of the request (path + query + fragment)
24 * - {hostname}:       Hostname of the machine that sent the request
25 * - {code}:           Status code of the response (if available)
26 * - {phrase}:         Reason phrase of the response  (if available)
27 * - {error}:          Any error messages (if available)
28 * - {req_header_*}:   Replace `*` with the lowercased name of a request header to add to the message
29 * - {res_header_*}:   Replace `*` with the lowercased name of a response header to add to the message
30 * - {req_headers}:    Request headers
31 * - {res_headers}:    Response headers
32 * - {req_body}:       Request body
33 * - {res_body}:       Response body
34 */
35class MessageFormatter
36{
37    /**
38     * Apache Common Log Format.
39     * @link http://httpd.apache.org/docs/2.4/logs.html#common
40     * @var string
41     */
42    const CLF = "{hostname} {req_header_User-Agent} - [{date_common_log}] \"{method} {target} HTTP/{version}\" {code} {res_header_Content-Length}";
43    const DEBUG = ">>>>>>>>\n{request}\n<<<<<<<<\n{response}\n--------\n{error}";
44    const SHORT = '[{ts}] "{method} {target} HTTP/{version}" {code}';
45
46    /** @var string Template used to format log messages */
47    private $template;
48
49    /**
50     * @param string $template Log message template
51     */
52    public function __construct($template = self::CLF)
53    {
54        $this->template = $template ?: self::CLF;
55    }
56
57    /**
58     * Returns a formatted message string.
59     *
60     * @param RequestInterface  $request  Request that was sent
61     * @param ResponseInterface $response Response that was received
62     * @param \Exception        $error    Exception that was received
63     *
64     * @return string
65     */
66    public function format(
67        RequestInterface $request,
68        ResponseInterface $response = null,
69        \Exception $error = null
70    ) {
71        $cache = [];
72
73        return preg_replace_callback(
74            '/{\s*([A-Za-z_\-\.0-9]+)\s*}/',
75            function (array $matches) use ($request, $response, $error, &$cache) {
76                if (isset($cache[$matches[1]])) {
77                    return $cache[$matches[1]];
78                }
79
80                $result = '';
81                switch ($matches[1]) {
82                    case 'request':
83                        $result = Psr7\str($request);
84                        break;
85                    case 'response':
86                        $result = $response ? Psr7\str($response) : '';
87                        break;
88                    case 'req_headers':
89                        $result = trim($request->getMethod()
90                                . ' ' . $request->getRequestTarget())
91                            . ' HTTP/' . $request->getProtocolVersion() . "\r\n"
92                            . $this->headers($request);
93                        break;
94                    case 'res_headers':
95                        $result = $response ?
96                            sprintf(
97                                'HTTP/%s %d %s',
98                                $response->getProtocolVersion(),
99                                $response->getStatusCode(),
100                                $response->getReasonPhrase()
101                            ) . "\r\n" . $this->headers($response)
102                            : 'NULL';
103                        break;
104                    case 'req_body':
105                        $result = $request->getBody();
106                        break;
107                    case 'res_body':
108                        $result = $response ? $response->getBody() : 'NULL';
109                        break;
110                    case 'ts':
111                    case 'date_iso_8601':
112                        $result = gmdate('c');
113                        break;
114                    case 'date_common_log':
115                        $result = date('d/M/Y:H:i:s O');
116                        break;
117                    case 'method':
118                        $result = $request->getMethod();
119                        break;
120                    case 'version':
121                        $result = $request->getProtocolVersion();
122                        break;
123                    case 'uri':
124                    case 'url':
125                        $result = $request->getUri();
126                        break;
127                    case 'target':
128                        $result = $request->getRequestTarget();
129                        break;
130                    case 'req_version':
131                        $result = $request->getProtocolVersion();
132                        break;
133                    case 'res_version':
134                        $result = $response
135                            ? $response->getProtocolVersion()
136                            : 'NULL';
137                        break;
138                    case 'host':
139                        $result = $request->getHeaderLine('Host');
140                        break;
141                    case 'hostname':
142                        $result = gethostname();
143                        break;
144                    case 'code':
145                        $result = $response ? $response->getStatusCode() : 'NULL';
146                        break;
147                    case 'phrase':
148                        $result = $response ? $response->getReasonPhrase() : 'NULL';
149                        break;
150                    case 'error':
151                        $result = $error ? $error->getMessage() : 'NULL';
152                        break;
153                    default:
154                        // handle prefixed dynamic headers
155                        if (strpos($matches[1], 'req_header_') === 0) {
156                            $result = $request->getHeaderLine(substr($matches[1], 11));
157                        } elseif (strpos($matches[1], 'res_header_') === 0) {
158                            $result = $response
159                                ? $response->getHeaderLine(substr($matches[1], 11))
160                                : 'NULL';
161                        }
162                }
163
164                $cache[$matches[1]] = $result;
165                return $result;
166            },
167            $this->template
168        );
169    }
170
171    /**
172     * Get headers from message as string
173     *
174     * @return string
175     */
176    private function headers(MessageInterface $message)
177    {
178        $result = '';
179        foreach ($message->getHeaders() as $name => $values) {
180            $result .= $name . ': ' . implode(', ', $values) . "\r\n";
181        }
182
183        return trim($result);
184    }
185}
186