Api   A
last analyzed

Complexity

Total Complexity 42

Size/Duplication

Total Lines 320
Duplicated Lines 0 %

Test Coverage

Coverage 6.43%

Importance

Changes 4
Bugs 1 Features 0
Metric Value
eloc 143
c 4
b 1
f 0
dl 0
loc 320
ccs 9
cts 140
cp 0.0643
rs 9.0399
wmc 42

14 Methods

Rating   Name   Duplication   Size   Complexity  
A getFileSize() 0 11 3
A getAttachment() 0 9 2
A showSourceCode() 0 25 3
A putAttachment() 0 14 3
A getPage() 0 22 4
B handleError() 0 29 7
A getList() 0 40 5
A createPage() 0 20 3
A setSpace() 0 3 1
A __construct() 0 5 1
A deletePage() 0 6 2
A updatePage() 0 28 4
A getClient() 0 8 1
A uploadAttachment() 0 19 3

How to fix   Complexity   

Complex Class

Complex classes like Api often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Api, and based on these observations, apply Extract Interface, too.

1
<?php namespace Todaymade\Daux\Format\Confluence;
2
3
use GuzzleHttp\Client;
4
use GuzzleHttp\Exception\BadResponseException;
5
6
class Api
7
{
8
    protected $base_url;
9
    protected $user;
10
    protected $pass;
11
12
    protected $space;
13
14 1
    public function __construct($base_url, $user, $pass)
15
    {
16 1
        $this->base_url = $base_url;
17 1
        $this->user = $user;
18 1
        $this->pass = $pass;
19 1
    }
20
21
    public function setSpace($space_id)
22
    {
23
        $this->space = $space_id;
24
    }
25
26
    /**
27
     * This method is public due to test purposes
28
     * @return Client
29
     */
30 1
    public function getClient()
31
    {
32
        $options = [
33 1
            'base_uri' => $this->base_url . 'rest/api/',
34 1
            'auth' => [$this->user, $this->pass],
35
        ];
36
37 1
        return new Client($options);
38
    }
39
40
    /**
41
     * The standard error message from guzzle is quite poor in informations,
42
     * this will give little bit more sense to it and return it
43
     *
44
     * @param BadResponseException $e
45
     * @return \Exception
46
     */
47
    protected function handleError(BadResponseException $e)
48
    {
49
        $request = $e->getRequest();
50
        $response = $e->getResponse();
51
52
        $level = floor($response->getStatusCode() / 100);
53
54
        if ($level == '4') {
55
            $label = 'Client error response';
56
        } elseif ($level == '5') {
57
            $label = 'Server error response';
58
        } else {
59
            $label = 'Unsuccessful response';
60
        }
61
62
        $message = $label .
63
            "\n [url] " . $request->getUri() .
64
            "\n [status code] " . $response->getStatusCode() .
65
            "\n [message] ";
66
67
        $body = $response->getBody();
68
        $json = json_decode($body, true);
69
        $message .= ($json != null && array_key_exists('message', $json)) ? $json['message'] : $body;
70
71
        if ($level == '4' && strpos($message, 'page with this title already exists') !== false) {
72
            return new DuplicateTitleException($message, 0, $e->getPrevious());
73
        }
74
75
        return new BadResponseException($message, $request, $response, $e->getPrevious());
76
    }
77
78
    public function getPage($id)
79
    {
80
        $url = "content/$id?expand=ancestors,version,body.storage";
81
82
        try {
83
            $result = json_decode($this->getClient()->get($url)->getBody(), true);
84
        } catch (BadResponseException $e) {
85
            throw $this->handleError($e);
86
        }
87
88
        $ancestor_id = null;
89
        if (array_key_exists('ancestors', $result) && count($result['ancestors'])) {
90
            $ancestor_page = end($result['ancestors']); // We need the direct parent
91
            $ancestor_id = $ancestor_page['id'];
92
        }
93
94
        return [
95
            'id' => $result['id'],
96
            'ancestor_id' => $ancestor_id,
97
            'title' => $result['title'],
98
            'version' => $result['version']['number'],
99
            'content' => $result['body']['storage']['value'],
100
        ];
101
    }
102
103
    /**
104
     * Get a list of pages
105
     *
106
     * @param int $rootPage
107
     * @param bool $recursive
108
     * @return array
109
     */
110
    public function getList($rootPage, $recursive = false)
111
    {
112
        $increment = 15;
113
114
        // We set a limit of 15 as it appears that
115
        // Confluence fails silently when retrieving
116
        // more than 20 entries with "body.storage"
117
        $base_url = $url = "content/$rootPage/child/page?expand=version,body.storage&limit=$increment";
118
        $start = 0;
119
120
        $pages = [];
121
122
        do {
123
            try {
124
                $hierarchy = json_decode($this->getClient()->get($url)->getBody(), true);
125
            } catch (BadResponseException $e) {
126
                throw $this->handleError($e);
127
            }
128
129
            foreach ($hierarchy['results'] as $result) {
130
                $pages[$result['title']] = [
131
                    'id' => $result['id'],
132
                    'title' => $result['title'],
133
                    'version' => $result['version']['number'],
134
                    'content' => $result['body']['storage']['value'],
135
                ];
136
137
                if ($recursive) {
138
                    $pages[$result['title']]['children'] = $this->getList($result['id'], true);
139
                }
140
            }
141
142
            // We don't use _links->next as after ~30 elements
143
            // it doesn't show any new elements. This seems
144
            // to be a bug in Confluence
145
            $start += $increment;
146
            $url = "$base_url&start=$start";
147
        } while (!empty($hierarchy['results']));
148
149
        return $pages;
150
    }
151
152
    /**
153
     * @param int $parent_id
154
     * @param string $title
155
     * @param string $content
156
     * @return int
157
     */
158
    public function createPage($parent_id, $title, $content)
159
    {
160
        $body = [
161
            'type' => 'page',
162
            'space' => ['key' => $this->space],
163
            'title' => $title,
164
            'body' => ['storage' => ['value' => $content, 'representation' => 'storage']],
165
        ];
166
167
        if ($parent_id) {
168
            $body['ancestors'] = [['type' => 'page', 'id' => $parent_id]];
169
        }
170
171
        try {
172
            $response = json_decode($this->getClient()->post('content', ['json' => $body])->getBody(), true);
173
        } catch (BadResponseException $e) {
174
            throw $this->handleError($e);
175
        }
176
177
        return $response['id'];
178
    }
179
180
    /**
181
     * @param int $parent_id
182
     * @param int $page_id
183
     * @param int $newVersion
184
     * @param string $title
185
     * @param string $content
186
     */
187
    public function updatePage($parent_id, $page_id, $newVersion, $title, $content)
188
    {
189
        $body = [
190
            'type' => 'page',
191
            'space' => ['key' => $this->space],
192
            'version' => ['number' => $newVersion, 'minorEdit' => true],
193
            'title' => $title,
194
            'body' => ['storage' => ['value' => $content, 'representation' => 'storage']],
195
        ];
196
197
        if ($parent_id) {
198
            $body['ancestors'] = [['type' => 'page', 'id' => $parent_id]];
199
        }
200
201
        try {
202
            $this->getClient()->put("content/$page_id", ['json' => $body]);
203
        } catch (BadResponseException $e) {
204
            $error = $this->handleError($e);
205
206
            $re = '/\[([0-9]*),([0-9]*)\]$/';
207
            preg_match($re, $error->getMessage(), $matches, PREG_OFFSET_CAPTURE, 0);
208
209
            if (count($matches) == 3) {
210
                echo "\nContent: \n";
211
                echo $this->showSourceCode($content, $matches[1][0], $matches[2][0]);
212
            }
213
214
            throw $error;
215
        }
216
    }
217
218
    public function showSourceCode($css, $lineNumber, $column)
219
    {
220
        $lines = preg_split("/\r?\n/", $css);
221
        $start = max($lineNumber - 3, 0);
222
        $end   = min($lineNumber + 2, count($lines));
0 ignored issues
show
Bug introduced by Stéphane Goetz
It seems like $lines can also be of type false; however, parameter $var of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

222
        $end   = min($lineNumber + 2, count(/** @scrutinizer ignore-type */ $lines));
Loading history...
223
224
        $maxWidth = strlen("$end");
225
226
        $filtered = array_slice($lines, $start, $end - $start);
0 ignored issues
show
Bug introduced by Stéphane Goetz
It seems like $lines can also be of type false; however, parameter $array of array_slice() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

226
        $filtered = array_slice(/** @scrutinizer ignore-type */ $lines, $start, $end - $start);
Loading history...
227
228
        $prepared = [];
229
        foreach ($filtered as $index => $line) {
230
231
            $number = $start + 1 + $index;
232
            $gutter = substr(' ' . (' ' . $number), -$maxWidth) . ' | ';
233
234
            if ($number == $lineNumber) {
235
                $spacing = str_repeat(" ", strlen($gutter) + $column - 2);
236
                $prepared[] = '>' . $gutter . $line . "\n " . $spacing . '^';
237
            } else {
238
                $prepared[] = ' ' . $gutter . $line;
239
            }
240
        }
241
242
        return implode("\n", $prepared);
243
    }
244
245
    /**
246
     * Delete a page
247
     *
248
     * @param int $page_id
249
     * @return mixed
250
     */
251
    public function deletePage($page_id)
252
    {
253
        try {
254
            return json_decode($this->getClient()->delete('content/' . $page_id)->getBody(), true);
255
        } catch (BadResponseException $e) {
256
            throw $this->handleError($e);
257
        }
258
    }
259
260
    private function getAttachment($id, $attachment)
261
    {
262
        // Check if an attachment with
263
        // this name is uploaded
264
        try {
265
            $url = "content/$id/child/attachment?filename=" . urlencode($attachment['filename']);
266
            return json_decode($this->getClient()->get($url)->getBody(), true);
267
        } catch (BadResponseException $e) {
268
            throw $this->handleError($e);
269
        }
270
    }
271
272
    private function putAttachment($url, $attachment)
273
    {
274
        $contents = array_key_exists('file', $attachment) ? fopen($attachment['file']->getPath(), 'r') : $attachment['content'];
275
276
        try {
277
            $this->getClient()->post(
278
                $url,
279
                [
280
                    'multipart' => [['name' => 'file', 'contents' => $contents, 'filename' => $attachment['filename']]],
281
                    'headers' => ['X-Atlassian-Token' => 'nocheck'],
282
                ]
283
            );
284
        } catch (BadResponseException $e) {
285
            throw $this->handleError($e);
286
        }
287
    }
288
289
    private function getFileSize($attachment)
290
    {
291
        if (array_key_exists('file', $attachment)) {
292
            return filesize($attachment['file']->getPath());
293
        }
294
295
        if (function_exists('mb_strlen')) {
296
            return mb_strlen($attachment['content']);
297
        }
298
299
        return strlen($attachment['content']);
300
    }
301
302
    /**
303
     * @param int $id
304
     * @param array $attachment
305
     * @param callback $write Write output to the console
306
     */
307
    public function uploadAttachment($id, $attachment, $write)
308
    {
309
        $result = $this->getAttachment($id, $attachment);
310
311
        $url = "content/$id/child/attachment";
312
313
        // If the attachment is already uploaded,
314
        // the update URL is different
315
        if (count($result['results'])) {
316
317
            if ($this->getFileSize($attachment) == $result['results'][0]['extensions']['fileSize']) {
318
                $write(" ( An attachment of the same size already exists, skipping. )");
319
                return;
320
            }
321
322
            $url .= "/{$result['results'][0]['id']}/data";
323
        }
324
325
        $this->putAttachment($url, $attachment);
326
    }
327
}
328