Completed
Push — master ( d4f070...1ddfcf )
by Jan-Petter
05:06
created

Cache   A

Complexity

Total Complexity 30

Size/Duplication

Total Lines 299
Duplicated Lines 8.36 %

Coupling/Cohesion

Components 2
Dependencies 5

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 30
c 1
b 0
f 0
lcom 2
cbo 5
dl 25
loc 299
rs 10

10 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A pdoInitialize() 0 18 4
B client() 0 29 3
A clockSyncCheck() 0 6 2
A markAsActive() 14 14 2
B push() 0 29 5
B displacePush() 0 30 3
B cron() 0 28 4
B setWorkerID() 0 13 5
A clean() 11 11 1

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

1
<?php
2
namespace vipnytt\RobotsTxtParser;
3
4
use PDO;
5
use vipnytt\RobotsTxtParser\Exceptions\SQLException;
6
use vipnytt\RobotsTxtParser\Parser\UrlParser;
7
use vipnytt\RobotsTxtParser\SQL\SQLInterface;
8
use vipnytt\RobotsTxtParser\SQL\TableConstructor;
9
10
/**
11
 * Class Cache
12
 *
13
 * @package vipnytt\RobotsTxtParser
14
 */
15
class Cache implements RobotsTxtInterface, SQLInterface
16
{
17
    use UrlParser;
18
19
    /**
20
     * Supported database drivers
21
     */
22
    const SUPPORTED_DRIVERS = [
23
        self::DRIVER_MYSQL,
24
    ];
25
26
    /**
27
     * Database connection
28
     * @var PDO
29
     */
30
    private $pdo;
31
32
    /**
33
     * GuzzleHTTP config
34
     * @var array
35
     */
36
    private $guzzleConfig = [];
37
38
    /**
39
     * Byte limit
40
     * @var int|null
41
     */
42
    private $byteLimit = self::BYTE_LIMIT;
43
44
    /**
45
     * Client nextUpdate margin in seconds
46
     * @var int
47
     */
48
    private $clientUpdateMargin = 300;
49
50
    /**
51
     * PDO driver
52
     * @var string
53
     */
54
    private $driver;
55
56
    /**
57
     * Cache constructor.
58
     *
59
     * @param PDO $pdo
60
     * @param array $guzzleConfig
61
     * @param int|null $byteLimit
62
     */
63
    public function __construct(PDO $pdo, array $guzzleConfig = [], $byteLimit = self::BYTE_LIMIT)
64
    {
65
        $this->pdo = $this->pdoInitialize($pdo);
66
        $this->guzzleConfig = $guzzleConfig;
67
        $this->byteLimit = $byteLimit;
68
    }
69
70
    /**
71
     * Initialize PDO connection
72
     *
73
     * @param PDO $pdo
74
     * @return PDO
75
     * @throws SQLException
76
     */
77
    private function pdoInitialize(PDO $pdo)
78
    {
79
        if ($pdo->getAttribute(PDO::ATTR_ERRMODE) === PDO::ERRMODE_SILENT) {
80
            $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING);
81
        }
82
        $pdo->setAttribute(PDO::ATTR_CASE, PDO::CASE_NATURAL);
83
        $pdo->setAttribute(PDO::ATTR_ORACLE_NULLS, PDO::NULL_NATURAL);
84
        $pdo->exec('SET NAMES ' . self::SQL_ENCODING);
85
        $this->driver = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
86
        if (!in_array($this->driver, self::SUPPORTED_DRIVERS)) {
87
            throw new SQLException('Unsupported database. ' . self::README_SQL_CACHE);
88
        }
89
        $tableConstructor = new TableConstructor($pdo, self::TABLE_CACHE);
90
        if ($tableConstructor->exists() === false) {
91
            $tableConstructor->create(file_get_contents(__DIR__ . '/SQL/cache.sql'), self::README_SQL_CACHE);
92
        }
93
        return $pdo;
94
    }
95
96
    /**
97
     * Parser client
98
     *
99
     * @param string $baseUri
100
     * @return TxtClient
101
     */
102
    public function client($baseUri)
103
    {
104
        $base = $this->urlBase($this->urlEncode($baseUri));
105
        $query = $this->pdo->prepare(<<<SQL
106
SELECT
107
  content,
108
  statusCode,
109
  nextUpdate,
110
  worker,
111
  UNIX_TIMESTAMP()
112
FROM robotstxt__cache0
113
WHERE base = :base;
114
SQL
115
        );
116
        $query->bindParam(':base', $base, PDO::PARAM_STR);
117
        $query->execute();
118
        if ($query->rowCount() > 0) {
119
            $row = $query->fetch(PDO::FETCH_ASSOC);
120
            $this->clockSyncCheck($row['UNIX_TIMESTAMP()']);
121
            if ($row['nextUpdate'] > ($row['UNIX_TIMESTAMP()'] - $this->clientUpdateMargin)) {
122
                $this->markAsActive($base, $row['worker']);
123
                return new TxtClient($base, $row['statusCode'], $row['content'], self::ENCODING, $this->byteLimit);
124
            }
125
        }
126
        $request = new UriClient($base, $this->guzzleConfig, $this->byteLimit);
127
        $this->push($request);
128
        $this->markAsActive($base);
129
        return new TxtClient($base, $request->getStatusCode(), $request->getContents(), self::ENCODING, $this->byteLimit);
130
    }
131
132
    /**
133
     * Clock sync check
134
     *
135
     * @param int $time
136
     * @throws SQLException
137
     */
138
    private function clockSyncCheck($time)
139
    {
140
        if (abs(time() - $time) > 10) {
141
            throw new SQLException('`PHP server` and `SQL server` timestamps are out of sync. Please fix!');
142
        }
143
    }
144
145
    /**
146
     * Mark robots.txt as active
147
     *
148
     * @param string $base
149
     * @param int|null $workerID
150
     * @return bool
151
     */
152 View Code Duplication
    private function markAsActive($base, $workerID = 0)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
153
    {
154
        if ($workerID == 0) {
155
            $query = $this->pdo->prepare(<<<SQL
156
UPDATE robotstxt__cache0
157
SET worker = NULL
158
WHERE base = :base AND worker = 0;
159
SQL
160
            );
161
            $query->bindParam(':base', $base, PDO::PARAM_STR);
162
            return $query->execute();
163
        }
164
        return true;
165
    }
166
167
    /**
168
     * Update an robots.txt in the database
169
     *
170
     * @param UriClient $client
171
     * @return bool
172
     */
173
    private function push(UriClient $client)
174
    {
175
        $base = $client->getBaseUri();
176
        $statusCode = $client->getStatusCode();
177
        $nextUpdate = $client->nextUpdate();
178
        if (
179
            $statusCode >= 500 &&
180
            $statusCode < 600 &&
181
            mb_stripos($base, 'http') === 0 &&
182
            $this->displacePush($base, $nextUpdate)
183
        ) {
184
            return true;
185
        }
186
        $validUntil = $client->validUntil();
187
        $content = $client->render();
188
        $query = $this->pdo->prepare(<<<SQL
189
INSERT INTO robotstxt__cache0 (base, content, statusCode, validUntil, nextUpdate)
190
VALUES (:base, :content, :statusCode, :validUntil, :nextUpdate)
191
ON DUPLICATE KEY UPDATE content = :content, statusCode = :statusCode, validUntil = :validUntil,
192
  nextUpdate = :nextUpdate, worker = 0;
193
SQL
194
        );
195
        $query->bindParam(':base', $base, PDO::PARAM_STR);
196
        $query->bindParam(':content', $content, PDO::PARAM_STR);
197
        $query->bindParam(':statusCode', $statusCode, PDO::PARAM_INT);
198
        $query->bindParam(':validUntil', $validUntil, PDO::PARAM_INT);
199
        $query->bindParam(':nextUpdate', $nextUpdate, PDO::PARAM_INT);
200
        return $query->execute();
201
    }
202
203
    /**
204
     * Displace push timestamp
205
     *
206
     * @param string $base
207
     * @param int $nextUpdate
208
     * @return bool
209
     */
210
    private function displacePush($base, $nextUpdate)
211
    {
212
        $query = $this->pdo->prepare(<<<SQL
213
SELECT
214
  validUntil,
215
  UNIX_TIMESTAMP()
216
FROM robotstxt__cache0
217
WHERE base = :base;
218
SQL
219
        );
220
        $query->bindParam(':base', $base, PDO::PARAM_STR);
221
        $query->execute();
222
        if ($query->rowCount() > 0) {
223
            $row = $query->fetch(PDO::FETCH_ASSOC);
224
            $this->clockSyncCheck($row['UNIX_TIMESTAMP()']);
225
            if ($row['validUntil'] > $row['UNIX_TIMESTAMP()']) {
226
                $nextUpdate = min($row['validUntil'], $nextUpdate);
227
                $query = $this->pdo->prepare(<<<SQL
228
UPDATE robotstxt__cache0
229
SET nextUpdate = :nextUpdate, worker = NULL
230
WHERE base = :base;
231
SQL
232
                );
233
                $query->bindParam(':base', $base, PDO::PARAM_STR);
234
                $query->bindParam(':nextUpdate', $nextUpdate, PDO::PARAM_INT);
235
                return $query->execute();
236
            }
237
        }
238
        return false;
239
    }
240
241
    /**
242
     * Process the update queue
243
     *
244
     * @param int|null $workerID
245
     * @return bool
246
     */
247
    public function cron($workerID = null)
248
    {
249
        $worker = $this->setWorkerID($workerID);
250
        $result = true;
251
        while ($result) {
252
            $query = $this->pdo->prepare(<<<SQL
253
UPDATE robotstxt__cache0
254
SET worker = :workerID
255
WHERE worker IS NULL AND nextUpdate <= UNIX_TIMESTAMP()
256
ORDER BY nextUpdate ASC
257
LIMIT 1;
258
SELECT base
259
FROM robotstxt__cache0
260
WHERE worker = :workerID;
261
SQL
262
            );
263
            $query->bindParam(':workerID', $worker, PDO::PARAM_INT);
264
            $query->execute();
265
            if ($query->rowCount() > 0) {
266
                while ($row = $query->fetch(PDO::FETCH_ASSOC)) {
267
                    $result = $this->push(new UriClient($row['base'], $this->guzzleConfig, $this->byteLimit));
268
                }
269
                continue;
270
            }
271
            return true;
272
        }
273
        return false;
274
    }
275
276
    /**
277
     * Set WorkerID
278
     *
279
     * @param int|null $workerID
280
     * @return int
281
     */
282
    protected function setWorkerID($workerID = null)
283
    {
284
        if (
285
            is_int($workerID) &&
286
            $workerID <= 255 &&
287
            $workerID >= 1
288
        ) {
289
            return $workerID;
290
        } elseif ($workerID !== null) {
291
            trigger_error('WorkerID out of range (1-255)', E_USER_WARNING);
292
        }
293
        return rand(1, 255);
294
    }
295
296
    /**
297
     * Clean the cache table
298
     *
299
     * @param int $delay - in seconds
300
     * @return bool
301
     */
302 View Code Duplication
    public function clean($delay = 600)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
303
    {
304
        $delay = self::CACHE_TIME + $delay;
305
        $query = $this->pdo->prepare(<<<SQL
306
DELETE FROM robotstxt__cache0
307
WHERE worker = 0 AND nextUpdate < (UNIX_TIMESTAMP() - :delay);
308
SQL
309
        );
310
        $query->bindParam(':delay', $delay, PDO::PARAM_INT);
311
        return $query->execute();
312
    }
313
}
314