Move Cache to strategies
parent
f1da323b07
commit
f2c02a79b9
|
@ -21,16 +21,14 @@
|
||||||
|
|
||||||
namespace Friendica\Core\Cache\Factory;
|
namespace Friendica\Core\Cache\Factory;
|
||||||
|
|
||||||
use Friendica\App\BaseURL;
|
|
||||||
use Friendica\Core\Cache\Enum;
|
use Friendica\Core\Cache\Enum;
|
||||||
use Friendica\Core\Cache\Capability\ICanCache;
|
use Friendica\Core\Cache\Capability\ICanCache;
|
||||||
use Friendica\Core\Cache\Exception\CachePersistenceException;
|
use Friendica\Core\Cache\Exception\CachePersistenceException;
|
||||||
use Friendica\Core\Cache\Exception\InvalidCacheDriverException;
|
use Friendica\Core\Cache\Exception\InvalidCacheDriverException;
|
||||||
use Friendica\Core\Cache\Type;
|
use Friendica\Core\Cache\Type;
|
||||||
use Friendica\Core\Config\Capability\IManageConfigValues;
|
use Friendica\Core\Config\Capability\IManageConfigValues;
|
||||||
use Friendica\Database\Database;
|
use Friendica\Core\Hooks\Capabilities\ICanCreateInstances;
|
||||||
use Friendica\Util\Profiler;
|
use Friendica\Util\Profiler;
|
||||||
use Psr\Log\LoggerInterface;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class CacheFactory
|
* Class CacheFactory
|
||||||
|
@ -45,39 +43,18 @@ class Cache
|
||||||
* @var string The default cache if nothing set
|
* @var string The default cache if nothing set
|
||||||
*/
|
*/
|
||||||
const DEFAULT_TYPE = Enum\Type::DATABASE;
|
const DEFAULT_TYPE = Enum\Type::DATABASE;
|
||||||
|
/** @var ICanCreateInstances */
|
||||||
|
protected $instanceCreator;
|
||||||
|
/** @var IManageConfigValues */
|
||||||
|
protected $config;
|
||||||
|
/** @var Profiler */
|
||||||
|
protected $profiler;
|
||||||
|
|
||||||
/**
|
public function __construct(ICanCreateInstances $instanceCreator, IManageConfigValues $config, Profiler $profiler)
|
||||||
* @var IManageConfigValues The IConfiguration to read parameters out of the config
|
|
||||||
*/
|
|
||||||
private $config;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var Database The database connection in case that the cache is used the dba connection
|
|
||||||
*/
|
|
||||||
private $dba;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var string The hostname, used as Prefix for Caching
|
|
||||||
*/
|
|
||||||
private $hostname;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var Profiler The optional profiler if the cached should be profiled
|
|
||||||
*/
|
|
||||||
private $profiler;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var LoggerInterface The Friendica Logger
|
|
||||||
*/
|
|
||||||
private $logger;
|
|
||||||
|
|
||||||
public function __construct(BaseURL $baseURL, IManageConfigValues $config, Database $dba, Profiler $profiler, LoggerInterface $logger)
|
|
||||||
{
|
{
|
||||||
$this->hostname = $baseURL->getHost();
|
|
||||||
$this->config = $config;
|
$this->config = $config;
|
||||||
$this->dba = $dba;
|
$this->instanceCreator = $instanceCreator;
|
||||||
$this->profiler = $profiler;
|
$this->profiler = $profiler;
|
||||||
$this->logger = $logger;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -92,7 +69,7 @@ class Cache
|
||||||
*/
|
*/
|
||||||
public function createDistributed(string $type = null): ICanCache
|
public function createDistributed(string $type = null): ICanCache
|
||||||
{
|
{
|
||||||
if ($type === Enum\Type::APCU) {
|
if ($type === Type\APCuCache::$NAME) {
|
||||||
throw new InvalidCacheDriverException('apcu doesn\'t support distributed caching.');
|
throw new InvalidCacheDriverException('apcu doesn\'t support distributed caching.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,31 +94,17 @@ class Cache
|
||||||
/**
|
/**
|
||||||
* Creates a new Cache instance
|
* Creates a new Cache instance
|
||||||
*
|
*
|
||||||
* @param string $type The type of cache
|
* @param string $strategy The strategy, which cache instance should be used
|
||||||
*
|
*
|
||||||
* @return ICanCache
|
* @return ICanCache
|
||||||
*
|
*
|
||||||
* @throws InvalidCacheDriverException In case the underlying cache driver isn't valid or not configured properly
|
* @throws InvalidCacheDriverException In case the underlying cache driver isn't valid or not configured properly
|
||||||
* @throws CachePersistenceException In case the underlying cache has errors during persistence
|
* @throws CachePersistenceException In case the underlying cache has errors during persistence
|
||||||
*/
|
*/
|
||||||
protected function create(string $type): ICanCache
|
protected function create(string $strategy): ICanCache
|
||||||
{
|
{
|
||||||
switch ($type) {
|
/** @var ICanCache $cache */
|
||||||
case Enum\Type::MEMCACHE:
|
$cache = $this->instanceCreator->create(ICanCache::class, $strategy);
|
||||||
$cache = new Type\MemcacheCache($this->hostname, $this->config);
|
|
||||||
break;
|
|
||||||
case Enum\Type::MEMCACHED:
|
|
||||||
$cache = new Type\MemcachedCache($this->hostname, $this->config, $this->logger);
|
|
||||||
break;
|
|
||||||
case Enum\Type::REDIS:
|
|
||||||
$cache = new Type\RedisCache($this->hostname, $this->config);
|
|
||||||
break;
|
|
||||||
case Enum\Type::APCU:
|
|
||||||
$cache = new Type\APCuCache($this->hostname);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
$cache = new Type\DatabaseCache($this->hostname, $this->dba);
|
|
||||||
}
|
|
||||||
|
|
||||||
$profiling = $this->config->get('system', 'profiling', false);
|
$profiling = $this->config->get('system', 'profiling', false);
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,6 @@ namespace Friendica\Core\Cache\Type;
|
||||||
|
|
||||||
use Friendica\Core\Cache\Enum\Duration;
|
use Friendica\Core\Cache\Enum\Duration;
|
||||||
use Friendica\Core\Cache\Capability\ICanCacheInMemory;
|
use Friendica\Core\Cache\Capability\ICanCacheInMemory;
|
||||||
use Friendica\Core\Cache\Enum\Type;
|
|
||||||
use Friendica\Core\Cache\Exception\InvalidCacheDriverException;
|
use Friendica\Core\Cache\Exception\InvalidCacheDriverException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -31,12 +30,12 @@ use Friendica\Core\Cache\Exception\InvalidCacheDriverException;
|
||||||
*/
|
*/
|
||||||
class APCuCache extends AbstractCache implements ICanCacheInMemory
|
class APCuCache extends AbstractCache implements ICanCacheInMemory
|
||||||
{
|
{
|
||||||
|
public static $NAME = 'apcu';
|
||||||
|
|
||||||
use CompareSetTrait;
|
use CompareSetTrait;
|
||||||
use CompareDeleteTrait;
|
use CompareDeleteTrait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $hostname
|
|
||||||
*
|
|
||||||
* @throws InvalidCacheDriverException
|
* @throws InvalidCacheDriverException
|
||||||
*/
|
*/
|
||||||
public function __construct(string $hostname)
|
public function __construct(string $hostname)
|
||||||
|
@ -173,12 +172,4 @@ class APCuCache extends AbstractCache implements ICanCacheInMemory
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
public function getName(): string
|
|
||||||
{
|
|
||||||
return Type::APCU;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,8 @@ use Friendica\Core\Cache\Capability\ICanCache;
|
||||||
*/
|
*/
|
||||||
abstract class AbstractCache implements ICanCache
|
abstract class AbstractCache implements ICanCache
|
||||||
{
|
{
|
||||||
|
public static $NAME = '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string The hostname
|
* @var string The hostname
|
||||||
*/
|
*/
|
||||||
|
@ -105,4 +107,10 @@ abstract class AbstractCache implements ICanCache
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
public function getName(): string
|
||||||
|
{
|
||||||
|
return static::$NAME;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,8 @@ use Friendica\Core\Cache\Enum;
|
||||||
*/
|
*/
|
||||||
class ArrayCache extends AbstractCache implements ICanCacheInMemory
|
class ArrayCache extends AbstractCache implements ICanCacheInMemory
|
||||||
{
|
{
|
||||||
|
public static $NAME = 'array';
|
||||||
|
|
||||||
use CompareDeleteTrait;
|
use CompareDeleteTrait;
|
||||||
|
|
||||||
/** @var array Array with the cached data */
|
/** @var array Array with the cached data */
|
||||||
|
@ -108,12 +110,4 @@ class ArrayCache extends AbstractCache implements ICanCacheInMemory
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
public function getName(): string
|
|
||||||
{
|
|
||||||
return Enum\Type::ARRAY;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,8 @@ use Friendica\Util\DateTimeFormat;
|
||||||
*/
|
*/
|
||||||
class DatabaseCache extends AbstractCache implements ICanCache
|
class DatabaseCache extends AbstractCache implements ICanCache
|
||||||
{
|
{
|
||||||
|
public static $NAME = 'database';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Database
|
* @var Database
|
||||||
*/
|
*/
|
||||||
|
@ -154,12 +156,4 @@ class DatabaseCache extends AbstractCache implements ICanCache
|
||||||
throw new CachePersistenceException('Cannot clear cache', $exception);
|
throw new CachePersistenceException('Cannot clear cache', $exception);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
public function getName(): string
|
|
||||||
{
|
|
||||||
return Enum\Type::DATABASE;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,6 @@ namespace Friendica\Core\Cache\Type;
|
||||||
|
|
||||||
use Friendica\Core\Cache\Enum\Duration;
|
use Friendica\Core\Cache\Enum\Duration;
|
||||||
use Friendica\Core\Cache\Capability\ICanCacheInMemory;
|
use Friendica\Core\Cache\Capability\ICanCacheInMemory;
|
||||||
use Friendica\Core\Cache\Enum\Type;
|
|
||||||
use Friendica\Core\Cache\Exception\CachePersistenceException;
|
use Friendica\Core\Cache\Exception\CachePersistenceException;
|
||||||
use Friendica\Core\Cache\Exception\InvalidCacheDriverException;
|
use Friendica\Core\Cache\Exception\InvalidCacheDriverException;
|
||||||
use Friendica\Core\Config\Capability\IManageConfigValues;
|
use Friendica\Core\Config\Capability\IManageConfigValues;
|
||||||
|
@ -34,6 +33,8 @@ use Memcache;
|
||||||
*/
|
*/
|
||||||
class MemcacheCache extends AbstractCache implements ICanCacheInMemory
|
class MemcacheCache extends AbstractCache implements ICanCacheInMemory
|
||||||
{
|
{
|
||||||
|
static $NAME = 'memcached';
|
||||||
|
|
||||||
use CompareSetTrait;
|
use CompareSetTrait;
|
||||||
use CompareDeleteTrait;
|
use CompareDeleteTrait;
|
||||||
use MemcacheCommandTrait;
|
use MemcacheCommandTrait;
|
||||||
|
@ -169,12 +170,4 @@ class MemcacheCache extends AbstractCache implements ICanCacheInMemory
|
||||||
$cacheKey = $this->getCacheKey($key);
|
$cacheKey = $this->getCacheKey($key);
|
||||||
return $this->memcache->add($cacheKey, serialize($value), MEMCACHE_COMPRESSED, $ttl);
|
return $this->memcache->add($cacheKey, serialize($value), MEMCACHE_COMPRESSED, $ttl);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
public function getName(): string
|
|
||||||
{
|
|
||||||
return Type::MEMCACHE;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,6 @@ namespace Friendica\Core\Cache\Type;
|
||||||
|
|
||||||
use Friendica\Core\Cache\Enum\Duration;
|
use Friendica\Core\Cache\Enum\Duration;
|
||||||
use Friendica\Core\Cache\Capability\ICanCacheInMemory;
|
use Friendica\Core\Cache\Capability\ICanCacheInMemory;
|
||||||
use Friendica\Core\Cache\Enum\Type;
|
|
||||||
use Friendica\Core\Cache\Exception\CachePersistenceException;
|
use Friendica\Core\Cache\Exception\CachePersistenceException;
|
||||||
use Friendica\Core\Cache\Exception\InvalidCacheDriverException;
|
use Friendica\Core\Cache\Exception\InvalidCacheDriverException;
|
||||||
use Friendica\Core\Config\Capability\IManageConfigValues;
|
use Friendica\Core\Config\Capability\IManageConfigValues;
|
||||||
|
@ -35,6 +34,8 @@ use Psr\Log\LoggerInterface;
|
||||||
*/
|
*/
|
||||||
class MemcachedCache extends AbstractCache implements ICanCacheInMemory
|
class MemcachedCache extends AbstractCache implements ICanCacheInMemory
|
||||||
{
|
{
|
||||||
|
static $NAME = 'memcached';
|
||||||
|
|
||||||
use CompareSetTrait;
|
use CompareSetTrait;
|
||||||
use CompareDeleteTrait;
|
use CompareDeleteTrait;
|
||||||
use MemcacheCommandTrait;
|
use MemcacheCommandTrait;
|
||||||
|
@ -185,12 +186,4 @@ class MemcachedCache extends AbstractCache implements ICanCacheInMemory
|
||||||
$cacheKey = $this->getCacheKey($key);
|
$cacheKey = $this->getCacheKey($key);
|
||||||
return $this->memcached->add($cacheKey, $value, $ttl);
|
return $this->memcached->add($cacheKey, $value, $ttl);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
public function getName(): string
|
|
||||||
{
|
|
||||||
return Type::MEMCACHED;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,10 +21,8 @@
|
||||||
|
|
||||||
namespace Friendica\Core\Cache\Type;
|
namespace Friendica\Core\Cache\Type;
|
||||||
|
|
||||||
use Exception;
|
|
||||||
use Friendica\Core\Cache\Enum\Duration;
|
use Friendica\Core\Cache\Enum\Duration;
|
||||||
use Friendica\Core\Cache\Capability\ICanCacheInMemory;
|
use Friendica\Core\Cache\Capability\ICanCacheInMemory;
|
||||||
use Friendica\Core\Cache\Enum\Type;
|
|
||||||
use Friendica\Core\Cache\Exception\CachePersistenceException;
|
use Friendica\Core\Cache\Exception\CachePersistenceException;
|
||||||
use Friendica\Core\Cache\Exception\InvalidCacheDriverException;
|
use Friendica\Core\Cache\Exception\InvalidCacheDriverException;
|
||||||
use Friendica\Core\Config\Capability\IManageConfigValues;
|
use Friendica\Core\Config\Capability\IManageConfigValues;
|
||||||
|
@ -35,6 +33,8 @@ use Redis;
|
||||||
*/
|
*/
|
||||||
class RedisCache extends AbstractCache implements ICanCacheInMemory
|
class RedisCache extends AbstractCache implements ICanCacheInMemory
|
||||||
{
|
{
|
||||||
|
public static $NAME = 'redis';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Redis
|
* @var Redis
|
||||||
*/
|
*/
|
||||||
|
@ -59,9 +59,11 @@ class RedisCache extends AbstractCache implements ICanCacheInMemory
|
||||||
$redis_pw = $config->get('system', 'redis_password');
|
$redis_pw = $config->get('system', 'redis_password');
|
||||||
$redis_db = $config->get('system', 'redis_db', 0);
|
$redis_db = $config->get('system', 'redis_db', 0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
if (!empty($redis_port) && !@$this->redis->connect($redis_host, $redis_port)) {
|
if (!empty($redis_port) && !@$this->redis->connect($redis_host, $redis_port)) {
|
||||||
throw new CachePersistenceException('Expected Redis server at ' . $redis_host . ':' . $redis_port . ' isn\'t available');
|
throw new CachePersistenceException('Expected Redis server at ' . $redis_host . ':' . $redis_port . ' isn\'t available');
|
||||||
} elseif (!@$this->redis->connect($redis_host)) {
|
} else if (!@$this->redis->connect($redis_host)) {
|
||||||
throw new CachePersistenceException('Expected Redis server at ' . $redis_host . ' isn\'t available');
|
throw new CachePersistenceException('Expected Redis server at ' . $redis_host . ' isn\'t available');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,6 +74,9 @@ class RedisCache extends AbstractCache implements ICanCacheInMemory
|
||||||
if ($redis_db !== 0 && !$this->redis->select($redis_db)) {
|
if ($redis_db !== 0 && !$this->redis->select($redis_db)) {
|
||||||
throw new CachePersistenceException('Cannot switch to redis db ' . $redis_db . ' at ' . $redis_host . ':' . $redis_port);
|
throw new CachePersistenceException('Cannot switch to redis db ' . $redis_db . ' at ' . $redis_host . ':' . $redis_port);
|
||||||
}
|
}
|
||||||
|
} catch (\RedisException $exception) {
|
||||||
|
throw new CachePersistenceException('Redis connection fails unexpectedly', $exception);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -211,12 +216,4 @@ class RedisCache extends AbstractCache implements ICanCacheInMemory
|
||||||
$this->redis->unwatch();
|
$this->redis->unwatch();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
public function getName(): string
|
|
||||||
{
|
|
||||||
return Type::REDIS;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -178,6 +178,20 @@ return [
|
||||||
$_SERVER,
|
$_SERVER,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
'$hostname' => [
|
||||||
|
'instanceOf' => App\BaseURL::class,
|
||||||
|
'constructParams' => [
|
||||||
|
$_SERVER,
|
||||||
|
],
|
||||||
|
'call' => [
|
||||||
|
['getHost', [], Dice::CHAIN_CALL],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
Cache\Type\AbstractCache::class => [
|
||||||
|
'constructParams' => [
|
||||||
|
[Dice::INSTANCE => '$hostname'],
|
||||||
|
],
|
||||||
|
],
|
||||||
App\Page::class => [
|
App\Page::class => [
|
||||||
'constructParams' => [
|
'constructParams' => [
|
||||||
[Dice::INSTANCE => '$basepath'],
|
[Dice::INSTANCE => '$basepath'],
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use Friendica\Core\Hooks\Capabilities\BehavioralHookType as H;
|
use Friendica\Core\Cache;
|
||||||
use Friendica\Core\Logger\Type;
|
use Friendica\Core\Logger\Type;
|
||||||
use Psr\Log;
|
use Psr\Log;
|
||||||
|
|
||||||
|
@ -29,4 +29,11 @@ return [
|
||||||
Type\SyslogLogger::class => ['syslog'],
|
Type\SyslogLogger::class => ['syslog'],
|
||||||
Type\StreamLogger::class => ['stream'],
|
Type\StreamLogger::class => ['stream'],
|
||||||
],
|
],
|
||||||
|
Cache\Capability\ICanCache::class => [
|
||||||
|
Cache\Type\APCuCache::class => ['apcu'],
|
||||||
|
Cache\Type\DatabaseCache::class => ['database', ''],
|
||||||
|
Cache\Type\MemcacheCache::class => ['memcache'],
|
||||||
|
Cache\Type\MemcachedCache::class => ['memcached'],
|
||||||
|
Cache\Type\RedisCache::class => ['redis'],
|
||||||
|
]
|
||||||
];
|
];
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<phpunit
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
bootstrap="bootstrap.php"
|
||||||
|
verbose="true"
|
||||||
|
timeoutForSmallTests="900"
|
||||||
|
timeoutForMediumTests="900"
|
||||||
|
timeoutForLargeTests="900"
|
||||||
|
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
|
||||||
|
<testsuite name="friendica-addons">
|
||||||
|
<directory suffix=".php">../addon/*/tests/</directory>
|
||||||
|
</testsuite>
|
||||||
|
<!-- Filters for Code Coverage -->
|
||||||
|
<coverage>
|
||||||
|
<include>
|
||||||
|
<directory suffix=".php">../addon/</directory>
|
||||||
|
</include>
|
||||||
|
<exclude>
|
||||||
|
<directory suffix=".php">../addon/*/tests/</directory>
|
||||||
|
<directory suffix=".php">../addon/*/view/</directory>
|
||||||
|
<directory suffix=".php">../addon/*/vendor/</directory>
|
||||||
|
</exclude>
|
||||||
|
</coverage>
|
||||||
|
</phpunit>
|
|
@ -21,6 +21,7 @@
|
||||||
|
|
||||||
namespace Friendica\Test\src\Core\Cache;
|
namespace Friendica\Test\src\Core\Cache;
|
||||||
|
|
||||||
|
use Friendica\App\BaseURL;
|
||||||
use Friendica\Core\Cache;
|
use Friendica\Core\Cache;
|
||||||
use Friendica\Test\DatabaseTestTrait;
|
use Friendica\Test\DatabaseTestTrait;
|
||||||
use Friendica\Test\Util\CreateDatabaseTrait;
|
use Friendica\Test\Util\CreateDatabaseTrait;
|
||||||
|
|
Loading…
Reference in New Issue