Passed
Push — master ( 357df8...5617f0 )
by Georges
02:00 queued 12s
created

Driver::driverWrite()   A

Complexity

Conditions 5
Paths 10

Size

Total Lines 35
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

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