Completed
Push — master ( c28fef...7014bb )
by James
05:24
created

BrowscapUpdater::fetch()   C

Complexity

Conditions 7
Paths 11

Size

Total Lines 51
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 8.1426

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 51
ccs 20
cts 28
cp 0.7143
rs 6.9743
cc 7
eloc 29
nc 11
nop 2
crap 8.1426

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