Completed
Push — master ( 517741...718c02 )
by Georges
16s queued 13s
created

Driver::decode()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 27
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 15
nc 4
nop 1
dl 0
loc 27
rs 9.7666
c 1
b 0
f 0
1
<?php
2
3
/**
4
 *
5
 * This file is part of Phpfastcache.
6
 *
7
 * @license MIT License (MIT)
8
 *
9
 * For full copyright and license information, please see the docs/CREDITS.txt and LICENCE files.
10
 *
11
 * @author Georges.L (Geolim4)  <[email protected]>
12
 * @author Contributors  https://github.com/PHPSocialNetwork/phpfastcache/graphs/contributors
13
 */
14
15
declare(strict_types=1);
16
17
namespace Phpfastcache\Drivers\Couchdb;
18
19
use Doctrine\CouchDB\CouchDBClient;
20
use Doctrine\CouchDB\CouchDBException;
21
use Doctrine\CouchDB\HTTP\HTTPException;
22
use Phpfastcache\Cluster\AggregatablePoolInterface;
23
use Phpfastcache\Core\Pool\ExtendedCacheItemPoolInterface;
24
use Phpfastcache\Core\Pool\TaggableCacheItemPoolTrait;
25
use Phpfastcache\Core\Item\ExtendedCacheItemInterface;
26
use Phpfastcache\Entities\DriverStatistic;
27
use Phpfastcache\Exceptions\PhpfastcacheDriverException;
28
use Phpfastcache\Exceptions\PhpfastcacheInvalidArgumentException;
29
use Phpfastcache\Exceptions\PhpfastcacheLogicException;
30
31
/**
32
 * Class Driver
33
 * @property CouchdbClient $instance Instance of driver service
34
 * @method Config getConfig()
35
 */
36
class Driver implements AggregatablePoolInterface
37
{
38
    use TaggableCacheItemPoolTrait;
39
40
    public const COUCHDB_DEFAULT_DB_NAME = 'phpfastcache'; // Public because used in config
41
42
    /**
43
     * @return bool
44
     */
45
    public function driverCheck(): bool
46
    {
47
        return class_exists(CouchDBClient::class);
48
    }
49
50
    /**
51
     * @return string
52
     */
53
    public function getHelp(): string
54
    {
55
        return <<<HELP
56
<p>
57
To install the Couchdb HTTP client library via Composer:
58
<code>composer require "doctrine/couchdb" "@dev"</code>
59
</p>
60
HELP;
61
    }
62
63
    /**
64
     * @return DriverStatistic
65
     * @throws HTTPException
66
     */
67
    public function getStats(): DriverStatistic
68
    {
69
        $info = $this->instance->getDatabaseInfo();
70
71
        return (new DriverStatistic())
72
            ->setSize($info['sizes']['active'] ?? 0)
73
            ->setRawData($info)
74
            ->setData(implode(', ', array_keys($this->itemInstances)))
75
            ->setInfo('Couchdb version ' . $this->instance->getVersion() . "\n For more information see RawData.");
76
    }
77
78
    /**
79
     * @return bool
80
     * @throws HTTPException
81
     */
82
    protected function driverConnect(): bool
83
    {
84
        $clientConfig = $this->getConfig();
85
86
        $url = ($clientConfig->isSsl() ? 'https://' : 'http://');
87
        if ($clientConfig->getUsername()) {
88
            $url .= $clientConfig->getUsername();
89
            if ($clientConfig->getPassword()) {
90
                $url .= ":{$clientConfig->getPassword()}";
91
            }
92
            $url .= '@';
93
        }
94
        $url .= $clientConfig->getHost();
95
        $url .= ":{$clientConfig->getPort()}";
96
        $url .= '/' . \urlencode($this->getDatabaseName());
97
98
        $this->instance = CouchDBClient::create(
99
            [
100
                'dbname' => $this->getDatabaseName(),
101
                'url' => $url,
102
                'timeout' => $clientConfig->getTimeout(),
103
            ]
104
        );
105
106
        $this->createDatabase();
107
108
        return true;
109
    }
110
111
    /**
112
     * @return string
113
     */
114
    protected function getDatabaseName(): string
115
    {
116
        return $this->getConfig()->getDatabase() ?: static::COUCHDB_DEFAULT_DB_NAME;
117
    }
118
119
    /**
120
     * @return void
121
     * @throws HTTPException
122
     */
123
    protected function createDatabase(): void
124
    {
125
        try {
126
            $this->instance->getDatabaseInfo($this->getDatabaseName());
127
        } catch (HTTPException) {
128
            $this->instance->createDatabase($this->getDatabaseName());
129
        }
130
    }
131
132
    protected function getCouchDbItemKey(ExtendedCacheItemInterface $item): string
133
    {
134
        return 'pfc_' . $item->getEncodedKey();
135
    }
136
137
    /**
138
     * @param ExtendedCacheItemInterface $item
139
     * @return ?array<string, mixed>
140
     * @throws PhpfastcacheDriverException
141
     * @throws \Exception
142
     */
143
    protected function driverRead(ExtendedCacheItemInterface $item): ?array
144
    {
145
        try {
146
            $response = $this->instance->findDocument($this->getCouchDbItemKey($item));
147
        } catch (CouchDBException $e) {
148
            throw new PhpfastcacheDriverException('Got error while trying to get a document: ' . $e->getMessage(), 0, $e);
149
        }
150
151
        if ($response->status === 404 || empty($response->body[ExtendedCacheItemPoolInterface::DRIVER_DATA_WRAPPER_INDEX])) {
152
            return null;
153
        }
154
155
        if ($response->status === 200) {
156
            return $this->decodeDocument($response->body);
157
        }
158
159
        throw new PhpfastcacheDriverException('Got unexpected HTTP status: ' . $response->status);
160
    }
161
162
    /**
163
     * @param ExtendedCacheItemInterface $item
164
     * @return bool
165
     * @throws PhpfastcacheDriverException
166
     * @throws PhpfastcacheInvalidArgumentException
167
     * @throws PhpfastcacheLogicException
168
     */
169
    protected function driverWrite(ExtendedCacheItemInterface $item): bool
170
    {
171
        $this->assertCacheItemType($item, Item::class);
172
173
        try {
174
            $this->instance->putDocument(
175
                $this->encodeDocument($this->driverPreWrap($item)),
176
                $this->getCouchDbItemKey($item),
177
                $this->getLatestDocumentRevision($this->getCouchDbItemKey($item))
178
            );
179
        } catch (CouchDBException $e) {
180
            throw new PhpfastcacheDriverException('Got error while trying to upsert a document: ' . $e->getMessage(), 0, $e);
181
        }
182
        return true;
183
    }
184
185
    /**
186
     * @param string $docId
187
     * @return string|null
188
     */
189
    protected function getLatestDocumentRevision(string $docId): ?string
190
    {
191
        $path = '/' . \urlencode($this->getDatabaseName()) . '/' . urlencode($docId);
192
193
        $response = $this->instance->getHttpClient()->request(
194
            'HEAD',
195
            $path,
196
            null,
197
            false
198
        );
199
        if (!empty($response->headers['etag'])) {
200
            return trim($response->headers['etag'], " '\"\t\n\r\0\x0B");
201
        }
202
203
        return null;
204
    }
205
206
    /**
207
     * @param ExtendedCacheItemInterface $item
208
     * @return bool
209
     * @throws PhpfastcacheDriverException
210
     * @throws PhpfastcacheInvalidArgumentException
211
     */
212
    protected function driverDelete(ExtendedCacheItemInterface $item): bool
213
    {
214
        $this->assertCacheItemType($item, Item::class);
215
216
        try {
217
            $this->instance->deleteDocument($this->getCouchDbItemKey($item), $this->getLatestDocumentRevision($this->getCouchDbItemKey($item)));
218
        } catch (CouchDBException $e) {
219
            throw new PhpfastcacheDriverException('Got error while trying to delete a document: ' . $e->getMessage(), 0, $e);
220
        }
221
        return true;
222
    }
223
224
    /**
225
     * @return bool
226
     * @throws PhpfastcacheDriverException
227
     */
228
    protected function driverClear(): bool
229
    {
230
        try {
231
            $this->instance->deleteDatabase($this->getDatabaseName());
232
            $this->createDatabase();
233
        } catch (CouchDBException $e) {
234
            throw new PhpfastcacheDriverException('Got error while trying to delete and recreate the database: ' . $e->getMessage(), 0, $e);
235
        }
236
237
        return true;
238
    }
239
240
    /**
241
     * @param array<string, mixed> $data
242
     * @return array<string, mixed>
243
     */
244
    protected function encodeDocument(array $data): array
245
    {
246
        $data[ExtendedCacheItemPoolInterface::DRIVER_DATA_WRAPPER_INDEX] = $this->encode($data[ExtendedCacheItemPoolInterface::DRIVER_DATA_WRAPPER_INDEX]);
247
248
        return $data;
249
    }
250
251
    /**
252
     * Specific document decoder for Couchdb
253
     * since we don't store encoded version
254
     * for performance purposes
255
     *
256
     * @param array<string, mixed> $value
257
     * @return array<string, mixed>
258
     * @throws \Exception
259
     */
260
    protected function decodeDocument(array $value): array
261
    {
262
        $value[ExtendedCacheItemPoolInterface::DRIVER_DATA_WRAPPER_INDEX] = \unserialize(
263
            $value[ExtendedCacheItemPoolInterface::DRIVER_DATA_WRAPPER_INDEX],
264
            ['allowed_classes' => true]
265
        );
266
267
        $value[ExtendedCacheItemPoolInterface::DRIVER_EDATE_WRAPPER_INDEX] = new \DateTime(
268
            $value[ExtendedCacheItemPoolInterface::DRIVER_EDATE_WRAPPER_INDEX]['date'],
269
            new \DateTimeZone($value[ExtendedCacheItemPoolInterface::DRIVER_EDATE_WRAPPER_INDEX]['timezone'])
270
        );
271
272
        if (isset($value[ExtendedCacheItemPoolInterface::DRIVER_CDATE_WRAPPER_INDEX])) {
273
            $value[ExtendedCacheItemPoolInterface::DRIVER_CDATE_WRAPPER_INDEX] = new \DateTime(
274
                $value[ExtendedCacheItemPoolInterface::DRIVER_CDATE_WRAPPER_INDEX]['date'],
275
                new \DateTimeZone($value[ExtendedCacheItemPoolInterface::DRIVER_CDATE_WRAPPER_INDEX]['timezone'])
276
            );
277
        }
278
279
        if (isset($value[ExtendedCacheItemPoolInterface::DRIVER_MDATE_WRAPPER_INDEX])) {
280
            $value[ExtendedCacheItemPoolInterface::DRIVER_MDATE_WRAPPER_INDEX] = new \DateTime(
281
                $value[ExtendedCacheItemPoolInterface::DRIVER_MDATE_WRAPPER_INDEX]['date'],
282
                new \DateTimeZone($value[ExtendedCacheItemPoolInterface::DRIVER_MDATE_WRAPPER_INDEX]['timezone'])
283
            );
284
        }
285
286
        return $value;
287
    }
288
}
289