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

SupportSugarcrm   B

Complexity

Total Complexity 40

Size/Duplication

Total Lines 359
Duplicated Lines 4.46 %

Coupling/Cohesion

Components 1
Dependencies 9

Importance

Changes 0
Metric Value
dl 16
loc 359
rs 8.2608
c 0
b 0
f 0
wmc 40
lcom 1
cbo 9

15 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
D getVersions() 3 34 9
C getReleaseNotes() 8 68 12
A getHealthCheckInfo() 0 19 2
B getUpgraderInfo() 5 28 5
A getContent() 0 6 1
A getHealthCheckInfoUri() 0 4 1
A getUpgraderInfoUri() 0 4 1
A getUpgradeGuideUri() 0 7 1
A getVersionUri() 0 6 1
A getReleaseNotesUri() 0 9 1
A getMajorVersion() 0 6 1
A getCacheKey() 0 8 1
A purifyHtml() 0 15 2
A processRequestPool() 0 9 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like SupportSugarcrm 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 SupportSugarcrm, and based on these observations, apply Extract Interface, too.

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