Completed
Push — master ( 7d5ab4...247798 )
by James
05:51 queued 10s
created

BrowscapUpdater::fetch()   C

Complexity

Conditions 9
Paths 15

Size

Total Lines 75

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 10.1536

Importance

Changes 0
Metric Value
dl 0
loc 75
ccs 25
cts 33
cp 0.7576
rs 6.9898
c 0
b 0
f 0
cc 9
nc 15
nop 2
crap 10.1536

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