Completed
Push — master ( 57b1db...16daf9 )
by Mike
03:22
created

SupportSugarcrm::getReleaseNotes()   C

Complexity

Conditions 12
Paths 6

Size

Total Lines 68
Code Lines 38

Duplication

Lines 8
Ratio 11.76 %

Importance

Changes 0
Metric Value
cc 12
eloc 38
nc 6
nop 2
dl 8
loc 68
rs 5.7751
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Sugarcrm\UpgradeSpec\Data\Provider\Doc;
4
5
use GuzzleHttp\Client;
6
use GuzzleHttp\Pool;
7
use League\HTMLToMarkdown\HtmlConverter;
8
use Psr\Http\Message\ResponseInterface;
9
use Sugarcrm\UpgradeSpec\Cache\Cache;
10
use Sugarcrm\UpgradeSpec\Purifier\Html;
11
use Symfony\Component\DomCrawler\Crawler;
12
13
class SupportSugarcrm implements DocProviderInterface
14
{
15
    /**
16
     * @var Client
17
     */
18
    private $httpClient;
19
20
    /**
21
     * @var Cache
22
     */
23
    private $cache;
24
25
    /**
26
     * @var HtmlConverter
27
     */
28
    private $htmlConverter;
29
30
    /**
31
     * SupportSugarcrm constructor.
32
     *
33
     * @param Cache         $cache
34
     * @param HtmlConverter $htmlConverter
35
     */
36
    public function __construct(Cache $cache, HtmlConverter $htmlConverter)
37
    {
38
        $this->httpClient = new Client(['base_uri' => 'http://support.sugarcrm.com/']);
39
        $this->cache = $cache;
40
        $this->htmlConverter = $htmlConverter;
41
    }
42
43
    /**
44
     * Get all available SugarCRM versions (sorted ASC).
45
     *
46
     * @param $flav
47
     *
48
     * @return mixed
49
     */
50
    public function getVersions($flav)
51
    {
52
        if ($this->cache->has('versions')) {
53
            return $this->cache->get('versions');
54
        }
55
56
        $crawler = new Crawler($this->getContent('/Documentation/Sugar_Versions/index.html'));
57
        $majors = [];
58
        foreach ($crawler->filter('section.content-body > h1') as $node) {
59
            $major = $node->textContent;
60
            if (preg_match('/^\d+\.\d+$/', $major)) {
61
                $majors[] = $major;
62
            }
63
        }
64
65
        $versions = [];
66
        foreach ($majors as $major) {
67
            $crawler = new Crawler($this->getContent($this->getVersionUri($flav, $major)));
68
            foreach ($crawler->filter('#Release_Notes')->first()->nextAll()->filter('a') as $node) {
69
                if (preg_match_all('/\d+(\.\d+){1,3}/', $node->textContent, $match)) {
70
                    $versions[] = $match[0][0];
71
                }
72
            }
73
        }
74
75
        // sort versions (ASC)
76 View Code Duplication
        usort($versions, function ($v1, $v2) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
77
            return version_compare($v1, $v2, '<') ? -1 : (version_compare($v1, $v2, '>') ? 1 : 0);
78
        });
79
80
        $this->cache->set('versions', $versions);
81
82
        return $versions;
83
    }
84
85
    /**
86
     * @param $flav
87
     * @param array $versions
88
     *
89
     * @return array
90
     */
91
    public function getReleaseNotes($flav, array $versions)
92
    {
93
        $newVersions = array_filter($versions, function ($version) use ($flav) {
94
            return !$this->cache->has($this->getCacheKey([$flav, 'release_notes', $version]));
95
        });
96
97
        $requests = function () use ($flav, $newVersions) {
98
            foreach ($newVersions as $version) {
99
                yield $version => function () use ($flav, $version) {
100
                    return $this->httpClient->getAsync($this->getReleaseNotesUri($flav, $version));
101
                };
102
            }
103
        };
104
105
        if ($newVersions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $newVersions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
106
            $this->processRequestPool($requests, [
107
                'fulfilled' => function (ResponseInterface $response, $version) use ($flav) {
108
                    $html = $response->getBody()->getContents();
109
                    $crawler = new Crawler($this->purifyHtml($html, $this->getReleaseNotesUri($flav, $version)));
110
111
                    $identifiers = [
112
                        'feature_enhancements' => '#Feature_Enhancements',
113
                        'development_changes' => '#Development_Changes',
114
                    ];
115
116
                    $releaseNote = [];
117
                    foreach ($identifiers as $key => $identifier) {
118
                        $nodes = $crawler->filter($identifier);
119
                        if (count($nodes)) {
120
                            $nextSiblings = $nodes->nextAll();
121
122
                            $content = [];
123 View Code Duplication
                            foreach (['p', 'ul'] as $tag) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
124
                                if (count($filtered = $nextSiblings->filter($tag))) {
125
                                    $content[] = sprintf('<%1$s>%2$s</%1$s>', $tag, $filtered->first()->html());
126
                                }
127
                            }
128
129
                            if ($content) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $content of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
130
                                $releaseNote[$key] = $this->htmlConverter->convert(implode('<br>', $content));
131
                            }
132
                        }
133
                    }
134
135
                    $this->cache->set($this->getCacheKey([$flav, 'release_notes', $version]), $releaseNote);
136
                },
137
                'rejected' => function ($reason, $version) {
138
                    throw new \RuntimeException(
139
                        sprintf('Can\'t get release notes for version: %s (reason: %s)', $version, $reason)
140
                    );
141
                },
142
            ]);
143
        }
144
145
        $releaseNotes = [];
146
        foreach ($versions as $version) {
147
            $releaseNote = $this->cache->get($this->getCacheKey([$flav, 'release_notes', $version]), null);
148
            if ($releaseNote) {
149
                $releaseNotes[$version] = $releaseNote;
150
            }
151
        }
152
153 View Code Duplication
        uksort($releaseNotes, function ($v1, $v2) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
154
            return version_compare($v1, $v2, '<') ? -1 : (version_compare($v1, $v2, '>') ? 1 : 0);
155
        });
156
157
        return $releaseNotes;
158
    }
159
160
    /**
161
     * Gets all required information to perform health check.
162
     *
163
     * @param $version
164
     *
165
     * @return mixed
166
     */
167
    public function getHealthCheckInfo($version)
168
    {
169
        $version = $this->getMajorVersion($version);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $version. This often makes code more readable.
Loading history...
170
        $cacheKey = $this->getCacheKey(['health_check', $version]);
171
172
        if ($this->cache->has($cacheKey)) {
173
            return $this->cache->get($cacheKey);
174
        }
175
176
        $url = $this->getHealthCheckInfoUri($version);
177
        $crawler = new Crawler($this->purifyHtml($this->getContent($url), $url));
178
179
        $infoNode = $crawler->filter('#Performing_the_Health_Check_2')->nextAll()->first();
180
        $info = str_replace(['**<', '>**'], '**', $this->htmlConverter->convert($infoNode->html()));
181
182
        $this->cache->set($cacheKey, $info);
183
184
        return $info;
185
    }
186
187
    /**
188
     * Gets all required information to perform upgrade.
189
     *
190
     * @param $version
191
     *
192
     * @return mixed
193
     */
194
    public function getUpgraderInfo($version)
195
    {
196
        $version = $this->getMajorVersion($version);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $version. This often makes code more readable.
Loading history...
197
        $cacheKey = $this->getCacheKey(['upgrader', $version]);
198
199
        if ($this->cache->has($cacheKey)) {
200
            return $this->cache->get($cacheKey);
201
        }
202
203
        $url = $this->getUpgraderInfoUri($version);
204
        $crawler = new Crawler($this->purifyHtml($this->getContent($url), $url));
205
206
        $id = $version == '6.5' ? '#Upgrading_Via_Silent_Upgrader' : '#Performing_the_Upgrade_2';
207
        $nodes = $crawler->filter($id)->nextAll();
208
209
        $content = [];
210 View Code Duplication
        foreach (['ol', 'p'] as $tag) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
211
            if (count($filtered = $nodes->filter($tag))) {
212
                $content[] = sprintf('<%1$s>%2$s</%1$s>', $tag, $filtered->first()->html());
213
            }
214
        }
215
216
        $info = str_replace(['**<', '>**'], '**', $this->htmlConverter->convert(implode('<br>', $content)));
217
218
        $this->cache->set($cacheKey, $info);
219
220
        return $info;
221
    }
222
223
    /**
224
     * Returns the result (response body) of GET request.
225
     *
226
     * @param $url
227
     *
228
     * @return string
229
     */
230
    private function getContent($url)
231
    {
232
        $response = $this->httpClient->request('GET', $url);
233
234
        return $response->getBody()->getContents();
235
    }
236
237
    /**
238
     * @param $version
239
     *
240
     * @return string
241
     */
242
    private function getHealthCheckInfoUri($version)
243
    {
244
        return $this->getUpgradeGuideUri($version);
245
    }
246
247
    /**
248
     * @param $version
249
     *
250
     * @return string
251
     */
252
    private function getUpgraderInfoUri($version)
253
    {
254
        return $this->getUpgradeGuideUri($version);
255
    }
256
257
    /**
258
     * @param $version
259
     *
260
     * @return string
261
     */
262
    private function getUpgradeGuideUri($version)
263
    {
264
        return sprintf(
265
            'Documentation/Sugar_Versions/%s/Ult/Installation_and_Upgrade_Guide/index.html',
266
            $version
267
        );
268
    }
269
270
    /**
271
     * @param $flav
272
     * @param $major
273
     *
274
     * @return string
275
     */
276
    private function getVersionUri($flav, $major)
277
    {
278
        $flav = ucfirst(mb_strtolower($flav));
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $flav. This often makes code more readable.
Loading history...
279
280
        return sprintf('/Documentation/Sugar_Versions/%s/%s/index.html', $major, $flav);
281
    }
282
283
    /**
284
     * Returns version specific release note uri.
285
     *
286
     * @param $flav
287
     * @param $version
288
     *
289
     * @return string
290
     */
291
    private function getReleaseNotesUri($flav, $version)
292
    {
293
        return sprintf(
294
            '/Documentation/Sugar_Versions/%s/%s/Sugar_%s_Release_Notes/index.html',
295
            $this->getMajorVersion($version),
296
            ucfirst(mb_strtolower($flav)),
297
            $version
298
        );
299
    }
300
301
    /**
302
     * @param $version
303
     *
304
     * @return string
305
     */
306
    private function getMajorVersion($version)
307
    {
308
        list($v1, $v2) = explode('.', $version);
309
310
        return implode('.', [$v1, $v2]);
311
    }
312
313
    /**
314
     * Returns cache key.
315
     *
316
     * @param $keyParts
317
     *
318
     * @return string
319
     */
320
    private function getCacheKey(array $keyParts)
321
    {
322
        $delimiter = '___';
323
324
        return implode($delimiter, array_map(function ($key) {
325
            return preg_replace('/[^a-zA-Z0-9_\.]+/', '', mb_strtolower($key));
326
        }, $keyParts));
327
    }
328
329
    /**
330
     * Lightweight HTML purifier.
331
     *
332
     * @param $html
333
     * @param string $url
334
     * @param array  $options
335
     *
336
     * @return string
337
     */
338
    private function purifyHtml($html, $url = '', $options = [])
339
    {
340
        $baseUrl = rtrim($this->httpClient->getConfig('base_uri'), '/') . '/';
341
        if ($url) {
342
            $baseUrl = dirname($baseUrl . ltrim($url, '/')) . '/';
343
        }
344
345
        $options = array_merge([
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $options. This often makes code more readable.
Loading history...
346
            'absolute_urls' => true,
347
            'no_tag_duplicates' => true,
348
            'pre_to_code' => true,
349
        ], $options);
350
351
        return (new Html($baseUrl, $options))->purify($html);
352
    }
353
354
    /**
355
     * Processes request pool.
356
     *
357
     * @param callable $requests
358
     * @param array    $config
359
     *
360
     * @return mixed
361
     */
362
    private function processRequestPool(callable $requests, $config = [])
363
    {
364
        /*
365
         * 1. create request pool
366
         * 2. initiate the transfers and create a promise
367
         * 3. force the pool of requests to complete
368
         */
369
        return (new Pool($this->httpClient, $requests(), $config))->promise()->wait();
370
    }
371
}
372