Passed
Push — master ( 385965...a81548 )
by Shaharia
04:48
created

RankChecker::setSearchPageLimit()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 4
rs 10
1
<?php
2
/**
3
 * RankChecker class
4
 *
5
 * @package ShahariaAzam\WPRankChecker
6
 */
7
8
namespace ShahariaAzam\WPRankChecker;
9
10
use Exception;
11
use Psr\Cache\CacheItemPoolInterface;
12
use Psr\Http\Client\ClientExceptionInterface;
13
use Psr\Http\Client\ClientInterface;
14
use ShahariaAzam\HTTPClientSupport\Exception\FlexiHTTPException;
15
use ShahariaAzam\HTTPClientSupport\HTTPSupport;
16
use Symfony\Component\DomCrawler\Crawler;
17
18
/**
19
 * Class RankChecker
20
 *
21
 * @package ShahariaAzam\WPRankChecker
22
 */
23
class RankChecker extends HTTPSupport
24
{
25
    /**
26
     * @var CacheItemPoolInterface
27
     */
28
    private $cache;
29
30
    /**
31
     * How many pages we need to crawl.
32
     * Currently WordPress displays 20 plugins in it's every search result page.
33
     * i.e: https://wordpress.org/plugins/search/mail/page/1
34
     *
35
     * So we will crawl 5 pages to get 100 plugins for that keyword
36
     *
37
     * @var int
38
     */
39
    private $searchPageLimit;
40
41
    /**
42
     * Keyword that we need to check rank for
43
     *
44
     * @var
45
     */
46
    private $keyword;
47
48
    /**
49
     * @var string
50
     */
51
    private $pluginListsHTML;
52
53
    /**
54
     * @var PluginEntity[]
55
     */
56
    private $results;
57
58
    /**
59
     * RankChecker constructor.
60
     *
61
     * @param ClientInterface $httpClient
62
     * @param CacheItemPoolInterface $cache
63
     */
64
    public function __construct(ClientInterface $httpClient, CacheItemPoolInterface $cache = null)
65
    {
66
        $this->setHttpClient($httpClient);
67
        $this->searchPageLimit = 5;
68
        $this->cache = $cache;
69
    }
70
71
    /**
72
     * @param int $searchPageLimit
73
     * @return RankChecker
74
     */
75
    public function setSearchPageLimit(int $searchPageLimit)
76
    {
77
        $this->searchPageLimit = $searchPageLimit;
78
        return $this;
79
    }
80
81
    /**
82
     * @param mixed $keyword
83
     * @return RankChecker
84
     */
85
    public function setKeyword($keyword)
86
    {
87
        $this->keyword = $keyword;
88
        return $this;
89
    }
90
91
    /**
92
     * @return RankChecker
93
     * @throws RankCheckerException
94
     */
95
    public function checkRanks()
96
    {
97
        if (empty($this->keyword)) {
98
            throw new RankCheckerException("You didn't provide any keyword");
99
        }
100
101
        for ($i = 1; $i <= $this->searchPageLimit; $i++) {
102
            $contents = $this->fetchPage($i);
103
            $dom = new Crawler($contents);
104
            $this->filterHTML($dom);
105
        }
106
107
        $this->parseResult();
108
109
        return $this;
110
    }
111
112
    /**
113
     * @return PluginEntity[]
114
     */
115
    public function getResults()
116
    {
117
        return $this->results;
118
    }
119
120
    /**
121
     * @param  $slug
122
     * @return int|null
123
     */
124
    public function getRankBySlug($slug)
125
    {
126
        $result = array_filter(
127
            $this->results,
128
            function (PluginEntity $entity) use ($slug) {
129
                return $entity->getSlug() === $slug;
130
            }
131
        );
132
133
        if (empty($result)) {
134
            return null;
135
        }
136
137
        return array_keys($result)[0];
138
    }
139
140
    /**
141
     * @param int $page
142
     * @return string
143
     * @throws RankCheckerException
144
     */
145
    private function fetchPage($page = 1)
146
    {
147
        $resultPageUrl = sprintf("https://wordpress.org/plugins/search/%s/page/%d", $this->keyword, $page);
148
149
        try {
150
            $response = $this->httpRequest('GET', $resultPageUrl);
151
        } catch (ClientExceptionInterface $e) {
152
            throw new RankCheckerException($e->getMessage(), $e->getCode(), $e->getPrevious());
153
        }
154
155
        if ($response->getStatusCode() !== 200) {
156
            throw new RankCheckerException("Failed to fetch " . $resultPageUrl, $response->getStatusCode());
157
        }
158
159
        $responseBody = (string) $response->getBody();
160
        if (empty($responseBody)) {
161
            throw new RankCheckerException("Empty response from " . $resultPageUrl, $response->getStatusCode());
162
        }
163
164
        return (string) $response->getBody();
165
    }
166
167
    /**
168
     * @param  Crawler $dom
169
     * @return RankChecker
170
     * @throws Exception
171
     */
172
    private function filterHTML(Crawler $dom)
173
    {
174
        $pc = $dom->filter('article.plugin-card');
175
        if ($pc->count() < 1) {
176
            throw new RankCheckerException(".plugin-card HTML node couldn't be found in the DOM tree");
177
        }
178
179
        $pc->each(
180
            function (Crawler $crawler) {
181
                $this->pluginListsHTML .= "<div class='plugin-card'>".$crawler->html()."</div>";
182
            }
183
        );
184
185
        return $this;
186
    }
187
188
    /**
189
     * @return RankChecker
190
     */
191
    private function parseResult()
192
    {
193
        $pcDom = new Crawler($this->pluginListsHTML);
194
195
        $pluginCard = $pcDom->filter('.plugin-card');
196
197
        $rank = 1;
198
199
        $pluginCard->each(
200
            function (Crawler $crawler) use (&$rank) {
201
                $titleDom = $crawler->filter('h3.entry-title');
202
                if (!empty($titleDom)) {
203
                    $title = $titleDom->eq(0);
204
                }
205
206
                $linkDom = $titleDom->filter('a');
207
                if (!empty($linkDom)) {
208
                    $link = $linkDom->attr('href');
209
                }
210
211
                $pluginRatingDom = $crawler->filter('.plugin-rating')->eq(0);
212
                $pluginRating = $pluginRatingDom->filter('.wporg-ratings')->eq(0)->attr('data-rating');
213
214
                $summaryExcerpt = $crawler->filter('.entry-excerpt')->text();
215
                $author = $crawler->filter('.plugin-author')->text();
216
217
                $plugin = new PluginEntity();
218
                $plugin->setTitle($title->text());
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $title does not seem to be defined for all execution paths leading up to this point.
Loading history...
219
                $plugin->setUrl($link);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $link does not seem to be defined for all execution paths leading up to this point.
Loading history...
220
221
                $re = '/\/plugins\/(.*)\//';
222
223
                preg_match($re, $plugin->getUrl(), $matches, PREG_OFFSET_CAPTURE, 1);
224
                if (!empty($matches)) {
225
                    $plugin->setSlug($matches[1][0]);
226
                }
227
228
                $plugin->setRating((float) $pluginRating);
229
                $plugin->setSummary(trim($summaryExcerpt));
230
                $plugin->setAuthor(trim($author));
231
232
233
                $this->results[$rank] = $plugin;
234
                $rank++;
235
            }
236
        );
237
238
        return $this;
239
    }
240
}
241