Passed
Pull Request — master (#857)
by Georges
01:55
created

Driver::getConfig()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
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\Mongodb;
18
19
use LogicException;
20
use MongoClient;
21
use MongoDB\BSON\Binary;
22
use MongoDB\BSON\UTCDateTime;
23
use MongoDB\Client;
0 ignored issues
show
Bug introduced by
The type MongoDB\Client was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
24
use MongoDB\Collection;
0 ignored issues
show
Bug introduced by
The type MongoDB\Collection was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
25
use MongoDB\Database;
0 ignored issues
show
Bug introduced by
The type MongoDB\Database was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
26
use MongoDB\DeleteResult;
0 ignored issues
show
Bug introduced by
The type MongoDB\DeleteResult was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
27
use MongoDB\Driver\Command;
28
use MongoDB\Driver\Exception\Exception as MongoDBException;
29
use MongoDB\Driver\Manager;
30
use Phpfastcache\Cluster\AggregatablePoolInterface;
31
use Phpfastcache\Core\Pool\ExtendedCacheItemPoolInterface;
32
use Phpfastcache\Core\Pool\TaggableCacheItemPoolTrait;
33
use Phpfastcache\Core\Item\ExtendedCacheItemInterface;
34
use Phpfastcache\Entities\DriverStatistic;
35
use Phpfastcache\Exceptions\PhpfastcacheDriverException;
36
use Phpfastcache\Exceptions\PhpfastcacheInvalidArgumentException;
37
use Phpfastcache\Exceptions\PhpfastcacheLogicException;
38
use Psr\Cache\CacheItemInterface;
39
40
/**
41
 * @property Client $instance Instance of driver service
42
 * @method Config getConfig()
43
 */
44
class Driver implements ExtendedCacheItemPoolInterface, AggregatablePoolInterface
45
{
46
    use TaggableCacheItemPoolTrait;
47
48
    public const MONGODB_DEFAULT_DB_NAME = 'phpfastcache'; // Public because used in config
49
50
    /**
51
     * @var Collection
52
     */
53
    public $collection;
54
55
    /**
56
     * @var Database
57
     */
58
    public $database;
59
60
    /**
61
     * @return bool
62
     */
63
    public function driverCheck(): bool
64
    {
65
        $mongoExtensionExists = class_exists(Manager::class);
66
67
        if (!$mongoExtensionExists && class_exists(MongoClient::class)) {
68
            trigger_error(
69
                'This driver is used to support the pecl MongoDb extension with mongo-php-library.
70
            For MongoDb with Mongo PECL support use Mongo Driver.',
71
                E_USER_ERROR
72
            );
73
        }
74
75
        return $mongoExtensionExists && class_exists(Collection::class);
76
    }
77
78
    /**
79
     * @return bool
80
     * @throws MongodbException
81
     * @throws LogicException
82
     */
83
    protected function driverConnect(): bool
84
    {
85
        $timeout = $this->getConfig()->getTimeout() * 1000;
86
        $collectionName = $this->getConfig()->getCollectionName();
87
        $databaseName = $this->getConfig()->getDatabaseName();
88
        $driverOptions = $this->getConfig()->getDriverOptions();
89
90
        $this->instance = new Client($this->buildConnectionURI($databaseName), ['connectTimeoutMS' => $timeout], $driverOptions);
91
        $this->database = $this->instance->selectDatabase($databaseName);
92
93
        if (!$this->collectionExists($collectionName)) {
94
            $this->database->createCollection($collectionName);
95
            $this->database->selectCollection($collectionName)
96
                ->createIndex(
97
                    [self::DRIVER_KEY_WRAPPER_INDEX => 1],
98
                    ['unique' => true, 'name' => 'unique_key_index']
99
                );
100
            $this->database->selectCollection($collectionName)
101
                ->createIndex(
102
                    [self::DRIVER_EDATE_WRAPPER_INDEX => 1],
103
                    ['expireAfterSeconds' => 0,  'name' => 'auto_expire_index']
104
                );
105
        }
106
107
        $this->collection = $this->database->selectCollection($collectionName);
108
109
        return true;
110
    }
111
112
    /**
113
     * @param ExtendedCacheItemInterface $item
114
     * @return null|array
115
     */
116
    protected function driverRead(ExtendedCacheItemInterface $item): ?array
117
    {
118
        $document = $this->getCollection()->findOne(['_id' => $this->getMongoDbItemKey($item)]);
119
120
        if ($document) {
121
            $return = [
122
                self::DRIVER_DATA_WRAPPER_INDEX => $this->decode($document[self::DRIVER_DATA_WRAPPER_INDEX]->getData()),
123
                self::DRIVER_TAGS_WRAPPER_INDEX => $document[self::DRIVER_TAGS_WRAPPER_INDEX]->jsonSerialize(),
124
                self::DRIVER_EDATE_WRAPPER_INDEX => $document[self::DRIVER_EDATE_WRAPPER_INDEX]->toDateTime(),
125
            ];
126
127
            if (!empty($this->getConfig()->isItemDetailedDate())) {
128
                $return += [
129
                    self::DRIVER_MDATE_WRAPPER_INDEX => $document[self::DRIVER_MDATE_WRAPPER_INDEX]->toDateTime(),
130
                    self::DRIVER_CDATE_WRAPPER_INDEX => $document[self::DRIVER_CDATE_WRAPPER_INDEX]->toDateTime(),
131
                ];
132
            }
133
134
            return $return;
135
        }
136
137
        return null;
138
    }
139
140
    /**
141
     * @param ExtendedCacheItemInterface $item
142
     * @return mixed
143
     * @throws PhpfastcacheDriverException
144
     * @throws PhpfastcacheInvalidArgumentException
145
     * @throws PhpfastcacheLogicException
146
     */
147
    protected function driverWrite(ExtendedCacheItemInterface $item): bool
148
    {
149
        $this->assertCacheItemType($item, Item::class);
150
151
        try {
152
            $set = [
153
                self::DRIVER_KEY_WRAPPER_INDEX => $item->getKey(),
154
                self::DRIVER_DATA_WRAPPER_INDEX => new Binary($this->encode($item->getRawValue()), Binary::TYPE_GENERIC),
155
                self::DRIVER_TAGS_WRAPPER_INDEX => $item->getTags(),
156
                self::DRIVER_EDATE_WRAPPER_INDEX => new UTCDateTime($item->getExpirationDate()),
157
            ];
158
159
            if (!empty($this->getConfig()->isItemDetailedDate())) {
160
                $set += [
161
                    self::DRIVER_MDATE_WRAPPER_INDEX =>  new UTCDateTime($item->getModificationDate()),
162
                    self::DRIVER_CDATE_WRAPPER_INDEX =>  new UTCDateTime($item->getCreationDate()),
163
                ];
164
            }
165
            $result = $this->getCollection()->updateOne(
166
                ['_id' => $this->getMongoDbItemKey($item)],
167
                [
168
                    '$set' => $set,
169
                ],
170
                ['upsert' => true, 'multiple' => false]
171
            );
172
        } catch (MongoDBException $e) {
173
            throw new PhpfastcacheDriverException('Got an exception while trying to write data to MongoDB server: ' . $e->getMessage(), 0, $e);
174
        }
175
176
        return $result->isAcknowledged();
177
    }
178
179
    /**
180
     * @param ExtendedCacheItemInterface $item
181
     * @return bool
182
     * @throws PhpfastcacheInvalidArgumentException
183
     */
184
    protected function driverDelete(ExtendedCacheItemInterface $item): bool
185
    {
186
        $this->assertCacheItemType($item, Item::class);
187
188
        $deletionResult = $this->getCollection()->deleteOne(['_id' =>  $this->getMongoDbItemKey($item)]);
189
190
        return $deletionResult->isAcknowledged();
191
    }
192
193
    /**
194
     * @return bool
195
     * @throws PhpfastcacheDriverException
196
     */
197
    protected function driverClear(): bool
198
    {
199
        try {
200
            return $this->collection->deleteMany([])->isAcknowledged();
201
        } catch (MongoDBException $e) {
202
            throw new PhpfastcacheDriverException('Got error while trying to empty the collection: ' . $e->getMessage(), 0, $e);
203
        }
204
    }
205
206
    /**
207
     * @return DriverStatistic
208
     * @throws MongoDBException
209
     */
210
    public function getStats(): DriverStatistic
211
    {
212
        $serverStats = $this->instance->getManager()->executeCommand(
213
            $this->getConfig()->getDatabaseName(),
214
            new Command(
215
                [
216
                    'serverStatus' => 1,
217
                    'recordStats' => 0,
218
                    'repl' => 0,
219
                    'metrics' => 0,
220
                ]
221
            )
222
        )->toArray()[0];
223
224
        $collectionStats = $this->instance->getManager()->executeCommand(
225
            $this->getConfig()->getDatabaseName(),
226
            new Command(
227
                [
228
                    'collStats' => $this->getConfig()->getCollectionName(),
229
                    'verbose' => true,
230
                ]
231
            )
232
        )->toArray()[0];
233
234
        $arrayFilterRecursive = static function ($array, callable $callback = null) use (&$arrayFilterRecursive) {
235
            $array = $callback($array);
236
237
            if (\is_object($array) || \is_array($array)) {
238
                foreach ($array as &$value) {
239
                    $value = $arrayFilterRecursive($value, $callback);
240
                }
241
            }
242
243
            return $array;
244
        };
245
246
        $callback = static function ($item) {
247
            /**
248
             * Remove unserializable properties
249
             */
250
            if ($item instanceof UTCDateTime) {
251
                return (string)$item;
252
            }
253
            return $item;
254
        };
255
256
        $serverStats = $arrayFilterRecursive($serverStats, $callback);
257
        $collectionStats = $arrayFilterRecursive($collectionStats, $callback);
258
259
        return (new DriverStatistic())
260
            ->setInfo(
261
                'MongoDB version ' . $serverStats->version . ', Uptime (in days): ' . round(
262
                    $serverStats->uptime / 86400,
263
                    1
264
                ) . "\n For more information see RawData."
265
            )
266
            ->setSize($collectionStats->size)
267
            ->setData(implode(', ', array_keys($this->itemInstances)))
268
            ->setRawData(
269
                [
270
                    'serverStatus' => $serverStats,
271
                    'collStats' => $collectionStats,
272
                ]
273
            );
274
    }
275
276
    /**
277
     * @return Collection
278
     */
279
    protected function getCollection(): Collection
280
    {
281
        return $this->collection;
282
    }
283
284
    /**
285
     * Builds the connection URI from the given parameters.
286
     *
287
     * @param string $databaseName
288
     * @return string The connection URI.
289
     */
290
    protected function buildConnectionURI(string $databaseName): string
291
    {
292
        $databaseName = \urlencode($databaseName);
293
        $servers = $this->getConfig()->getServers();
294
        $options = $this->getConfig()->getOptions();
295
296
        $protocol = $this->getConfig()->getProtocol();
297
        $host = $this->getConfig()->getHost();
298
        $port = $this->getConfig()->getPort();
299
        $username = $this->getConfig()->getUsername();
300
        $password = $this->getConfig()->getPassword();
301
302
        if (count($servers) > 0) {
303
            $host = array_reduce(
304
                $servers,
305
                static fn ($carry, $data) => $carry . ($carry === '' ? '' : ',') . $data['host'] . ':' . $data['port'],
306
                ''
307
            );
308
            $port = false;
309
        }
310
311
        return implode(
312
            '',
313
            [
314
                "{$protocol}://",
315
                $username ?: '',
316
                $password ? ":{$password}" : '',
317
                $username ? '@' : '',
318
                $host,
319
                $port !== 27017 && $port !== false ? ":{$port}" : '',
320
                $databaseName ? "/{$databaseName}" : '',
321
                count($options) > 0 ? '?' . http_build_query($options) : '',
322
            ]
323
        );
324
    }
325
326
    protected function getMongoDbItemKey(ExtendedCacheItemInterface $item): string
327
    {
328
        return 'pfc_' . $item->getEncodedKey();
329
    }
330
331
    /**
332
     * Checks if a collection name exists on the Mongo database.
333
     *
334
     * @param string $collectionName The collection name to check.
335
     *
336
     * @return bool True if the collection exists, false if not.
337
     */
338
    protected function collectionExists(string $collectionName): bool
339
    {
340
        foreach ($this->database->listCollections() as $collection) {
341
            if ($collection->getName() === $collectionName) {
342
                return true;
343
            }
344
        }
345
346
        return false;
347
    }
348
}
349