Completed
Push — master ( 41d47b...c28fef )
by James
10s
created

BrowscapUpdater::checkUpdate()   B

Complexity

Conditions 8
Paths 6

Size

Total Lines 55
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 8.0231

Importance

Changes 0
Metric Value
dl 0
loc 55
ccs 26
cts 28
cp 0.9286
rs 7.4033
c 0
b 0
f 0
cc 8
eloc 29
nc 6
nop 0
crap 8.0231

How to fix   Long Method   

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
declare(strict_types = 1);
3
4
namespace BrowscapPHP;
5
6
use BrowscapPHP\Cache\BrowscapCache;
7
use BrowscapPHP\Cache\BrowscapCacheInterface;
8
use BrowscapPHP\Exception\FetcherException;
9
use BrowscapPHP\Helper\Converter;
10
use BrowscapPHP\Helper\Filesystem;
11
use BrowscapPHP\Helper\IniLoader;
12
use GuzzleHttp\Client;
13
use GuzzleHttp\ClientInterface;
14
use Psr\Log\LoggerInterface;
15
use Psr\Log\NullLogger;
16
use WurflCache\Adapter\AdapterInterface;
17
use WurflCache\Adapter\File;
18
19
/**
20
 * Browscap.ini parsing class with caching and update capabilities
21
 */
22
final class BrowscapUpdater
23
{
24
    /**
25
     * The cache instance
26
     *
27
     * @var \BrowscapPHP\Cache\BrowscapCacheInterface|null
28
     */
29
    private $cache;
30
31
    /**
32
     * @var @var \Psr\Log\LoggerInterface|null
33
     */
34
    private $logger;
35
36
    /**
37
     * @var \GuzzleHttp\ClientInterface|null
38
     */
39
    private $client;
40
41
    /**
42
     * Curl connect timeout in seconds
43
     *
44
     * @var int
45
     */
46
    private $connectTimeout = 5;
47
48
    /**
49
     * Gets a cache instance
50
     *
51
     * @return \BrowscapPHP\Cache\BrowscapCacheInterface
52
     */
53 14
    public function getCache() : BrowscapCacheInterface
54
    {
55 14
        if (null === $this->cache) {
56 1
            $cacheDirectory = __DIR__ . '/../resources/';
57
58 1
            $cacheAdapter = new File(
59 1
                [File::DIR => $cacheDirectory]
60
            );
61
62 1
            $this->cache = new BrowscapCache($cacheAdapter);
63
        }
64
65 14
        return $this->cache;
66
    }
67
68
    /**
69
     * Sets a cache instance
70
     *
71
     * @param \BrowscapPHP\Cache\BrowscapCacheInterface|\WurflCache\Adapter\AdapterInterface $cache
72
     * @throws \BrowscapPHP\Exception
73
     * @return self
74
     */
75 14
    public function setCache($cache) : self
76
    {
77 14
        if ($cache instanceof BrowscapCacheInterface) {
78 10
            $this->cache = $cache;
79 4
        } elseif ($cache instanceof AdapterInterface) {
80 3
            $this->cache = new BrowscapCache($cache);
81
        } else {
82 1
            throw new Exception(
83
                'the cache has to be an instance of \BrowscapPHP\Cache\BrowscapCacheInterface or '
84 1
                . 'an instanceof of \WurflCache\Adapter\AdapterInterface',
85 1
                Exception::CACHE_INCOMPATIBLE
86
            );
87
        }
88
89 13
        return $this;
90
    }
91
92
    /**
93
     * Sets a logger instance
94
     *
95
     * @param \Psr\Log\LoggerInterface $logger
96
     *
97
     * @return self
98
     */
99 10
    public function setLogger(LoggerInterface $logger) : self
100
    {
101 10
        $this->logger = $logger;
102
103 10
        return $this;
104
    }
105
106
    /**
107
     * returns a logger instance
108
     *
109
     * @return \Psr\Log\LoggerInterface
110
     */
111 11
    public function getLogger() : LoggerInterface
112
    {
113 11
        if (null === $this->logger) {
114 3
            $this->logger = new NullLogger();
115
        }
116
117 11
        return $this->logger;
118
    }
119
120
    /**
121
     * Sets the Connection Timeout
122
     *
123
     * @param int $connectTimeout
124
     */
125 1
    public function setConnectTimeout(int $connectTimeout) : void
126
    {
127 1
        $this->connectTimeout = $connectTimeout;
128 1
    }
129
130 10
    public function getClient() : ClientInterface
131
    {
132 10
        if (null === $this->client) {
133 1
            $this->client = new Client();
134
        }
135
136 10
        return $this->client;
137
    }
138
139 10
    public function setClient(ClientInterface $client)
140
    {
141 10
        $this->client = $client;
142 10
    }
143
144
    /**
145
     * reads and parses an ini file and writes the results into the cache
146
     *
147
     * @param string $iniFile
148
     * @throws \BrowscapPHP\Exception
149
     */
150 3
    public function convertFile(string $iniFile) : void
151
    {
152 3
        if (empty($iniFile)) {
153 1
            throw new Exception('the file name can not be empty');
154
        }
155
156 2
        if (! is_readable($iniFile)) {
157 1
            throw new Exception('it was not possible to read the local file ' . $iniFile);
158
        }
159
160
        try {
161 1
            $iniString = file_get_contents($iniFile);
162
        } catch (Helper\Exception $e) {
163
            throw new Exception('an error occured while converting the local file into the cache', 0, $e);
164
        }
165
166 1
        $this->convertString($iniString);
167 1
    }
168
169
    /**
170
     * reads and parses an ini string and writes the results into the cache
171
     *
172
     * @param string $iniString
173
     */
174 2
    public function convertString(string $iniString) : void
175
    {
176 2
        $cachedVersion = $this->getCache()->getItem('browscap.version', false, $success);
177 2
        $converter = new Converter($this->getLogger(), $this->getCache());
178
179 2
        $this->storeContent($converter, $iniString, $cachedVersion);
180 2
    }
181
182
    /**
183
     * fetches a remote file and stores it into a local folder
184
     *
185
     * @param string $file The name of the file where to store the remote content
186
     * @param string $remoteFile The code for the remote file to load
187
     *
188
     * @throws \BrowscapPHP\Exception\FetcherException
189
     * @throws \BrowscapPHP\Helper\Exception
190
     * @throws \GuzzleHttp\Exception\GuzzleException
191
     */
192 3
    public function fetch(string $file, string $remoteFile = IniLoader::PHP_INI) : void
193
    {
194 3
        if (null === ($cachedVersion = $this->checkUpdate())) {
195
            // no newer version available
196
            return;
197
        }
198
199 2
        $this->getLogger()->debug('started fetching remote file');
200
201 2
        $uri = (new IniLoader())->setRemoteFilename($remoteFile)->getRemoteIniUrl();
202
203
        /** @var \Psr\Http\Message\ResponseInterface $response */
204 2
        $response = $this->getClient()->request('get', $uri, ['connect_timeout' => $this->connectTimeout]);
205
206 2
        if ($response->getStatusCode() !== 200) {
207
            throw new FetcherException(
208
                'an error occured while fetching remote data from URI ' . $uri . ': StatusCode was '
209
                . $response->getStatusCode()
210
            );
211
        }
212
213
        try {
214 2
            $content = $response->getBody()->getContents();
215
        } catch (\Exception $e) {
216
            throw new FetcherException('an error occured while fetching remote data', 0, $e);
217
        }
218
219 2
        if (empty($content)) {
220
            $error = error_get_last();
221
            throw FetcherException::httpError($uri, $error['message']);
222
        }
223
224 2
        $this->getLogger()->debug('finished fetching remote file');
225 2
        $this->getLogger()->debug('started storing remote file into local file');
226
227 2
        $content = $this->sanitizeContent($content);
228
229 2
        $converter = new Converter($this->getLogger(), $this->getCache());
230 2
        $iniVersion = $converter->getIniVersion($content);
231
232 2
        if ($iniVersion > $cachedVersion) {
233 2
            $fs = new Filesystem();
234 2
            $fs->dumpFile($file, $content);
235
        }
236
237 2
        $this->getLogger()->debug('finished storing remote file into local file');
238 2
    }
239
240
    /**
241
     * fetches a remote file, parses it and writes the result into the cache
242
     *
243
     * if the local stored information are in the same version as the remote data no actions are
244
     * taken
245
     *
246
     * @param string $remoteFile The code for the remote file to load
247
     *
248
     * @throws \BrowscapPHP\Exception\FileNotFoundException
249
     * @throws \BrowscapPHP\Helper\Exception
250
     * @throws \BrowscapPHP\Exception\FetcherException
251
     * @throws \GuzzleHttp\Exception\GuzzleException
252
     */
253 2
    public function update(string $remoteFile = IniLoader::PHP_INI) : void
254
    {
255 2
        $this->getLogger()->debug('started fetching remote file');
256
257 2
        if (null === ($cachedVersion = $this->checkUpdate())) {
258
            // no newer version available
259
            return;
260
        }
261
262 2
        $uri = (new IniLoader())->setRemoteFilename($remoteFile)->getRemoteIniUrl();
263
264
        /** @var \Psr\Http\Message\ResponseInterface $response */
265 2
        $response = $this->getClient()->request('get', $uri, ['connect_timeout' => $this->connectTimeout]);
266
267 2
        if ($response->getStatusCode() !== 200) {
268
            throw new FetcherException(
269
                'an error occured while fetching remote data from URI ' . $uri . ': StatusCode was '
270
                . $response->getStatusCode()
271
            );
272
        }
273
274
        try {
275 2
            $content = $response->getBody()->getContents();
276
        } catch (\Exception $e) {
277
            throw new FetcherException('an error occured while fetching remote data', 0, $e);
278
        }
279
280 2
        if (empty($content)) {
281 1
            $error = error_get_last();
282
283 1
            throw FetcherException::httpError($uri, $error['message'] ?? '');
284
        }
285
286 1
        $this->getLogger()->debug('finished fetching remote file');
287
288 1
        $converter = new Converter($this->getLogger(), $this->getCache());
289
290 1
        $this->storeContent($converter, $content, $cachedVersion);
291 1
    }
292
293
    /**
294
     * checks if an update on a remote location for the local file or the cache
295
     *
296
     * @throws \BrowscapPHP\Helper\Exception
297
     * @throws \BrowscapPHP\Exception\FetcherException
298
     * @return int|null The actual cached version if a newer version is available, null otherwise
299
     * @throws \GuzzleHttp\Exception\GuzzleException
300
     */
301 9
    public function checkUpdate() : ?int
302
    {
303 9
        $success = null;
304 9
        $cachedVersion = $this->getCache()->getItem('browscap.version', false, $success);
305
306 9
        if (! $cachedVersion) {
307
            // could not load version from cache
308 5
            $this->getLogger()->info('there is no cached version available, please update from remote');
309
310 5
            return 0;
311
        }
312
313 4
        $uri = (new IniLoader())->getRemoteVersionUrl();
314
315
        /** @var \Psr\Http\Message\ResponseInterface $response */
316 4
        $response = $this->getClient()->request('get', $uri, ['connect_timeout' => $this->connectTimeout]);
317
318 4
        if ($response->getStatusCode() !== 200) {
319 1
            throw new FetcherException(
320 1
                'an error occured while fetching version data from URI ' . $uri . ': StatusCode was '
321 1
                . $response->getStatusCode()
322
            );
323
        }
324
325
        try {
326 3
            $remoteVersion = $response->getBody()->getContents();
327 1
        } catch (\Exception $e) {
328 1
            throw new FetcherException(
329 1
                'an error occured while fetching version data from URI ' . $uri . ': StatusCode was '
330 1
                . $response->getStatusCode(),
331 1
                0,
332 1
                $e
333
            );
334
        }
335
336 2
        if (! $remoteVersion) {
337
            // could not load remote version
338
            $this->getLogger()->info('could not load version from remote location');
339
340
            return 0;
341
        }
342
343 2
        if ($cachedVersion && $remoteVersion && $remoteVersion <= $cachedVersion) {
344
            // no newer version available
345 1
            $this->getLogger()->info('there is no newer version available');
346
347 1
            return null;
348
        }
349
350 1
        $this->getLogger()->info(
351 1
            'a newer version is available, local version: ' . $cachedVersion . ', remote version: ' . $remoteVersion
352
        );
353
354 1
        return (int) $cachedVersion;
355
    }
356
357 5
    private function sanitizeContent(string $content) : string
358
    {
359
        // replace everything between opening and closing php and asp tags
360 5
        $content = preg_replace('/<[?%].*[?%]>/', '', $content);
361
362
        // replace opening and closing php and asp tags
363 5
        return str_replace(['<?', '<%', '?>', '%>'], '', $content);
364
    }
365
366
    /**
367
     * reads and parses an ini string and writes the results into the cache
368
     *
369
     * @param \BrowscapPHP\Helper\Converter $converter
370
     * @param string                        $content
371
     * @param int|null                      $cachedVersion
372
     */
373 3
    private function storeContent(Converter $converter, string $content, ?int $cachedVersion)
374
    {
375 3
        $iniString = $this->sanitizeContent($content);
376 3
        $iniVersion = $converter->getIniVersion($iniString);
377
378 3
        if (! $cachedVersion || $iniVersion > $cachedVersion) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $cachedVersion of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
379
            $converter
380 3
                ->storeVersion()
381 3
                ->convertString($iniString);
382
        }
383 3
    }
384
}
385