Issues (2)

src/RankChecker.php (2 issues)

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
        $cacheItemKey = 'WP_Rank_Checker_FetchPage_' . $page;
148
149
        if (!empty($this->cache)) {
150
            if ($this->cache->hasItem($cacheItemKey)) {
151
                return $this->cache->getItem($cacheItemKey)->get();
152
            }
153
        }
154
155
        $resultPageUrl = sprintf("https://wordpress.org/plugins/search/%s/page/%d", $this->keyword, $page);
156
157
        try {
158
            $response = $this->httpRequest('GET', $resultPageUrl);
159
        } catch (ClientExceptionInterface $e) {
160
            throw new RankCheckerException($e->getMessage(), $e->getCode(), $e->getPrevious());
161
        }
162
163
        if ($response->getStatusCode() !== 200) {
164
            throw new RankCheckerException("Failed to fetch " . $resultPageUrl, $response->getStatusCode());
165
        }
166
167
        $responseBody = (string) $response->getBody();
168
        if (empty($responseBody)) {
169
            throw new RankCheckerException("Empty response from " . $resultPageUrl, $response->getStatusCode());
170
        }
171
172
        $responseData = (string) $response->getBody();
173
174
        if (!empty($this->cache)) {
175
            $cachedItem = $this->cache->getItem($cacheItemKey);
176
            $cachedItem->set($responseData);
177
            $this->cache->save($cachedItem);
178
        }
179
180
        return $responseData;
181
    }
182
183
    /**
184
     * @param  Crawler $dom
185
     * @return RankChecker
186
     * @throws Exception
187
     */
188
    private function filterHTML(Crawler $dom)
189
    {
190
        $pc = $dom->filter('article.plugin-card');
191
        if ($pc->count() < 1) {
192
            throw new RankCheckerException(".plugin-card HTML node couldn't be found in the DOM tree");
193
        }
194
195
        $pc->each(
196
            function (Crawler $crawler) {
197
                $this->pluginListsHTML .= "<div class='plugin-card'>".$crawler->html()."</div>";
198
            }
199
        );
200
201
        return $this;
202
    }
203
204
    /**
205
     * @return RankChecker
206
     */
207
    private function parseResult()
208
    {
209
        $pcDom = new Crawler($this->pluginListsHTML);
210
211
        $pluginCard = $pcDom->filter('.plugin-card');
212
213
        $rank = 1;
214
215
        $pluginCard->each(
216
            function (Crawler $crawler) use (&$rank) {
217
                $titleDom = $crawler->filter('h3.entry-title');
218
                if (!empty($titleDom)) {
219
                    $title = $titleDom->eq(0);
220
                }
221
222
                $linkDom = $titleDom->filter('a');
223
                if (!empty($linkDom)) {
224
                    $link = $linkDom->attr('href');
225
                }
226
227
                $pluginRatingDom = $crawler->filter('.plugin-rating')->eq(0);
228
                $pluginRating = $pluginRatingDom->filter('.wporg-ratings')->eq(0)->attr('data-rating');
229
230
                $summaryExcerpt = $crawler->filter('.entry-excerpt')->text();
231
                $author = $crawler->filter('.plugin-author')->text();
232
233
                $plugin = new PluginEntity();
234
                $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...
235
                $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...
236
237
                $re = '/\/plugins\/(.*)\//';
238
239
                preg_match($re, $plugin->getUrl(), $matches, PREG_OFFSET_CAPTURE, 1);
240
                if (!empty($matches)) {
241
                    $plugin->setSlug($matches[1][0]);
242
                }
243
244
                $plugin->setRating((float) $pluginRating);
245
                $plugin->setSummary(trim($summaryExcerpt));
246
                $plugin->setAuthor(trim($author));
247
248
249
                $this->results[$rank] = $plugin;
250
                $rank++;
251
            }
252
        );
253
254
        return $this;
255
    }
256
}
257