Completed
Push — master ( 7014bb...a38bcf )
by James
15s
created

BrowscapUpdater::fetch()   B

Complexity

Conditions 7
Paths 11

Size

Total Lines 54
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 7.9295

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 54
ccs 22
cts 30
cp 0.7332
rs 7.8331
cc 7
eloc 31
nc 11
nop 2
crap 7.9295

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