BrowscapUpdater::fetch()   C
last analyzed

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;
0 ignored issues
show
Documentation Bug introduced by
It seems like $client can also be of type object<GuzzleHttp\Client>. However, the property $client is declared as type object<GuzzleHttp\ClientInterface>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
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) {
0 ignored issues
show
Bug introduced by
The class Psr\SimpleCache\InvalidArgumentException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
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) {
0 ignored issues
show
Bug introduced by
The class GuzzleHttp\Exception\GuzzleException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
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) {
0 ignored issues
show
Bug introduced by
The class GuzzleHttp\Exception\GuzzleException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
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) {
0 ignored issues
show
Bug introduced by
The class Psr\SimpleCache\InvalidArgumentException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
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) {
0 ignored issues
show
Bug introduced by
The class GuzzleHttp\Exception\GuzzleException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
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