367 lines
12 KiB
PHP

<?php
/**
*
* This file is part of phpFastCache.
*
* @license MIT License (MIT)
*
* For full copyright and license information, please see the docs/CREDITS.txt file.
*
* @author Khoa Bui (khoaofgod) <khoaofgod@gmail.com> https://www.phpfastcache.com
* @author Georges.L (Geolim4) <contact@geolim4.com>
* @author Fabio Covolo Mazzo (fabiocmazzo) <fabiomazzo@gmail.com>
*
*/
declare(strict_types=1);
namespace Phpfastcache\Drivers\Mongodb;
use LogicException;
use MongoClient;
use MongoDB\{BSON\Binary, BSON\UTCDateTime, Client, Collection, Database, DeleteResult, Driver\Command, Driver\Exception\Exception as MongoDBException, Driver\Manager};
use Phpfastcache\Cluster\AggregatablePoolInterface;
use Phpfastcache\Core\Pool\{DriverBaseTrait, ExtendedCacheItemPoolInterface};
use Phpfastcache\Entities\DriverStatistic;
use Phpfastcache\Exceptions\{PhpfastcacheDriverException, PhpfastcacheInvalidArgumentException};
use Psr\Cache\CacheItemInterface;
/**
* Class Driver
* @package phpFastCache\Drivers
* @property Client $instance Instance of driver service
* @property Config $config Config object
* @method Config getConfig() Return the config object
*/
class Driver implements ExtendedCacheItemPoolInterface, AggregatablePoolInterface
{
public const MONGODB_DEFAULT_DB_NAME = 'phpfastcache'; // Public because used in config
use DriverBaseTrait;
/**
* @var Collection
*/
public $collection;
/**
* @var Database
*/
public $database;
/**
* @return bool
*/
public function driverCheck(): bool
{
$mongoExtensionExists = class_exists(Manager::class);
if (!$mongoExtensionExists && class_exists(MongoClient::class)) {
trigger_error(
'This driver is used to support the pecl MongoDb extension with mongo-php-library.
For MongoDb with Mongo PECL support use Mongo Driver.',
E_USER_ERROR
);
}
return $mongoExtensionExists && class_exists(Collection::class);
}
/**
* @return DriverStatistic
*/
public function getStats(): DriverStatistic
{
$serverStats = $this->instance->getManager()->executeCommand(
$this->getConfig()->getDatabaseName(),
new Command(
[
'serverStatus' => 1,
'recordStats' => 0,
'repl' => 0,
'metrics' => 0,
]
)
)->toArray()[0];
$collectionStats = $this->instance->getManager()->executeCommand(
$this->getConfig()->getDatabaseName(),
new Command(
[
'collStats' => $this->getConfig()->getCollectionName(),
'verbose' => true,
]
)
)->toArray()[0];
$array_filter_recursive = static function ($array, callable $callback = null) use (&$array_filter_recursive) {
$array = $callback($array);
if (\is_object($array) || \is_array($array)) {
foreach ($array as &$value) {
$value = $array_filter_recursive($value, $callback);
}
}
return $array;
};
$callback = static function ($item) {
/**
* Remove unserializable properties
*/
if ($item instanceof UTCDateTime) {
return (string)$item;
}
return $item;
};
$serverStats = $array_filter_recursive($serverStats, $callback);
$collectionStats = $array_filter_recursive($collectionStats, $callback);
$stats = (new DriverStatistic())
->setInfo(
'MongoDB version ' . $serverStats->version . ', Uptime (in days): ' . round(
$serverStats->uptime / 86400,
1
) . "\n For more information see RawData."
)
->setSize($collectionStats->size)
->setData(implode(', ', array_keys($this->itemInstances)))
->setRawData(
[
'serverStatus' => $serverStats,
'collStats' => $collectionStats,
]
);
return $stats;
}
/**
* @param CacheItemInterface $item
* @return null|array
*/
protected function driverRead(CacheItemInterface $item)
{
$document = $this->getCollection()->findOne(['_id' => $this->getMongoDbItemKey($item)]);
if ($document) {
$return = [
self::DRIVER_DATA_WRAPPER_INDEX => $this->decode($document[self::DRIVER_DATA_WRAPPER_INDEX]->getData()),
self::DRIVER_TAGS_WRAPPER_INDEX => $document[self::DRIVER_TAGS_WRAPPER_INDEX]->jsonSerialize(),
self::DRIVER_EDATE_WRAPPER_INDEX => $document[self::DRIVER_EDATE_WRAPPER_INDEX]->toDateTime(),
];
if (!empty($this->getConfig()->isItemDetailedDate())) {
$return += [
self::DRIVER_MDATE_WRAPPER_INDEX => $document[self::DRIVER_MDATE_WRAPPER_INDEX]->toDateTime(),
self::DRIVER_CDATE_WRAPPER_INDEX => $document[self::DRIVER_CDATE_WRAPPER_INDEX]->toDateTime(),
];
}
return $return;
}
return null;
}
/**
* @return Collection
*/
protected function getCollection(): Collection
{
return $this->collection;
}
/**
* @param CacheItemInterface $item
* @return mixed
* @throws PhpfastcacheInvalidArgumentException
* @throws PhpfastcacheDriverException
*/
protected function driverWrite(CacheItemInterface $item): bool
{
/**
* Check for Cross-Driver type confusion
*/
if ($item instanceof Item) {
try {
$set = [
self::DRIVER_KEY_WRAPPER_INDEX => $item->getKey(),
self::DRIVER_DATA_WRAPPER_INDEX => new Binary($this->encode($item->get()), Binary::TYPE_GENERIC),
self::DRIVER_TAGS_WRAPPER_INDEX => $item->getTags(),
self::DRIVER_EDATE_WRAPPER_INDEX => new UTCDateTime($item->getExpirationDate()),
];
if (!empty($this->getConfig()->isItemDetailedDate())) {
$set += [
self::DRIVER_MDATE_WRAPPER_INDEX => new UTCDateTime($item->getModificationDate()),
self::DRIVER_CDATE_WRAPPER_INDEX => new UTCDateTime($item->getCreationDate()),
];
}
$result = (array)$this->getCollection()->updateOne(
['_id' => $this->getMongoDbItemKey($item)],
[
'$set' => $set,
],
['upsert' => true, 'multiple' => false]
);
} catch (MongoDBException $e) {
throw new PhpfastcacheDriverException('Got an exception while trying to write data to MongoDB server: ' . $e->getMessage(), 0, $e);
}
return isset($result['ok']) ? $result['ok'] == 1 : true;
}
throw new PhpfastcacheInvalidArgumentException('Cross-Driver type confusion detected');
}
/**
* @param CacheItemInterface $item
* @return bool
* @throws PhpfastcacheInvalidArgumentException
*/
protected function driverDelete(CacheItemInterface $item): bool
{
/**
* Check for Cross-Driver type confusion
*/
if ($item instanceof Item) {
/**
* @var DeleteResult $deletionResult
*/
$deletionResult = $this->getCollection()->deleteOne(['_id' => $this->getMongoDbItemKey($item)]);
return $deletionResult->isAcknowledged();
}
throw new PhpfastcacheInvalidArgumentException('Cross-Driver type confusion detected');
}
/**
* @return bool
*/
protected function driverClear(): bool
{
try {
return $this->collection->deleteMany([])->isAcknowledged();
} catch (MongoDBException $e) {
throw new PhpfastcacheDriverException('Got error while trying to empty the collection: ' . $e->getMessage(), 0, $e);
}
}
/**
* @return bool
* @throws MongodbException
* @throws LogicException
*/
protected function driverConnect(): bool
{
if ($this->instance instanceof Client) {
throw new LogicException('Already connected to Mongodb server');
}
$timeout = $this->getConfig()->getTimeout() * 1000;
$collectionName = $this->getConfig()->getCollectionName();
$databaseName = $this->getConfig()->getDatabaseName();
$driverOptions = $this->getConfig()->getDriverOptions();
$this->instance = $this->instance ?: new Client($this->buildConnectionURI($databaseName), ['connectTimeoutMS' => $timeout], $driverOptions);
$this->database = $this->database ?: $this->instance->selectDatabase($databaseName);
if (!$this->collectionExists($collectionName)) {
$this->database->createCollection($collectionName);
$this->database->selectCollection($collectionName)
->createIndex(
[self::DRIVER_KEY_WRAPPER_INDEX => 1],
['unique' => true, 'name' => 'unique_key_index']
);
$this->database->selectCollection($collectionName)
->createIndex(
[self::DRIVER_EDATE_WRAPPER_INDEX => 1],
['expireAfterSeconds' => 0, 'name' => 'auto_expire_index']
);
}
$this->collection = $this->database->selectCollection($collectionName);
return true;
}
/**
* Builds the connection URI from the given parameters.
*
* @param string $databaseName
* @return string The connection URI.
*/
protected function buildConnectionURI(string $databaseName): string
{
$databaseName = \urlencode($databaseName);
$servers = $this->getConfig()->getServers();
$options = $this->getConfig()->getOptions();
$protocol = $this->getConfig()->getProtocol();
$host = $this->getConfig()->getHost();
$port = $this->getConfig()->getPort();
$username = $this->getConfig()->getUsername();
$password = $this->getConfig()->getPassword();
if (count($servers) > 0) {
$host = array_reduce(
$servers,
static function ($carry, $data) {
$carry .= ($carry === '' ? '' : ',') . $data['host'] . ':' . $data['port'];
return $carry;
},
''
);
$port = false;
}
return implode(
'',
[
"{$protocol}://",
$username ?: '',
$password ? ":{$password}" : '',
$username ? '@' : '',
$host,
$port !== 27017 && $port !== false ? ":{$port}" : '',
$databaseName ? "/{$databaseName}" : '',
count($options) > 0 ? '?' . http_build_query($options) : '',
]
);
}
protected function getMongoDbItemKey(CacheItemInterface $item)
{
return 'pfc_' . $item->getEncodedKey();
}
/********************
*
* PSR-6 Extended Methods
*
*******************/
/**
* Checks if a collection name exists on the Mongo database.
*
* @param string $collectionName The collection name to check.
*
* @return bool True if the collection exists, false if not.
*/
protected function collectionExists($collectionName): bool
{
foreach ($this->database->listCollections() as $collection) {
if ($collection->getName() === $collectionName) {
return true;
}
}
return false;
}
}