Completed
Push — master ( 4440c5...0c74a8 )
by Sergey
04:29
created

SlackTarget::export()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 2.003

Importance

Changes 0
Metric Value
dl 0
loc 13
ccs 10
cts 11
cp 0.9091
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 9
nc 2
nop 0
crap 2.003
1
<?php
2
/**
3
 * Slack log target for Yii 2
4
 *
5
 * @see       https://github.com/sergeymakinen/yii2-slack-log
6
 * @copyright Copyright (c) 2016 Sergey Makinen (https://makinen.ru)
7
 * @license   https://github.com/sergeymakinen/yii2-slack-log/blob/master/LICENSE The MIT License
8
 */
9
10
namespace sergeymakinen\log;
11
12
use yii\base\InvalidValueException;
13
use yii\di\Instance;
14
use yii\helpers\Json;
15
use yii\helpers\Url;
16
use yii\helpers\VarDumper;
17
use yii\httpclient\Client;
18
use yii\log\Logger;
19
use yii\log\Target;
20
use yii\web\Request;
21
22
class SlackTarget extends Target
23
{
24
    /**
25
     * Yii HTTP client configuration.
26
     * This can be a component ID, a configuration array or a Client instance.
27
     *
28
     * @var Client|array|string
29
     * @since 1.2
30
     */
31
    public $httpClient = [
32
        'class' => 'yii\httpclient\Client'
33
    ];
34
35
    /**
36
     * Incoming Webhook URL.
37
     *
38
     * @var string
39
     */
40
    public $webhookUrl;
41
42
    /**
43
     * Displayed username.
44
     *
45
     * @var string
46
     */
47
    public $username;
48
49
    /**
50
     * Icon URL.
51
     *
52
     * @var string
53
     */
54
    public $iconUrl;
55
56
    /**
57
     * Icon Emoji.
58
     *
59
     * @var string
60
     */
61
    public $iconEmoji;
62
63
    /**
64
     * Channel or Direct Message.
65
     *
66
     * @var string
67
     */
68
    public $channel;
69
70
    /**
71
     * Colors per a Logger level.
72
     *
73
     * @var array
74
     */
75
    public $colors = [
76
        Logger::LEVEL_ERROR => 'danger',
77
        Logger::LEVEL_WARNING => 'warning'
78
    ];
79
80
    /**
81
     * @inheritDoc
82
     */
83
    public $exportInterval = 50;
84
85
    /**
86
     * @inheritDoc
87
     */
88 5
    public function init()
89
    {
90 5
        parent::init();
91 5
        $this->httpClient = Instance::ensure($this->httpClient, Client::className());
92 5
    }
93
94
    /**
95
     * @inheritDoc
96
     */
97 1
    public function export()
98
    {
99 1
        $response = $this->httpClient->post(
100 1
            $this->webhookUrl,
101 1
            Json::encode($this->getPayload()),
102 1
            ['Content-Type: application/json; charset=UTF-8']
103 1
        )->send();
104 1
        if (!$response->getIsOk()) {
105 1
            throw new InvalidValueException(
106 1
                "Unable to send logs to Slack: {$response->getContent()}", (int) $response->getStatusCode()
107 1
            );
108
        }
109
    }
110
111
    /**
112
     * Encodes special chars in a message as HTML entities.
113
     *
114
     * @param string $message
115
     *
116
     * @return string
117
     */
118 4
    protected function encodeMessage($message)
119
    {
120 4
        return htmlspecialchars($message, ENT_NOQUOTES, 'UTF-8');
121
    }
122
123
    /**
124
     * Returns a Slack API payload.
125
     *
126
     * @return array
127
     * @since 1.2
128
     */
129 2
    protected function getPayload()
130
    {
131
        $payload = [
132 2
            'parse' => 'none',
133 2
            'attachments' => array_map([$this, 'formatMessageAttachment'], $this->messages)
134 2
        ];
135 2
        $this->insertIntoPayload($payload, 'username', $this->username);
136 2
        $this->insertIntoPayload($payload, 'icon_url', $this->iconUrl);
137 2
        $this->insertIntoPayload($payload, 'icon_emoji', $this->iconEmoji);
138 2
        $this->insertIntoPayload($payload, 'channel', $this->channel);
139 2
        return $payload;
140
    }
141
142
    /**
143
     * Returns a properly formatted message attachment for Slack API.
144
     *
145
     * @param array $message
146
     *
147
     * @return array
148
     */
149 3
    protected function formatMessageAttachment($message)
150
    {
151 3
        list($text, $level, $category, $timestamp) = $message;
152
        $attachment = [
153 3
            'fallback' => $this->encodeMessage($this->formatMessage($message)),
154 3
            'title' => ucwords(Logger::getLevelName($level)),
155
            'fields' => [
156
                [
157 3
                    'title' => 'Level',
158 3
                    'value' => Logger::getLevelName($level),
159
                    'short' => true
160 3
                ],
161
                [
162 3
                    'title' => 'Category',
163 3
                    'value' => '`' . $this->encodeMessage($category) . '`',
164
                    'short' => true
165 3
                ]
166 3
            ],
167 3
            'footer' => static::className(),
168 3
            'ts' => (int) round($timestamp),
169
            'mrkdwn_in' => [
170 3
                'fields',
171
                'text'
172 3
            ]
173 3
        ];
174 3
        if (isset($this->prefix)) {
175 3
            $attachment['fields'][] = [
176 3
                'title' => 'Prefix',
177 3
                'value' => '`' . $this->encodeMessage(call_user_func($this->prefix, $message)) . '`',
178 3
                'short' => true,
179
            ];
180 3
        }
181 3
        if (isset(\Yii::$app)) {
182 3
            $this->insertRequestIntoAttachment($attachment);
183 3
            $this->insertUserIntoAttachment($attachment);
184 3
            $this->insertSessionIntoAttachment($attachment);
185 3
        }
186 3
        if (isset($this->colors[$level])) {
187 2
            $attachment['color'] = $this->colors[$level];
188 2
        }
189 3
        if (!is_string($text)) {
190 2
            if ($text instanceof \Throwable || $text instanceof \Exception) {
191 1
                $text = (string) $text;
192 1
            } else {
193 1
                $text = VarDumper::export($text);
194
            }
195 2
        }
196 3
        $attachment['text'] = "```\n" . $this->encodeMessage($text) . "\n```";
197 3
        if (isset($message[4]) && !empty($message[4])) {
198 1
            $this->insertTracesIntoAttachment($message[4], $attachment);
199 1
        }
200 3
        return $attachment;
201
    }
202
203
    /**
204
     * Inserts session data into the attachement if applicable.
205
     *
206
     * @param array $attachment
207
     */
208 3
    private function insertSessionIntoAttachment(array &$attachment)
209
    {
210
        if (
211 3
            \Yii::$app->has('session', true)
212 3
            && !is_null(\Yii::$app->getSession())
213 3
            && \Yii::$app->getSession()->getIsActive()
214 3
        ) {
215 3
            $attachment['fields'][] = [
216 3
                'title' => 'Session ID',
217 3
                'value' => '`' . $this->encodeMessage(\Yii::$app->getSession()->getId()) . '`',
1 ignored issue
show
Bug introduced by
The method getSession does only exist in yii\web\Application, but not in yii\console\Application.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
218 3
                'short' => true,
219
            ];
220 3
        }
221 3
    }
222
223
    /**
224
     * Inserts traces into the attachement if applicable.
225
     *
226
     * @param array $traces
227
     * @param array $attachment
228
     */
229
    private function insertTracesIntoAttachment(array $traces, array &$attachment)
230
    {
231 1
        $traces = array_map(function ($trace) {
232 1
            return "in {$trace['file']}:{$trace['line']}";
233 1
        }, $traces);
234 1
        $attachment['fields'][] = [
235 1
            'title' => 'Stack Trace',
236 1
            'value' => "```\n" . $this->encodeMessage(implode("\n", $traces)) . "\n```",
237 1
            'short' => false,
238
        ];
239 1
    }
240
241
    /**
242
     * Inserts user data into the attachement if applicable.
243
     *
244
     * @param array $attachment
245
     */
246 3
    private function insertUserIntoAttachment(array &$attachment)
247
    {
248 3
        if (\Yii::$app->has('user', true) && !is_null(\Yii::$app->getUser())) {
249 3
            $user = \Yii::$app->getUser()->getIdentity(false);
250 3
            if (isset($user)) {
251 3
                $attachment['fields'][] = [
252 3
                    'title' => 'User ID',
253 3
                    'value' => $this->encodeMessage($user->getId()),
254 3
                    'short' => true,
255
                ];
256 3
            }
257 3
        }
258 3
    }
259
260
    /**
261
     * Inserts request data into the attachement if applicable.
262
     *
263
     * @param array $attachment
264
     */
265 4
    private function insertRequestIntoAttachment(array &$attachment)
266
    {
267 4
        if (\Yii::$app->getRequest() instanceof Request) {
268 3
            $attachment['author_name'] = $attachment['author_link'] = Url::current([], true);
269 3
            $attachment['fields'][] = [
270 3
                'title' => 'User IP',
271 3
                'value' => \Yii::$app->getRequest()->getUserIP(),
272 3
                'short' => true,
273
            ];
274 3
        } else {
275 1
            if (isset($_SERVER['argv'])) {
276 1
                $params = $_SERVER['argv'];
277 1
            } else {
278 1
                $params = [];
279
            }
280 1
            $attachment['author_name'] = implode(' ', $params);
281
        }
282 4
    }
283
284
    /**
285
     * Copies the value to the payload if the value is set.
286
     *
287
     * @param array $payload
288
     * @param string $name
289
     * @param mixed $value
290
     */
291 2
    private function insertIntoPayload(array &$payload, $name, $value)
292
    {
293 2
        if (isset($value)) {
294 2
            $payload[$name] = $value;
295 2
        }
296 2
    }
297
}
298