SupportSugarcrm   A
last analyzed

Complexity

Total Complexity 35

Size/Duplication

Total Lines 343
Duplicated Lines 2.92 %

Coupling/Cohesion

Components 1
Dependencies 11

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 10
loc 343
rs 9
wmc 35
lcom 1
cbo 11

14 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
C getVersions() 0 30 7
C getReleaseNotes() 5 68 10
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 getCacheKey() 0 8 1
A purifyHtml() 0 15 2
A processRequestPool() 0 9 1

How to fix   Duplicated Code   

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:

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 Sugarcrm\UpgradeSpec\Version\OrderedList;
12
use Sugarcrm\UpgradeSpec\Version\Version;
13
use Symfony\Component\DomCrawler\Crawler;
14
15
class SupportSugarcrm implements DocProviderInterface
16
{
17
    /**
18
     * @var Client
19
     */
20
    private $httpClient;
21
22
    /**
23
     * @var Cache
24
     */
25
    private $cache;
26
27
    /**
28
     * @var HtmlConverter
29
     */
30
    private $htmlConverter;
31
32
    /**
33
     * SupportSugarcrm constructor.
34
     *
35
     * @param Cache         $cache
36
     * @param HtmlConverter $htmlConverter
37
     */
38
    public function __construct(Cache $cache, HtmlConverter $htmlConverter)
39
    {
40
        $this->httpClient = new Client(['base_uri' => 'http://support.sugarcrm.com/']);
41
        $this->cache = $cache;
42
        $this->htmlConverter = $htmlConverter;
43
    }
44
45
    /**
46
     * Get all available SugarCRM versions (sorted ASC).
47
     *
48
     * @param string $flav
49
     *
50
     * @return OrderedList
51
     */
52
    public function getVersions($flav)
53
    {
54
        if ($this->cache->has($this->getCacheKey([$flav, 'versions']))) {
55
            return $this->cache->get($this->getCacheKey([$flav, 'versions']));
56
        }
57
58
        $crawler = new Crawler($this->getContent('/Documentation/Sugar_Versions/index.html'));
59
        $majorReleases = [];
60
        foreach ($crawler->filter('section.content-body > h1') as $node) {
61
            $majorRelease = $node->textContent;
62
            if (preg_match('/^\d+\.\d+$/', $majorRelease)) {
63
                $majorReleases[] = $majorRelease;
64
            }
65
        }
66
67
        $versions = [];
68
        foreach (new OrderedList($majorReleases) as $version) {
69
            $crawler = new Crawler($this->getContent($this->getVersionUri($flav, $version)));
70
            foreach ($crawler->filter('#Release_Notes')->first()->nextAll()->filter('a') as $node) {
71
                if (preg_match_all('/\d+(\.\d+){1,3}/', $node->textContent, $match)) {
72
                    $versions[] = $match[0][0];
73
                }
74
            }
75
        }
76
77
        $versions = new OrderedList($versions);
78
        $this->cache->set($this->getCacheKey([$flav, 'versions']), $versions);
79
80
        return $versions;
81
    }
82
83
    /**
84
     * @param string $flav
85
     * @param OrderedList $versions
86
     *
87
     * @return array
88
     */
89
    public function getReleaseNotes($flav, OrderedList $versions)
90
    {
91
        $newVersions = $versions->filter(function (Version $version) use ($flav) {
92
            return !$this->cache->has($this->getCacheKey([$flav, 'release_notes', (string) $version]));
93
        });
94
95
        $requests = function () use ($flav, $newVersions) {
96
            foreach ($newVersions as $version) {
97
                yield (string) $version => function () use ($flav, $version) {
98
                    return $this->httpClient->getAsync($this->getReleaseNotesUri($flav, $version));
99
                };
100
            }
101
        };
102
103
        if (count($newVersions)) {
104
            $this->processRequestPool($requests, [
105
                'fulfilled' => function (ResponseInterface $response, $version) use ($flav) {
106
                    $html = $response->getBody()->getContents();
107
                    $crawler = new Crawler($this->purifyHtml($html, $this->getReleaseNotesUri($flav, new Version($version))));
108
109
                    $identifiers = [
110
                        'feature_enhancements' => '#Feature_Enhancements',
111
                        'development_changes' => '#Development_Changes',
112
                    ];
113
114
                    $releaseNote = [];
115
                    foreach ($identifiers as $key => $identifier) {
116
                        $nodes = $crawler->filter($identifier);
117
                        if (count($nodes)) {
118
                            $nextSiblings = $nodes->nextAll();
119
120
                            $content = [];
121 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...
122
                                if (count($filtered = $nextSiblings->filter($tag))) {
123
                                    $content[] = sprintf('<%1$s>%2$s</%1$s>', $tag, $filtered->first()->html());
124
                                }
125
                            }
126
127
                            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...
128
                                $releaseNote[$key] = $this->htmlConverter->convert(implode('<br>', $content));
129
                            }
130
                        }
131
                    }
132
133
                    $this->cache->set($this->getCacheKey([$flav, 'release_notes', $version]), $releaseNote);
134
                },
135
                'rejected' => function ($reason, $version) {
136
                    throw new \RuntimeException(
137
                        sprintf('Can\'t get release notes for version: %s (reason: %s)', $version, $reason)
138
                    );
139
                },
140
            ]);
141
        }
142
143
        $releaseNotes = [];
144
        foreach ($versions as $version) {
145
            $releaseNote = $this->cache->get($this->getCacheKey([$flav, 'release_notes', (string) $version]), null);
146
            if ($releaseNote) {
147
                $releaseNotes[(string) $version] = $releaseNote;
148
            }
149
        }
150
151
//        uksort($releaseNotes, function ($v1, $v2) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
60% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
152
//            return version_compare($v1, $v2, '<') ? -1 : (version_compare($v1, $v2, '>') ? 1 : 0);
153
//        });
154
155
        return $releaseNotes;
156
    }
157
158
    /**
159
     * Gets all required information to perform health check.
160
     *
161
     * @param Version $version
162
     *
163
     * @return mixed
164
     */
165
    public function getHealthCheckInfo(Version $version)
166
    {
167
        $version = $version->getMajor();
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...
168
        $cacheKey = $this->getCacheKey(['health_check', (string) $version]);
169
170
        if ($this->cache->has($cacheKey)) {
171
            return $this->cache->get($cacheKey);
172
        }
173
174
        $url = $this->getHealthCheckInfoUri($version);
175
        $crawler = new Crawler($this->purifyHtml($this->getContent($url), $url));
176
177
        $infoNode = $crawler->filter('#Performing_the_Health_Check_2')->nextAll()->first();
178
        $info = str_replace(['**<', '>**'], '**', $this->htmlConverter->convert($infoNode->html()));
179
180
        $this->cache->set($cacheKey, $info);
181
182
        return $info;
183
    }
184
185
    /**
186
     * Gets all required information to perform upgrade.
187
     *
188
     * @param Version $version
189
     *
190
     * @return mixed
191
     */
192
    public function getUpgraderInfo(Version $version)
193
    {
194
        $version = $version->getMajor();
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...
195
        $cacheKey = $this->getCacheKey(['upgrader', (string) $version]);
196
197
        if ($this->cache->has($cacheKey)) {
198
            return $this->cache->get($cacheKey);
199
        }
200
201
        $url = $this->getUpgraderInfoUri($version);
202
        $crawler = new Crawler($this->purifyHtml($this->getContent($url), $url));
203
204
        $id = $version->isEqualTo(new Version('6.5')) ? '#Upgrading_Via_Silent_Upgrader' : '#Performing_the_Upgrade_2';
205
        $nodes = $crawler->filter($id)->nextAll();
206
207
        $content = [];
208 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...
209
            if (count($filtered = $nodes->filter($tag))) {
210
                $content[] = sprintf('<%1$s>%2$s</%1$s>', $tag, $filtered->first()->html());
211
            }
212
        }
213
214
        $info = str_replace(['**<', '>**'], '**', $this->htmlConverter->convert(implode('<br>', $content)));
215
216
        $this->cache->set($cacheKey, $info);
217
218
        return $info;
219
    }
220
221
    /**
222
     * Returns the result (response body) of GET request.
223
     *
224
     * @param string $url
225
     *
226
     * @return string
227
     */
228
    private function getContent($url)
229
    {
230
        $response = $this->httpClient->request('GET', $url);
231
232
        return $response->getBody()->getContents();
233
    }
234
235
    /**
236
     * @param Version $version
237
     *
238
     * @return string
239
     */
240
    private function getHealthCheckInfoUri(Version $version)
241
    {
242
        return $this->getUpgradeGuideUri($version);
243
    }
244
245
    /**
246
     * @param Version $version
247
     *
248
     * @return string
249
     */
250
    private function getUpgraderInfoUri(Version $version)
251
    {
252
        return $this->getUpgradeGuideUri($version);
253
    }
254
255
    /**
256
     * @param Version $version
257
     *
258
     * @return string
259
     */
260
    private function getUpgradeGuideUri(Version $version)
261
    {
262
        return sprintf(
263
            'Documentation/Sugar_Versions/%s/Ult/Installation_and_Upgrade_Guide/index.html',
264
            $version
265
        );
266
    }
267
268
    /**
269
     * @param string $flav
270
     * @param Version $version
271
     *
272
     * @return string
273
     */
274
    private function getVersionUri($flav, Version $version)
275
    {
276
        $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...
277
278
        return sprintf('/Documentation/Sugar_Versions/%s/%s/index.html', $version, $flav);
279
    }
280
281
    /**
282
     * Returns version specific release note uri.
283
     *
284
     * @param string $flav
285
     * @param Version $version
286
     *
287
     * @return string
288
     */
289
    private function getReleaseNotesUri($flav, Version $version)
290
    {
291
        return sprintf(
292
            '/Documentation/Sugar_Versions/%s/%s/Sugar_%s_Release_Notes/index.html',
293
            $version->getMajor(),
294
            ucfirst(mb_strtolower($flav)),
295
            $version
296
        );
297
    }
298
299
    /**
300
     * Returns cache key.
301
     *
302
     * @param $keyParts
303
     *
304
     * @return string
305
     */
306
    private function getCacheKey(array $keyParts)
307
    {
308
        $delimiter = '___';
309
310
        return implode($delimiter, array_map(function ($key) {
311
            return preg_replace('/[^a-zA-Z0-9_\.]+/', '', mb_strtolower($key));
312
        }, $keyParts));
313
    }
314
315
    /**
316
     * Lightweight HTML purifier.
317
     *
318
     * @param $html
319
     * @param string $url
320
     * @param array  $options
321
     *
322
     * @return string
323
     */
324
    private function purifyHtml($html, $url = '', $options = [])
325
    {
326
        $baseUrl = rtrim($this->httpClient->getConfig('base_uri'), '/') . '/';
327
        if ($url) {
328
            $baseUrl = dirname($baseUrl . ltrim($url, '/')) . '/';
329
        }
330
331
        $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...
332
            'absolute_urls' => true,
333
            'no_tag_duplicates' => true,
334
            'pre_to_code' => true,
335
        ], $options);
336
337
        return (new Html($baseUrl, $options))->purify($html);
338
    }
339
340
    /**
341
     * Processes request pool.
342
     *
343
     * @param callable $requests
344
     * @param array    $config
345
     *
346
     * @return mixed
347
     */
348
    private function processRequestPool(callable $requests, $config = [])
349
    {
350
        /*
351
         * 1. create request pool
352
         * 2. initiate the transfers and create a promise
353
         * 3. force the pool of requests to complete
354
         */
355
        return (new Pool($this->httpClient, $requests(), $config))->promise()->wait();
356
    }
357
}
358