Completed
Push — master ( 77714c...0ccfde )
by Jan-Petter
02:23
created

Cache::push()   C

Complexity

Conditions 7
Paths 4

Size

Total Lines 36
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 2 Features 0
Metric Value
c 4
b 2
f 0
dl 0
loc 36
rs 6.7272
cc 7
eloc 25
nc 4
nop 1
1
<?php
2
namespace vipnytt\RobotsTxtParser;
3
4
use PDO;
5
use vipnytt\RobotsTxtParser\Exceptions\SQLException;
6
use vipnytt\RobotsTxtParser\Parser\UriParser;
7
use vipnytt\RobotsTxtParser\SQL\SQLInterface;
8
9
/**
10
 * Class Cache
11
 *
12
 * @package vipnytt\RobotsTxtParser
13
 */
14
class Cache implements RobotsTxtInterface, SQLInterface
15
{
16
    use UriParser;
17
18
    /**
19
     * Supported database drivers
20
     */
21
    const SUPPORTED_DRIVERS = [
22
        self::DRIVER_MYSQL,
23
    ];
24
25
    /**
26
     * Client nextUpdate margin in seconds
27
     * @var int
28
     */
29
    protected $clientUpdateMargin = 300;
30
31
    /**
32
     * Database handler
33
     * @var PDO
34
     */
35
    private $pdo;
36
37
    /**
38
     * cURL options
39
     * @var array
40
     */
41
    private $curlOptions = [];
42
43
    /**
44
     * Byte limit
45
     * @var int|null
46
     */
47
    private $byteLimit = self::BYTE_LIMIT;
48
49
    /**
50
     * PDO driver
51
     * @var string
52
     */
53
    private $driver;
54
55
    /**
56
     * Cache constructor.
57
     *
58
     * @param PDO $pdo
59
     * @param array $curlOptions
60
     * @param int|null $byteLimit
61
     */
62
    public function __construct(PDO $pdo, array $curlOptions = [], $byteLimit = self::BYTE_LIMIT)
63
    {
64
        $this->pdo = $this->pdoInitialize($pdo);
65
        $this->curlOptions = $curlOptions;
66
        $this->byteLimit = $byteLimit;
67
    }
68
69
    /**
70
     * Initialize PDO connection
71
     *
72
     * @param PDO $pdo
73
     * @return PDO
74
     * @throws SQLException
75
     */
76
    private function pdoInitialize(PDO $pdo)
77
    {
78
        if ($pdo->getAttribute(PDO::ATTR_ERRMODE) === PDO::ERRMODE_SILENT) {
79
            $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING);
80
        }
81
        $pdo->setAttribute(PDO::ATTR_CASE, PDO::CASE_NATURAL);
82
        $pdo->setAttribute(PDO::ATTR_ORACLE_NULLS, PDO::NULL_NATURAL);
83
        $pdo->exec('SET NAMES ' . self::SQL_ENCODING);
84
        $this->driver = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
85
        if (!in_array($this->driver, self::SUPPORTED_DRIVERS)) {
86
            throw new SQLException('Unsupported database. ' . self::README_SQL_CACHE);
87
        }
88
        try {
89
            $pdo->query("SELECT 1 FROM robotstxt__cache1 LIMIT 1;");
90
        } catch (\Exception $exception1) {
91
            try {
92
                $pdo->query(file_get_contents(__DIR__ . '/SQL/cache.sql'));
93
            } catch (\Exception $exception2) {
94
                throw new SQLException('Missing table `' . self::TABLE_CACHE . '`. Setup instructions: ' . self::README_SQL_CACHE);
95
            }
96
        }
97
        return $pdo;
98
    }
99
100
    /**
101
     * Parser client
102
     *
103
     * @param string $baseUri
104
     * @return TxtClient
105
     */
106
    public function client($baseUri)
107
    {
108
        $base = $this->urlBase($baseUri);
109
        $query = $this->pdo->prepare(<<<SQL
110
SELECT
111
  content,
112
  statusCode,
113
  nextUpdate,
114
  effective,
115
  worker,
116
  UNIX_TIMESTAMP()
117
FROM robotstxt__cache1
118
WHERE base = :base;
119
SQL
120
        );
121
        $query->bindParam(':base', $base, PDO::PARAM_STR);
122
        $query->execute();
123
        if ($query->rowCount() > 0) {
124
            $row = $query->fetch(PDO::FETCH_ASSOC);
125
            $this->clockSyncCheck($row['UNIX_TIMESTAMP()']);
126
            if ($row['nextUpdate'] > ($row['UNIX_TIMESTAMP()'] - $this->clientUpdateMargin)) {
127
                $this->markAsActive($base, $row['worker']);
128
                return new TxtClient($base, $row['statusCode'], $row['content'], self::ENCODING, $row['effective'], $this->byteLimit);
129
            }
130
        }
131
        $request = new UriClient($base, $this->curlOptions, $this->byteLimit);
132
        $this->push($request);
133
        $this->markAsActive($base);
134
        return new TxtClient($base, $request->getStatusCode(), $request->getContents(), $request->getEncoding(), $request->getEffectiveUri(), $this->byteLimit);
135
    }
136
137
    /**
138
     * Clock sync check
139
     *
140
     * @param int $time
141
     * @throws SQLException
142
     */
143
    private function clockSyncCheck($time)
144
    {
145
        if (abs(time() - $time) >= 10) {
146
            throw new SQLException('`PHP server` and `SQL server` timestamps are out of sync. Please fix!');
147
        }
148
    }
149
150
    /**
151
     * Mark robots.txt as active
152
     *
153
     * @param string $base
154
     * @param int|null $workerID
155
     * @return bool
156
     */
157 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...
158
    {
159
        if ($workerID == 0) {
160
            $query = $this->pdo->prepare(<<<SQL
161
UPDATE robotstxt__cache1
162
SET worker = NULL
163
WHERE base = :base AND worker = 0;
164
SQL
165
            );
166
            $query->bindParam(':base', $base, PDO::PARAM_STR);
167
            return $query->execute();
168
        }
169
        return true;
170
    }
171
172
    /**
173
     * Update an robots.txt in the database
174
     *
175
     * @param UriClient $client
176
     * @return bool
177
     */
178
    private function push(UriClient $client)
179
    {
180
        $base = $client->getBaseUri();
181
        $statusCode = $client->getStatusCode();
182
        $nextUpdate = $client->nextUpdate();
183
        $effective = ($effective = $client->getEffectiveUri()) === $base ? null : $effective;
184
        if (
185
            stripos($base, 'http') === 0 &&
186
            (
187
                $statusCode === null ||
188
                (
189
                    $statusCode >= 500 &&
190
                    $statusCode < 600
191
                )
192
            ) &&
193
            $this->displacePush($base, $nextUpdate)
194
        ) {
195
            return true;
196
        }
197
        $validUntil = $client->validUntil();
198
        $content = $client->render();
199
        $query = $this->pdo->prepare(<<<SQL
200
INSERT INTO robotstxt__cache1 (base, content, statusCode, validUntil, nextUpdate, effective)
201
VALUES (:base, :content, :statusCode, :validUntil, :nextUpdate, :effective)
202
ON DUPLICATE KEY UPDATE content = :content, statusCode = :statusCode, validUntil = :validUntil,
203
  nextUpdate = :nextUpdate, effective = :effective, worker = 0;
204
SQL
205
        );
206
        $query->bindParam(':base', $base, PDO::PARAM_STR);
207
        $query->bindParam(':content', $content, PDO::PARAM_STR);
208
        $query->bindParam(':statusCode', $statusCode, PDO::PARAM_INT | PDO::PARAM_NULL);
209
        $query->bindParam(':validUntil', $validUntil, PDO::PARAM_INT);
210
        $query->bindParam(':nextUpdate', $nextUpdate, PDO::PARAM_INT);
211
        $query->bindParam(':effective', $effective, PDO::PARAM_STR | PDO::PARAM_NULL);
212
        return $query->execute();
213
    }
214
215
    /**
216
     * Displace push timestamp
217
     *
218
     * @param string $base
219
     * @param int $nextUpdate
220
     * @return bool
221
     */
222
    private function displacePush($base, $nextUpdate)
223
    {
224
        $query = $this->pdo->prepare(<<<SQL
225
SELECT
226
  validUntil,
227
  UNIX_TIMESTAMP()
228
FROM robotstxt__cache1
229
WHERE base = :base;
230
SQL
231
        );
232
        $query->bindParam(':base', $base, PDO::PARAM_STR);
233
        $query->execute();
234
        if ($query->rowCount() > 0) {
235
            $row = $query->fetch(PDO::FETCH_ASSOC);
236
            $this->clockSyncCheck($row['UNIX_TIMESTAMP()']);
237
            if ($row['validUntil'] > $row['UNIX_TIMESTAMP()']) {
238
                $nextUpdate = min($row['validUntil'], $nextUpdate);
239
                $query = $this->pdo->prepare(<<<SQL
240
UPDATE robotstxt__cache1
241
SET nextUpdate = :nextUpdate, worker = NULL
242
WHERE base = :base;
243
SQL
244
                );
245
                $query->bindParam(':base', $base, PDO::PARAM_STR);
246
                $query->bindParam(':nextUpdate', $nextUpdate, PDO::PARAM_INT);
247
                return $query->execute();
248
            }
249
            $this->invalidate($base);
250
        }
251
        return false;
252
    }
253
254
    /**
255
     * Invalidate cache
256
     *
257
     * @param $baseUri
258
     * @return bool
259
     */
260
    public function invalidate($baseUri)
261
    {
262
        $base = $this->urlBase($baseUri);
263
        $query = $this->pdo->prepare(<<<SQL
264
DELETE FROM robotstxt__cache1
265
WHERE base = :base;
266
SQL
267
        );
268
        $query->bindParam(':base', $base, PDO::PARAM_STR);
269
        return $query->execute();
270
    }
271
272
    /**
273
     * Process the update queue
274
     *
275
     * @param int|null $workerID
276
     * @return bool
277
     */
278
    public function cron($workerID = null)
279
    {
280
        $worker = $this->setWorkerID($workerID);
281
        $result = true;
282
        while ($result) {
283
            $query = $this->pdo->prepare(<<<SQL
284
UPDATE robotstxt__cache1
285
SET worker = :workerID
286
WHERE worker IS NULL AND nextUpdate <= UNIX_TIMESTAMP()
287
ORDER BY nextUpdate ASC
288
LIMIT 1;
289
SELECT base
290
FROM robotstxt__cache1
291
WHERE worker = :workerID;
292
SQL
293
            );
294
            $query->bindParam(':workerID', $worker, PDO::PARAM_INT);
295
            $query->execute();
296
            if ($query->rowCount() > 0) {
297
                while ($row = $query->fetch(PDO::FETCH_ASSOC)) {
298
                    $result = $this->push(new UriClient($row['base'], $this->curlOptions, $this->byteLimit));
299
                }
300
                continue;
301
            }
302
            return true;
303
        }
304
        return false;
305
    }
306
307
    /**
308
     * Set WorkerID
309
     *
310
     * @param int|null $workerID
311
     * @return int
312
     */
313
    protected function setWorkerID($workerID = null)
314
    {
315
        if (
316
            is_int($workerID) &&
317
            $workerID <= 255 &&
318
            $workerID >= 1
319
        ) {
320
            return $workerID;
321
        } elseif ($workerID !== null) {
322
            trigger_error('WorkerID out of range (1-255)', E_USER_WARNING);
323
        }
324
        return rand(1, 255);
325
    }
326
327
    /**
328
     * Clean the cache table
329
     *
330
     * @param int $delay - in seconds
331
     * @return bool
332
     */
333 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...
334
    {
335
        $delay = self::CACHE_TIME + $delay;
336
        $query = $this->pdo->prepare(<<<SQL
337
DELETE FROM robotstxt__cache1
338
WHERE worker = 0 AND nextUpdate < (UNIX_TIMESTAMP() - :delay);
339
SQL
340
        );
341
        $query->bindParam(':delay', $delay, PDO::PARAM_INT);
342
        return $query->execute();
343
    }
344
}
345