glastree_on_gitea

This commit is contained in:
2026-05-26 08:14:29 +02:00
commit 0bed099d05
9556 changed files with 1186307 additions and 0 deletions
+154
View File
@@ -0,0 +1,154 @@
<?php
namespace Illuminate\Cache;
class ApcStore extends TaggableStore
{
use RetrievesMultipleKeys;
/**
* The APC wrapper instance.
*
* @var \Illuminate\Cache\ApcWrapper
*/
protected $apc;
/**
* A string that should be prepended to keys.
*
* @var string
*/
protected $prefix;
/**
* Create a new APC store.
*
* @param \Illuminate\Cache\ApcWrapper $apc
* @param string $prefix
*/
public function __construct(ApcWrapper $apc, $prefix = '')
{
$this->apc = $apc;
$this->prefix = $prefix;
}
/**
* Retrieve an item from the cache by key.
*
* @param string $key
* @return mixed
*/
public function get($key)
{
return $this->apc->get($this->prefix.$key);
}
/**
* Store an item in the cache for a given number of seconds.
*
* @param string $key
* @param mixed $value
* @param int $seconds
* @return bool
*/
public function put($key, $value, $seconds)
{
return $this->apc->put($this->prefix.$key, $value, $seconds);
}
/**
* Increment the value of an item in the cache.
*
* @param string $key
* @param int $value
* @return int|false
*/
public function increment($key, $value = 1)
{
return $this->apc->increment($this->prefix.$key, $value);
}
/**
* Decrement the value of an item in the cache.
*
* @param string $key
* @param int $value
* @return int|false
*/
public function decrement($key, $value = 1)
{
return $this->apc->decrement($this->prefix.$key, $value);
}
/**
* Store an item in the cache indefinitely.
*
* @param string $key
* @param mixed $value
* @return bool
*/
public function forever($key, $value)
{
return $this->put($key, $value, 0);
}
/**
* Adjust the expiration time of a cached item.
*
* @param string $key
* @param int $seconds
* @return bool
*/
public function touch($key, $seconds)
{
$value = $this->apc->get($key = $this->getPrefix().$key);
if (is_null($value)) {
return false;
}
return $this->apc->put($key, $value, $seconds);
}
/**
* Remove an item from the cache.
*
* @param string $key
* @return bool
*/
public function forget($key)
{
return $this->apc->delete($this->prefix.$key);
}
/**
* Remove all items from the cache.
*
* @return bool
*/
public function flush()
{
return $this->apc->flush();
}
/**
* Get the cache key prefix.
*
* @return string
*/
public function getPrefix()
{
return $this->prefix;
}
/**
* Set the cache key prefix.
*
* @param string $prefix
* @return void
*/
public function setPrefix($prefix)
{
$this->prefix = $prefix;
}
}
+77
View File
@@ -0,0 +1,77 @@
<?php
namespace Illuminate\Cache;
class ApcWrapper
{
/**
* Get an item from the cache.
*
* @param string $key
* @return mixed
*/
public function get($key)
{
$fetchedValue = apcu_fetch($key, $success);
return $success ? $fetchedValue : null;
}
/**
* Store an item in the cache.
*
* @param string $key
* @param mixed $value
* @param int $seconds
* @return bool
*/
public function put($key, $value, $seconds)
{
return apcu_store($key, $value, $seconds);
}
/**
* Increment the value of an item in the cache.
*
* @param string $key
* @param int $value
* @return int|false
*/
public function increment($key, $value)
{
return apcu_inc($key, $value);
}
/**
* Decrement the value of an item in the cache.
*
* @param string $key
* @param int $value
* @return int|false
*/
public function decrement($key, $value)
{
return apcu_dec($key, $value);
}
/**
* Remove an item from the cache.
*
* @param string $key
* @return bool
*/
public function delete($key)
{
return apcu_delete($key);
}
/**
* Remove all items from the cache.
*
* @return bool
*/
public function flush()
{
return apcu_clear_cache();
}
}
@@ -0,0 +1,105 @@
<?php
namespace Illuminate\Cache;
use Illuminate\Support\Carbon;
class ArrayLock extends Lock
{
/**
* The parent array cache store.
*
* @var \Illuminate\Cache\ArrayStore
*/
protected $store;
/**
* Create a new lock instance.
*
* @param \Illuminate\Cache\ArrayStore $store
* @param string $name
* @param int $seconds
* @param string|null $owner
*/
public function __construct($store, $name, $seconds, $owner = null)
{
parent::__construct($name, $seconds, $owner);
$this->store = $store;
}
/**
* Attempt to acquire the lock.
*
* @return bool
*/
public function acquire()
{
$expiration = $this->store->locks[$this->name]['expiresAt'] ?? Carbon::now()->addSecond();
if ($this->exists() && $expiration->isFuture()) {
return false;
}
$this->store->locks[$this->name] = [
'owner' => $this->owner,
'expiresAt' => $this->seconds === 0 ? null : Carbon::now()->addSeconds($this->seconds),
];
return true;
}
/**
* Determine if the current lock exists.
*
* @return bool
*/
protected function exists()
{
return isset($this->store->locks[$this->name]);
}
/**
* Release the lock.
*
* @return bool
*/
public function release()
{
if (! $this->exists()) {
return false;
}
if (! $this->isOwnedByCurrentProcess()) {
return false;
}
$this->forceRelease();
return true;
}
/**
* Returns the owner value written into the driver for this lock.
*
* @return string|null
*/
protected function getCurrentOwner()
{
if (! $this->exists()) {
return null;
}
return $this->store->locks[$this->name]['owner'];
}
/**
* Releases this lock regardless of ownership.
*
* @return void
*/
public function forceRelease()
{
unset($this->store->locks[$this->name]);
}
}
@@ -0,0 +1,319 @@
<?php
namespace Illuminate\Cache;
use Illuminate\Contracts\Cache\CanFlushLocks;
use Illuminate\Contracts\Cache\LockProvider;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
use Illuminate\Support\InteractsWithTime;
use RuntimeException;
class ArrayStore extends TaggableStore implements CanFlushLocks, LockProvider
{
use InteractsWithTime, RetrievesMultipleKeys;
/**
* The array of stored values.
*
* @var array<string, array{value: mixed, expiresAt: float}>
*/
protected $storage = [];
/**
* The array of locks.
*
* @var array<string, array{owner: ?string, expiresAt: ?\Illuminate\Support\Carbon}>
*/
public $locks = [];
/**
* Indicates if values are serialized within the store.
*
* @var bool
*/
protected $serializesValues;
/**
* The classes that should be allowed during unserialization.
*
* @var array|bool|null
*/
protected $serializableClasses;
/**
* Create a new Array store.
*
* @param bool $serializesValues
* @param array|bool|null $serializableClasses
*/
public function __construct($serializesValues = false, $serializableClasses = null)
{
$this->serializesValues = $serializesValues;
$this->serializableClasses = $serializableClasses;
}
/**
* Get all of the cached values and their expiration times.
*
* @param bool $unserialize
* @return array<string, array{value: mixed, expiresAt: float}>
*/
public function all($unserialize = true)
{
if ($unserialize === false || $this->serializesValues === false) {
return $this->storage;
}
$storage = [];
foreach ($this->storage as $key => $data) {
$storage[$key] = [
'value' => $this->unserialize($data['value']),
'expiresAt' => $data['expiresAt'],
];
}
return $storage;
}
/**
* Retrieve an item from the cache by key.
*
* @param string $key
* @return mixed
*/
public function get($key)
{
if (! isset($this->storage[$key])) {
return;
}
$item = $this->storage[$key];
$expiresAt = $item['expiresAt'] ?? 0;
if ($expiresAt !== 0 && (Carbon::now()->getPreciseTimestamp(3) / 1000) >= $expiresAt) {
$this->forget($key);
return;
}
return $this->serializesValues ? $this->unserialize($item['value']) : $item['value'];
}
/**
* Store an item in the cache for a given number of seconds.
*
* @param string $key
* @param mixed $value
* @param int $seconds
* @return bool
*/
public function put($key, $value, $seconds)
{
$this->storage[$key] = [
'value' => $this->serializesValues ? serialize($value) : $value,
'expiresAt' => $this->calculateExpiration($seconds),
];
return true;
}
/**
* Increment the value of an item in the cache.
*
* @param string $key
* @param mixed $value
* @return int
*/
public function increment($key, $value = 1)
{
if (! is_null($existing = $this->get($key))) {
return tap(((int) $existing) + $value, function ($incremented) use ($key) {
$value = $this->serializesValues ? serialize($incremented) : $incremented;
$this->storage[$key]['value'] = $value;
});
}
$this->forever($key, $value);
return $value;
}
/**
* Decrement the value of an item in the cache.
*
* @param string $key
* @param mixed $value
* @return int
*/
public function decrement($key, $value = 1)
{
return $this->increment($key, $value * -1);
}
/**
* Store an item in the cache indefinitely.
*
* @param string $key
* @param mixed $value
* @return bool
*/
public function forever($key, $value)
{
return $this->put($key, $value, 0);
}
/**
* Adjust the expiration time of a cached item.
*
* @param string $key
* @param int $seconds
* @return bool
*/
public function touch($key, $seconds)
{
$item = Arr::get($this->storage, $key = $this->getPrefix().$key, null);
if (is_null($item)) {
return false;
}
$item['expiresAt'] = $this->calculateExpiration($seconds);
$this->storage = array_merge($this->storage, [$key => $item]);
return true;
}
/**
* Remove an item from the cache.
*
* @param string $key
* @return bool
*/
public function forget($key)
{
if (array_key_exists($key, $this->storage)) {
unset($this->storage[$key]);
return true;
}
return false;
}
/**
* Remove all items from the cache.
*
* @return bool
*/
public function flush()
{
$this->storage = [];
return true;
}
/**
* Remove all locks from the store.
*
* @return bool
*
* @throws \RuntimeException
*/
public function flushLocks(): bool
{
if (! $this->hasSeparateLockStore()) {
throw new RuntimeException('Flushing locks is only supported when the lock store is separate from the cache store.');
}
$this->locks = [];
return true;
}
/**
* Get the cache key prefix.
*
* @return string
*/
public function getPrefix()
{
return '';
}
/**
* Get the expiration time of the key.
*
* @param int $seconds
* @return float
*/
protected function calculateExpiration($seconds)
{
return $this->toTimestamp($seconds);
}
/**
* Get the UNIX timestamp, with milliseconds, for the given number of seconds in the future.
*
* @param int $seconds
* @return float
*/
protected function toTimestamp($seconds)
{
return $seconds > 0 ? (Carbon::now()->getPreciseTimestamp(3) / 1000) + $seconds : 0;
}
/**
* Get a lock instance.
*
* @param string $name
* @param int $seconds
* @param string|null $owner
* @return \Illuminate\Contracts\Cache\Lock
*/
public function lock($name, $seconds = 0, $owner = null)
{
return new ArrayLock($this, $name, $seconds, $owner);
}
/**
* Restore a lock instance using the owner identifier.
*
* @param string $name
* @param string $owner
* @return \Illuminate\Contracts\Cache\Lock
*/
public function restoreLock($name, $owner)
{
return $this->lock($name, 0, $owner);
}
/**
* Determine if the lock store is separate from the cache store.
*
* @return bool
*/
public function hasSeparateLockStore(): bool
{
return true;
}
/**
* Unserialize the given value.
*
* @param string $value
* @return mixed
*/
protected function unserialize($value)
{
if ($this->serializableClasses !== null) {
return unserialize($value, ['allowed_classes' => $this->serializableClasses]);
}
return unserialize($value);
}
}
@@ -0,0 +1,84 @@
<?php
namespace Illuminate\Cache;
class CacheLock extends Lock
{
/**
* The cache store implementation.
*
* @var \Illuminate\Contracts\Cache\Store
*/
protected $store;
/**
* Create a new lock instance.
*
* @param \Illuminate\Contracts\Cache\Store $store
* @param string $name
* @param int $seconds
* @param string|null $owner
*/
public function __construct($store, $name, $seconds, $owner = null)
{
parent::__construct($name, $seconds, $owner);
$this->store = $store;
}
/**
* Attempt to acquire the lock.
*
* @return bool
*/
public function acquire()
{
if (method_exists($this->store, 'add') && $this->seconds > 0) {
return $this->store->add(
$this->name, $this->owner, $this->seconds
);
}
if (! is_null($this->store->get($this->name))) {
return false;
}
return ($this->seconds > 0)
? $this->store->put($this->name, $this->owner, $this->seconds)
: $this->store->forever($this->name, $this->owner);
}
/**
* Release the lock.
*
* @return bool
*/
public function release()
{
if ($this->isOwnedByCurrentProcess()) {
return $this->store->forget($this->name);
}
return false;
}
/**
* Releases this lock regardless of ownership.
*
* @return void
*/
public function forceRelease()
{
$this->store->forget($this->name);
}
/**
* Returns the owner value written into the driver for this lock.
*
* @return mixed
*/
protected function getCurrentOwner()
{
return $this->store->get($this->name);
}
}
+586
View File
@@ -0,0 +1,586 @@
<?php
namespace Illuminate\Cache;
use Aws\DynamoDb\DynamoDbClient;
use Closure;
use Illuminate\Contracts\Cache\Factory as FactoryContract;
use Illuminate\Contracts\Cache\Store;
use Illuminate\Contracts\Events\Dispatcher as DispatcherContract;
use Illuminate\Support\Arr;
use Illuminate\Support\RebindsCallbacksToSelf;
use InvalidArgumentException;
use Mockery;
use Mockery\LegacyMockInterface;
use ReflectionException;
use RuntimeException;
use function Illuminate\Support\enum_value;
/**
* @mixin \Illuminate\Cache\Repository
* @mixin \Illuminate\Contracts\Cache\LockProvider
*/
class CacheManager implements FactoryContract
{
use RebindsCallbacksToSelf;
/**
* The application instance.
*
* @var \Illuminate\Contracts\Foundation\Application
*/
protected $app;
/**
* The array of resolved cache stores.
*
* @var array
*/
protected $stores = [];
/**
* The registered custom driver creators.
*
* @var array
*/
protected $customCreators = [];
/**
* Create a new Cache manager instance.
*
* @param \Illuminate\Contracts\Foundation\Application $app
*/
public function __construct($app)
{
$this->app = $app;
}
/**
* Get a cache store instance by name, wrapped in a repository.
*
* @param \UnitEnum|string|null $name
* @return \Illuminate\Contracts\Cache\Repository
*/
public function store($name = null)
{
$name = enum_value($name) ?? $this->getDefaultDriver();
return $this->stores[$name] ??= $this->resolve($name);
}
/**
* Get a cache driver instance.
*
* @param \UnitEnum|string|null $driver
* @return \Illuminate\Contracts\Cache\Repository
*/
public function driver($driver = null)
{
return $this->store($driver);
}
/**
* Get a memoized cache driver instance.
*
* @param \UnitEnum|string|null $driver
* @return \Illuminate\Contracts\Cache\Repository
*/
public function memo($driver = null)
{
$driver = enum_value($driver) ?? $this->getDefaultDriver();
$bindingKey = "cache.__memoized:{$driver}";
$isSpy = isset($this->app['cache']) && $this->app['cache'] instanceof LegacyMockInterface;
$this->app->scopedIf($bindingKey, function () use ($driver, $isSpy) {
$repository = $this->repository(
new MemoizedStore($driver, $this->store($driver)), ['events' => false]
);
return $isSpy ? Mockery::spy($repository) : $repository;
});
return $this->app->make($bindingKey);
}
/**
* Resolve the given store.
*
* @param string $name
* @return \Illuminate\Contracts\Cache\Repository
*
* @throws \InvalidArgumentException
*/
public function resolve($name)
{
$config = $this->getConfig($name);
if (is_null($config)) {
throw new InvalidArgumentException("Cache store [{$name}] is not defined.");
}
$config = Arr::add($config, 'store', $name);
return $this->build($config);
}
/**
* Build a cache repository with the given configuration.
*
* @param array $config
* @return \Illuminate\Cache\Repository
*
* @throws \InvalidArgumentException
*/
public function build(array $config)
{
$config = Arr::add($config, 'store', $config['name'] ?? 'ondemand');
if (isset($this->customCreators[$config['driver']])) {
return $this->callCustomCreator($config);
}
$driverMethod = 'create'.ucfirst($config['driver']).'Driver';
if (method_exists($this, $driverMethod)) {
return $this->{$driverMethod}($config);
}
throw new InvalidArgumentException("Driver [{$config['driver']}] is not supported.");
}
/**
* Call a custom driver creator.
*
* @param array $config
* @return mixed
*/
protected function callCustomCreator(array $config)
{
return $this->customCreators[$config['driver']]($this->app, $config);
}
/**
* Create an instance of the APC cache driver.
*
* @param array $config
* @return \Illuminate\Cache\Repository
*/
protected function createApcDriver(array $config)
{
$prefix = $this->getPrefix($config);
return $this->repository(new ApcStore(new ApcWrapper, $prefix), $config);
}
/**
* Create an instance of the array cache driver.
*
* @param array $config
* @return \Illuminate\Cache\Repository
*/
protected function createArrayDriver(array $config)
{
return $this->repository(new ArrayStore(
$config['serialize'] ?? false,
$this->getSerializableClasses($config),
), $config);
}
/**
* Create an instance of the database cache driver.
*
* @param array $config
* @return \Illuminate\Cache\Repository
*/
protected function createDatabaseDriver(array $config)
{
$connection = $this->app['db']->connection($config['connection'] ?? null);
$store = new DatabaseStore(
$connection,
$config['table'],
$this->getPrefix($config),
$config['lock_table'] ?? 'cache_locks',
$config['lock_lottery'] ?? [2, 100],
$config['lock_timeout'] ?? 86400,
$this->getSerializableClasses($config),
);
return $this->repository(
$store->setLockConnection(
$this->app['db']->connection($config['lock_connection'] ?? $config['connection'] ?? null)
),
$config
);
}
/**
* Create an instance of the DynamoDB cache driver.
*
* @param array $config
* @return \Illuminate\Cache\Repository
*/
protected function createDynamodbDriver(array $config)
{
$client = $this->newDynamodbClient($config);
return $this->repository(
new DynamoDbStore(
$client,
$config['table'],
$config['attributes']['key'] ?? 'key',
$config['attributes']['value'] ?? 'value',
$config['attributes']['expiration'] ?? 'expires_at',
$this->getPrefix($config),
$this->getSerializableClasses($config),
),
$config
);
}
/**
* Create new DynamoDb Client instance.
*
* @return \Aws\DynamoDb\DynamoDbClient
*/
protected function newDynamodbClient(array $config)
{
$dynamoConfig = [
'region' => $config['region'],
'version' => 'latest',
'endpoint' => $config['endpoint'] ?? null,
];
if (! empty($config['key']) && ! empty($config['secret'])) {
$dynamoConfig['credentials'] = Arr::only(
$config, ['key', 'secret']
);
if (! empty($config['token'])) {
$dynamoConfig['credentials']['token'] = $config['token'];
}
}
return new DynamoDbClient($dynamoConfig);
}
/**
* Create an instance of the failover cache driver.
*
* @param array $config
* @return \Illuminate\Cache\Repository
*/
protected function createFailoverDriver(array $config)
{
return $this->repository(new FailoverStore(
$this,
$this->app->make(DispatcherContract::class),
$config['stores']
), ['events' => false, ...$config]);
}
/**
* Create an instance of the file cache driver.
*
* @param array $config
* @return \Illuminate\Cache\Repository
*/
protected function createFileDriver(array $config)
{
return $this->repository(
(new FileStore(
$this->app['files'],
$config['path'],
$config['permission'] ?? null,
$this->getSerializableClasses($config),
))
->setLockDirectory($config['lock_path'] ?? null),
$config
);
}
/**
* Create an instance of the Memcached cache driver.
*
* @param array $config
* @return \Illuminate\Cache\Repository
*/
protected function createMemcachedDriver(array $config)
{
$prefix = $this->getPrefix($config);
$memcached = $this->app['memcached.connector']->connect(
$config['servers'],
$config['persistent_id'] ?? null,
$config['options'] ?? [],
array_filter($config['sasl'] ?? [])
);
return $this->repository(new MemcachedStore($memcached, $prefix), $config);
}
/**
* Create an instance of the Null cache driver.
*
* @return \Illuminate\Cache\Repository
*/
protected function createNullDriver()
{
return $this->repository(new NullStore, []);
}
/**
* Create an instance of the Redis cache driver.
*
* @param array $config
* @return \Illuminate\Cache\Repository
*/
protected function createRedisDriver(array $config)
{
$redis = $this->app['redis'];
$connection = $config['connection'] ?? 'default';
$store = new RedisStore(
$redis,
$this->getPrefix($config),
$connection,
$this->getSerializableClasses($config),
);
return $this->repository(
$store->setLockConnection($config['lock_connection'] ?? $connection),
$config
);
}
/**
* Create an instance of the session cache driver.
*
* @param array $config
* @return \Illuminate\Cache\Repository
*/
protected function createSessionDriver(array $config)
{
return $this->repository(
new SessionStore(
$this->getSession(),
$config['key'] ?? '_cache',
),
$config
);
}
/**
* Get the session store implementation.
*
* @return \Illuminate\Contracts\Session\Session
*
* @throws \InvalidArgumentException
*/
protected function getSession()
{
$session = $this->app['session'] ?? null;
if (! $session) {
throw new InvalidArgumentException('Session store requires session manager to be available in container.');
}
return $session;
}
/**
* Create a new cache repository with the given implementation.
*
* @param \Illuminate\Contracts\Cache\Store $store
* @param array $config
* @return \Illuminate\Cache\Repository
*/
public function repository(Store $store, array $config = [])
{
return tap(new Repository($store, Arr::only($config, ['store'])), function ($repository) use ($config) {
if ($config['events'] ?? true) {
$this->setEventDispatcher($repository);
}
});
}
/**
* Set the event dispatcher on the given repository instance.
*
* @param \Illuminate\Cache\Repository $repository
* @return void
*/
protected function setEventDispatcher(Repository $repository)
{
if (! $this->app->bound(DispatcherContract::class)) {
return;
}
$repository->setEventDispatcher(
$this->app[DispatcherContract::class]
);
}
/**
* Re-set the event dispatcher on all resolved cache repositories.
*
* @return void
*/
public function refreshEventDispatcher()
{
array_map($this->setEventDispatcher(...), $this->stores);
}
/**
* Get the cache prefix.
*
* @param array $config
* @return string
*/
protected function getPrefix(array $config)
{
return $config['prefix'] ?? $this->app['config']['cache.prefix'];
}
/**
* Get the classes that should be allowed during unserialization.
*
* @param array $config
* @return array|bool|null
*/
protected function getSerializableClasses(array $config)
{
return $this->app['config']['cache.serializable_classes'] ?? null;
}
/**
* Get the cache connection configuration.
*
* @param string $name
* @return array|null
*/
protected function getConfig($name)
{
return $name !== 'null'
? $this->app['config']["cache.stores.{$name}"]
: ['driver' => 'null'];
}
/**
* Get the default cache driver name.
*
* @return string
*/
public function getDefaultDriver()
{
return $this->app['config']['cache.default'] ?? 'null';
}
/**
* Set the default cache driver name.
*
* @param \UnitEnum|string $name
* @return void
*/
public function setDefaultDriver($name)
{
$this->app['config']['cache.default'] = enum_value($name);
}
/**
* Unset the given driver instances.
*
* @param array|\UnitEnum|string|null $name
* @return $this
*/
public function forgetDriver($name = null)
{
$name ??= $this->getDefaultDriver();
foreach ((array) $name as $cacheName) {
$cacheName = enum_value($cacheName);
if (isset($this->stores[$cacheName])) {
unset($this->stores[$cacheName]);
}
}
return $this;
}
/**
* Disconnect the given driver and remove from local cache.
*
* @param \UnitEnum|string|null $name
* @return void
*/
public function purge($name = null)
{
$name = enum_value($name) ?? $this->getDefaultDriver();
unset($this->stores[$name]);
}
/**
* Register a custom driver creator Closure.
*
* @param string $driver
* @param \Closure $callback
*
* @param-closure-this $this $callback
*
* @return $this
*/
public function extend($driver, Closure $callback)
{
try {
$callback = $this->bindCallbackToSelf($callback) ?? throw new RuntimeException('Unable to bind custom driver callback');
} catch (ReflectionException $e) {
throw new RuntimeException('Unable to bind custom driver callback', previous: $e);
}
$this->customCreators[$driver] = $callback;
return $this;
}
/**
* Set the application instance used by the manager.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @return $this
*/
public function setApplication($app)
{
$this->app = $app;
return $this;
}
/**
* Register a callback to be invoked when an unserializable class is encountered.
*
* @param callable|null $callback
* @return void
*/
public function handleUnserializableClassUsing(?callable $callback): void
{
Repository::handleUnserializableClassUsing($callback);
}
/**
* Dynamically call the default driver instance.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
return $this->store()->$method(...$parameters);
}
}
+52
View File
@@ -0,0 +1,52 @@
<?php
namespace Illuminate\Cache;
use Illuminate\Contracts\Support\DeferrableProvider;
use Illuminate\Support\ServiceProvider;
use Symfony\Component\Cache\Adapter\Psr16Adapter;
class CacheServiceProvider extends ServiceProvider implements DeferrableProvider
{
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$this->app->singleton('cache', function ($app) {
return new CacheManager($app);
});
$this->app->singleton('cache.store', function ($app) {
return $app['cache']->driver();
});
$this->app->singleton('cache.psr6', function ($app) {
return new Psr16Adapter($app['cache.store']);
});
$this->app->singleton('memcached.connector', function () {
return new MemcachedConnector;
});
$this->app->singleton(RateLimiter::class, function ($app) {
return new RateLimiter($app->make('cache')->driver(
$app['config']->get('cache.limiter')
));
});
}
/**
* Get the services provided by the provider.
*
* @return array
*/
public function provides()
{
return [
'cache', 'cache.store', 'cache.psr6', 'memcached.connector', RateLimiter::class,
];
}
}
@@ -0,0 +1,51 @@
<?php
namespace Illuminate\Cache\Console;
use Illuminate\Console\MigrationGeneratorCommand;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'make:cache-table', aliases: ['cache:table'])]
class CacheTableCommand extends MigrationGeneratorCommand
{
/**
* The console command name.
*
* @var string
*/
protected $name = 'make:cache-table';
/**
* The console command name aliases.
*
* @var array
*/
protected $aliases = ['cache:table'];
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a migration for the cache database table';
/**
* Get the migration table name.
*
* @return string
*/
protected function migrationTableName()
{
return 'cache';
}
/**
* Get the path to the migration stub file.
*
* @return string
*/
protected function migrationStubFile()
{
return __DIR__.'/stubs/cache.stub';
}
}
+188
View File
@@ -0,0 +1,188 @@
<?php
namespace Illuminate\Cache\Console;
use BadMethodCallException;
use Illuminate\Cache\CacheManager;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
#[AsCommand(name: 'cache:clear')]
class ClearCommand extends Command
{
/**
* The console command name.
*
* @var string
*/
protected $name = 'cache:clear';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Flush the application cache';
/**
* The cache manager instance.
*
* @var \Illuminate\Cache\CacheManager
*/
protected $cache;
/**
* The filesystem instance.
*
* @var \Illuminate\Filesystem\Filesystem
*/
protected $files;
/**
* Create a new cache clear command instance.
*
* @param \Illuminate\Cache\CacheManager $cache
* @param \Illuminate\Filesystem\Filesystem $files
*/
public function __construct(CacheManager $cache, Filesystem $files)
{
parent::__construct();
$this->cache = $cache;
$this->files = $files;
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
if ($this->option('locks')) {
return $this->clearLocks();
}
$this->laravel['events']->dispatch(
'cache:clearing', [$this->argument('store'), $this->tags()]
);
$successful = $this->cache()->flush();
$this->flushFacades();
if (! $successful) {
$this->components->error('Failed to clear cache. Make sure you have the appropriate permissions.');
return self::FAILURE;
}
$this->laravel['events']->dispatch(
'cache:cleared', [$this->argument('store'), $this->tags()]
);
$this->components->info('Application cache cleared successfully.');
return self::SUCCESS;
}
/**
* Clear all locks from the cache store.
*
* @return int
*/
protected function clearLocks()
{
if (! empty($this->tags())) {
$this->components->error('Cache tags cannot be used when clearing locks.');
return self::FAILURE;
}
try {
$successful = $this->cache()->flushLocks();
} catch (BadMethodCallException) {
$this->components->error('This cache store does not support clearing locks.');
return self::FAILURE;
}
if (! $successful) {
$this->components->error('Failed to clear cache locks. Make sure you have the appropriate permissions.');
return self::FAILURE;
}
$this->components->info('Application cache locks cleared successfully.');
return self::SUCCESS;
}
/**
* Flush the real-time facades stored in the cache directory.
*
* @return void
*/
public function flushFacades()
{
if (! $this->files->exists($storagePath = storage_path('framework/cache'))) {
return;
}
foreach ($this->files->files($storagePath) as $file) {
if (preg_match('/facade-.*\.php$/', $file)) {
$this->files->delete($file);
}
}
}
/**
* Get the cache instance for the command.
*
* @return \Illuminate\Cache\Repository
*/
protected function cache()
{
$cache = $this->cache->store($this->argument('store'));
return empty($this->tags()) ? $cache : $cache->tags($this->tags());
}
/**
* Get the tags passed to the command.
*
* @return array
*/
protected function tags()
{
return array_filter(explode(',', $this->option('tags') ?? ''));
}
/**
* Get the console command arguments.
*
* @return array
*/
protected function getArguments()
{
return [
['store', InputArgument::OPTIONAL, 'The name of the store you would like to clear'],
];
}
/**
* Get the console command options.
*
* @return array
*/
protected function getOptions()
{
return [
['tags', null, InputOption::VALUE_OPTIONAL, 'The cache tags you would like to clear', null],
['locks', null, InputOption::VALUE_NONE, 'Only clear cache locks'],
];
}
}
+58
View File
@@ -0,0 +1,58 @@
<?php
namespace Illuminate\Cache\Console;
use Illuminate\Cache\CacheManager;
use Illuminate\Console\Command;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'cache:forget')]
class ForgetCommand extends Command
{
/**
* The console command name.
*
* @var string
*/
protected $signature = 'cache:forget {key : The key to remove} {store? : The store to remove the key from}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Remove an item from the cache';
/**
* The cache manager instance.
*
* @var \Illuminate\Cache\CacheManager
*/
protected $cache;
/**
* Create a new cache clear command instance.
*
* @param \Illuminate\Cache\CacheManager $cache
*/
public function __construct(CacheManager $cache)
{
parent::__construct();
$this->cache = $cache;
}
/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
$this->cache->store($this->argument('store'))->forget(
$this->argument('key')
);
$this->components->info('The ['.$this->argument('key').'] key has been removed from the cache.');
}
}
@@ -0,0 +1,55 @@
<?php
namespace Illuminate\Cache\Console;
use Illuminate\Cache\CacheManager;
use Illuminate\Console\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
#[AsCommand(name: 'cache:prune-stale-tags')]
class PruneStaleTagsCommand extends Command
{
/**
* The console command name.
*
* @var string
*/
protected $name = 'cache:prune-stale-tags';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Prune stale cache tags from the cache (Redis only)';
/**
* Execute the console command.
*
* @param \Illuminate\Cache\CacheManager $cache
* @return int|null
*/
public function handle(CacheManager $cache)
{
$cache = $cache->store($this->argument('store'));
if (method_exists($cache->getStore(), 'flushStaleTags')) {
$cache->flushStaleTags();
}
$this->components->info('Stale cache tags pruned successfully.');
}
/**
* Get the console command arguments.
*
* @return array
*/
protected function getArguments()
{
return [
['store', InputArgument::OPTIONAL, 'The name of the store you would like to prune tags from'],
];
}
}
@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('cache', function (Blueprint $table) {
$table->string('key')->primary();
$table->mediumText('value');
$table->bigInteger('expiration')->index();
});
Schema::create('cache_locks', function (Blueprint $table) {
$table->string('key')->primary();
$table->string('owner');
$table->bigInteger('expiration')->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('cache');
Schema::dropIfExists('cache_locks');
}
};
@@ -0,0 +1,189 @@
<?php
namespace Illuminate\Cache;
use Illuminate\Database\Connection;
use Illuminate\Database\DetectsConcurrencyErrors;
use Illuminate\Database\QueryException;
use Throwable;
class DatabaseLock extends Lock
{
use DetectsConcurrencyErrors;
/**
* The database connection instance.
*
* @var \Illuminate\Database\Connection
*/
protected $connection;
/**
* The database table name.
*
* @var string
*/
protected $table;
/**
* The prune probability odds.
*
* @var array{int, int}|null
*/
protected $lottery;
/**
* The default number of seconds that a lock should be held.
*
* @var int
*/
protected $defaultTimeoutInSeconds;
/**
* Create a new lock instance.
*
* @param \Illuminate\Database\Connection $connection
* @param string $table
* @param string $name
* @param int $seconds
* @param string|null $owner
* @param array{int, int}|null $lottery
* @param int $defaultTimeoutInSeconds
*/
public function __construct(Connection $connection, $table, $name, $seconds, $owner = null, $lottery = [2, 100], $defaultTimeoutInSeconds = 86400)
{
parent::__construct($name, $seconds, $owner);
$this->connection = $connection;
$this->table = $table;
$this->lottery = $lottery;
$this->defaultTimeoutInSeconds = $defaultTimeoutInSeconds;
}
/**
* Attempt to acquire the lock.
*
* @return bool
*
* @throws \Throwable
*/
public function acquire()
{
try {
$this->connection->table($this->table)->insert([
'key' => $this->name,
'owner' => $this->owner,
'expiration' => $this->expiresAt(),
]);
$acquired = true;
} catch (QueryException) {
$updated = $this->connection->table($this->table)
->where('key', $this->name)
->where(function ($query) {
return $query->where('owner', $this->owner)->orWhere('expiration', '<=', $this->currentTime());
})->update([
'owner' => $this->owner,
'expiration' => $this->expiresAt(),
]);
$acquired = $updated >= 1;
}
if (count($this->lottery ?? []) === 2 && random_int(1, $this->lottery[1]) <= $this->lottery[0]) {
$this->pruneExpiredLocks();
}
return $acquired;
}
/**
* Get the UNIX timestamp indicating when the lock should expire.
*
* @return int
*/
protected function expiresAt()
{
$lockTimeout = $this->seconds > 0 ? $this->seconds : $this->defaultTimeoutInSeconds;
return $this->currentTime() + $lockTimeout;
}
/**
* Release the lock.
*
* @return bool
*
* @throws \Throwable
*/
public function release()
{
try {
return $this->connection->table($this->table)
->where('key', $this->name)
->where('owner', $this->owner)
->delete() > 0;
} catch (Throwable $e) {
if ($this->causedByConcurrencyError($e)) {
return true;
}
throw $e;
}
}
/**
* Releases this lock in disregard of ownership.
*
* @return void
*/
public function forceRelease()
{
$this->connection->table($this->table)
->where('key', $this->name)
->delete();
}
/**
* Deletes locks that are past expiration.
*
* @return void
*
* @throws \Throwable
*/
public function pruneExpiredLocks()
{
try {
$this->connection->table($this->table)
->where('expiration', '<=', $this->currentTime())
->delete();
} catch (Throwable $e) {
if (! $this->causedByConcurrencyError($e)) {
throw $e;
}
}
}
/**
* Returns the owner value written into the driver for this lock.
*
* @return string|null
*/
protected function getCurrentOwner()
{
return $this->connection->table($this->table)
->where('key', $this->name)
->where('expiration', '>', $this->currentTime())
->first()?->owner;
}
/**
* Get the name of the database connection being used to manage the lock.
*
* @return string
*/
public function getConnectionName()
{
return $this->connection->getName();
}
}
+609
View File
@@ -0,0 +1,609 @@
<?php
namespace Illuminate\Cache;
use Closure;
use Illuminate\Contracts\Cache\CanFlushLocks;
use Illuminate\Contracts\Cache\LockProvider;
use Illuminate\Contracts\Cache\Store;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Database\PostgresConnection;
use Illuminate\Database\QueryException;
use Illuminate\Database\SQLiteConnection;
use Illuminate\Database\SqlServerConnection;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\InteractsWithTime;
use Illuminate\Support\Str;
use RuntimeException;
class DatabaseStore implements CanFlushLocks, LockProvider, Store
{
use InteractsWithTime;
/**
* The database connection instance.
*
* @var \Illuminate\Database\ConnectionInterface
*/
protected $connection;
/**
* The database connection instance that should be used to manage locks.
*
* @var \Illuminate\Database\ConnectionInterface
*/
protected $lockConnection;
/**
* The name of the cache table.
*
* @var string
*/
protected $table;
/**
* A string that should be prepended to keys.
*
* @var string
*/
protected $prefix;
/**
* The name of the cache locks table.
*
* @var string
*/
protected $lockTable;
/**
* An array representation of the lock lottery odds.
*
* @var array
*/
protected $lockLottery;
/**
* The default number of seconds that a lock should be held.
*
* @var int
*/
protected $defaultLockTimeoutInSeconds;
/**
* The classes that should be allowed during unserialization.
*
* @var array|bool|null
*/
protected $serializableClasses;
/**
* Create a new database store.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param string $table
* @param string $prefix
* @param string $lockTable
* @param array $lockLottery
* @param int $defaultLockTimeoutInSeconds
* @param array|bool|null $serializableClasses
*/
public function __construct(
ConnectionInterface $connection,
$table,
$prefix = '',
$lockTable = 'cache_locks',
$lockLottery = [2, 100],
$defaultLockTimeoutInSeconds = 86400,
$serializableClasses = null,
) {
$this->table = $table;
$this->prefix = $prefix;
$this->connection = $connection;
$this->lockTable = $lockTable;
$this->lockLottery = $lockLottery;
$this->defaultLockTimeoutInSeconds = $defaultLockTimeoutInSeconds;
$this->serializableClasses = $serializableClasses;
}
/**
* Retrieve an item from the cache by key.
*
* @param string $key
* @return mixed
*/
public function get($key)
{
return $this->many([$key])[$key];
}
/**
* Retrieve multiple items from the cache by key.
*
* Items not found in the cache will have a null value.
*
* @return array
*/
public function many(array $keys)
{
if ($keys === []) {
return [];
}
$results = array_fill_keys($keys, null);
// First we will retrieve all of the items from the cache using their keys and
// the prefix value. Then we will need to iterate through each of the items
// and convert them to an object when they are currently in array format.
$values = $this->table()
->whereIn('key', array_map(function ($key) {
return $this->prefix.$key;
}, $keys))
->get()
->map(function ($value) {
return is_array($value) ? (object) $value : $value;
});
$currentTime = $this->currentTime();
// If this cache expiration date is past the current time, we will remove this
// item from the cache. Then we will return a null value since the cache is
// expired. We will use "Carbon" to make this comparison with the column.
[$values, $expired] = $values->partition(function ($cache) use ($currentTime) {
return $cache->expiration > $currentTime;
});
if ($expired->isNotEmpty()) {
$this->forgetManyIfExpired($expired->pluck('key')->all(), prefixed: true);
}
return Arr::map($results, function ($value, $key) use ($values) {
if ($cache = $values->firstWhere('key', $this->prefix.$key)) {
return $this->unserialize($cache->value);
}
return $value;
});
}
/**
* Store an item in the cache for a given number of seconds.
*
* @param string $key
* @param mixed $value
* @param int $seconds
* @return bool
*/
public function put($key, $value, $seconds)
{
return $this->putMany([$key => $value], $seconds);
}
/**
* Store multiple items in the cache for a given number of seconds.
*
* @param array $values
* @param int $seconds
* @return bool
*/
public function putMany(array $values, $seconds)
{
$serializedValues = [];
$expiration = $this->getTime() + $seconds;
foreach ($values as $key => $value) {
$serializedValues[] = [
'key' => $this->prefix.$key,
'value' => $this->serialize($value),
'expiration' => $expiration,
];
}
return $this->table()->upsert($serializedValues, 'key') > 0;
}
/**
* Store an item in the cache if the key doesn't exist.
*
* @param string $key
* @param mixed $value
* @param int $seconds
* @return bool
*/
public function add($key, $value, $seconds)
{
if (! is_null($this->get($key))) {
return false;
}
$key = $this->prefix.$key;
$value = $this->serialize($value);
$expiration = $this->getTime() + $seconds;
if (! $this->getConnection() instanceof SqlServerConnection) {
return $this->table()->insertOrIgnore(compact('key', 'value', 'expiration')) > 0;
}
try {
return $this->table()->insert(compact('key', 'value', 'expiration'));
} catch (QueryException) {
// ...
}
return false;
}
/**
* Increment the value of an item in the cache.
*
* @param string $key
* @param int $value
* @return int|false
*/
public function increment($key, $value = 1)
{
return $this->incrementOrDecrement($key, $value, function ($current, $value) {
return $current + $value;
});
}
/**
* Decrement the value of an item in the cache.
*
* @param string $key
* @param int $value
* @return int|false
*/
public function decrement($key, $value = 1)
{
return $this->incrementOrDecrement($key, $value, function ($current, $value) {
return $current - $value;
});
}
/**
* Increment or decrement an item in the cache.
*
* @param string $key
* @param int|float $value
* @param \Closure $callback
* @return int|false
*/
protected function incrementOrDecrement($key, $value, Closure $callback)
{
return $this->connection->transaction(function () use ($key, $value, $callback) {
$prefixed = $this->prefix.$key;
$cache = $this->table()->where('key', $prefixed)
->lockForUpdate()->first();
// If there is no value in the cache, we will return false here. Otherwise the
// value will be decrypted and we will proceed with this function to either
// increment or decrement this value based on the given action callbacks.
if (is_null($cache)) {
return false;
}
$cache = is_array($cache) ? (object) $cache : $cache;
$current = $this->unserialize($cache->value);
// Here we'll call this callback function that was given to the function which
// is used to either increment or decrement the function. We use a callback
// so we do not have to recreate all this logic in each of the functions.
$new = $callback((int) $current, $value);
if (! is_numeric($current)) {
return false;
}
// Here we will update the values in the table. We will also encrypt the value
// since database cache values are encrypted by default with secure storage
// that can't be easily read. We will return the new value after storing.
$this->table()->where('key', $prefixed)->update([
'value' => $this->serialize($new),
]);
return $new;
});
}
/**
* Get the current system time.
*
* @return int
*/
protected function getTime()
{
return $this->currentTime();
}
/**
* Store an item in the cache indefinitely.
*
* @param string $key
* @param mixed $value
* @return bool
*/
public function forever($key, $value)
{
return $this->put($key, $value, 315360000);
}
/**
* Get a lock instance.
*
* @param string $name
* @param int $seconds
* @param string|null $owner
* @return \Illuminate\Contracts\Cache\Lock
*/
public function lock($name, $seconds = 0, $owner = null)
{
return new DatabaseLock(
$this->lockConnection ?? $this->connection,
$this->lockTable,
$this->prefix.$name,
$seconds,
$owner,
$this->lockLottery,
$this->defaultLockTimeoutInSeconds
);
}
/**
* Restore a lock instance using the owner identifier.
*
* @param string $name
* @param string $owner
* @return \Illuminate\Contracts\Cache\Lock
*/
public function restoreLock($name, $owner)
{
return $this->lock($name, 0, $owner);
}
/**
* Adjust the expiration time of a cached item.
*
* @param string $key
* @param int $seconds
* @return bool
*/
public function touch($key, $seconds)
{
return (bool) $this->table()
->where('key', '=', $this->getPrefix().$key)
->where('expiration', '>', $now = $this->getTime())
->update(['expiration' => $now + $seconds]);
}
/**
* Remove an item from the cache.
*
* @param string $key
* @return bool
*/
public function forget($key)
{
return $this->forgetMany([$key]);
}
/**
* Remove an item from the cache if it is expired.
*
* @param string $key
* @return bool
*/
public function forgetIfExpired($key)
{
return $this->forgetManyIfExpired([$key]);
}
/**
* Remove all items from the cache.
*
* @param array $keys
* @return bool
*/
protected function forgetMany(array $keys)
{
$this->table()->whereIn('key', (new Collection($keys))->flatMap(fn ($key) => [
$this->prefix.$key,
"{$this->prefix}illuminate:cache:flexible:created:{$key}",
])->all())->delete();
return true;
}
/**
* Remove all expired items from the given set from the cache.
*
* @param array $keys
* @param bool $prefixed
* @return bool
*/
protected function forgetManyIfExpired(array $keys, bool $prefixed = false)
{
$this->table()
->whereIn('key', (new Collection($keys))->flatMap(fn ($key) => $prefixed ? [
$key,
$this->prefix.'illuminate:cache:flexible:created:'.Str::chopStart($key, $this->prefix),
] : [
"{$this->prefix}{$key}",
"{$this->prefix}illuminate:cache:flexible:created:{$key}",
])->all())
->where('expiration', '<=', $this->getTime())
->delete();
return true;
}
/**
* Remove all items from the cache.
*
* @return bool
*/
public function flush()
{
$this->table()->delete();
return true;
}
/**
* Remove all locks from the store.
*
* @return bool
*
* @throws \RuntimeException
*/
public function flushLocks(): bool
{
if (! $this->hasSeparateLockStore()) {
throw new RuntimeException('Flushing locks is only supported when the lock store is separate from the cache store.');
}
$this->lockTable()->delete();
return true;
}
/**
* Get a query builder for the cache table.
*
* @return \Illuminate\Database\Query\Builder
*/
protected function table()
{
return $this->connection->table($this->table);
}
/**
* Get a query builder for the cache locks table.
*
* @return \Illuminate\Database\Query\Builder
*/
protected function lockTable()
{
return $this->lockConnection->table($this->lockTable);
}
/**
* Get the underlying database connection.
*
* @return \Illuminate\Database\ConnectionInterface
*/
public function getConnection()
{
return $this->connection;
}
/**
* Set the underlying database connection.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @return $this
*/
public function setConnection($connection)
{
$this->connection = $connection;
return $this;
}
/**
* Get the connection used to manage locks.
*
* @return \Illuminate\Database\ConnectionInterface
*/
public function getLockConnection()
{
return $this->lockConnection;
}
/**
* Specify the connection that should be used to manage locks.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @return $this
*/
public function setLockConnection($connection)
{
$this->lockConnection = $connection;
return $this;
}
/**
* Get the cache key prefix.
*
* @return string
*/
public function getPrefix()
{
return $this->prefix;
}
/**
* Set the cache key prefix.
*
* @param string $prefix
* @return void
*/
public function setPrefix($prefix)
{
$this->prefix = $prefix;
}
/**
* Serialize the given value.
*
* @param mixed $value
* @return string
*/
protected function serialize($value)
{
$result = serialize($value);
if (($this->connection instanceof PostgresConnection ||
$this->connection instanceof SQLiteConnection) &&
str_contains($result, "\0")) {
$result = base64_encode($result);
}
return $result;
}
/**
* Unserialize the given value.
*
* @param string $value
* @return mixed
*/
protected function unserialize($value)
{
if (($this->connection instanceof PostgresConnection ||
$this->connection instanceof SQLiteConnection) &&
! Str::contains($value, [':', ';'])) {
$value = base64_decode($value);
}
if ($this->serializableClasses !== null) {
return unserialize($value, ['allowed_classes' => $this->serializableClasses]);
}
return unserialize($value);
}
/**
* Determine if the lock store is separate from the cache store.
*
* @return bool
*/
public function hasSeparateLockStore(): bool
{
return $this->lockTable !== $this->table;
}
}
@@ -0,0 +1,76 @@
<?php
namespace Illuminate\Cache;
class DynamoDbLock extends Lock
{
/**
* The DynamoDB client instance.
*
* @var \Illuminate\Cache\DynamoDbStore
*/
protected $dynamo;
/**
* Create a new lock instance.
*
* @param \Illuminate\Cache\DynamoDbStore $dynamo
* @param string $name
* @param int $seconds
* @param string|null $owner
*/
public function __construct(DynamoDbStore $dynamo, $name, $seconds, $owner = null)
{
parent::__construct($name, $seconds, $owner);
$this->dynamo = $dynamo;
}
/**
* Attempt to acquire the lock.
*
* @return bool
*/
public function acquire()
{
if ($this->seconds > 0) {
return $this->dynamo->add($this->name, $this->owner, $this->seconds);
}
return $this->dynamo->add($this->name, $this->owner, 86400);
}
/**
* Release the lock.
*
* @return bool
*/
public function release()
{
if ($this->isOwnedByCurrentProcess()) {
return $this->dynamo->forget($this->name);
}
return false;
}
/**
* Release this lock in disregard of ownership.
*
* @return void
*/
public function forceRelease()
{
$this->dynamo->forget($this->name);
}
/**
* Returns the owner value written into the driver for this lock.
*
* @return mixed
*/
protected function getCurrentOwner()
{
return $this->dynamo->get($this->name);
}
}
@@ -0,0 +1,562 @@
<?php
namespace Illuminate\Cache;
use Aws\DynamoDb\DynamoDbClient;
use Aws\DynamoDb\Exception\DynamoDbException;
use Illuminate\Contracts\Cache\LockProvider;
use Illuminate\Contracts\Cache\Store;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\InteractsWithTime;
use Illuminate\Support\Str;
use RuntimeException;
class DynamoDbStore implements LockProvider, Store
{
use InteractsWithTime;
/**
* A string that should be prepended to keys.
*
* @var string
*/
protected $prefix;
/**
* The classes that should be allowed during unserialization.
*
* @var array|bool|null
*/
protected $serializableClasses;
/**
* Create a new store instance.
*
* @param \Aws\DynamoDb\DynamoDbClient $dynamo The DynamoDB client instance.
* @param string $table The table name.
* @param string $keyAttribute The name of the attribute that should hold the key.
* @param string $valueAttribute The name of the attribute that should hold the value.
* @param string $expirationAttribute The name of the attribute that should hold the expiration timestamp.
* @param string $prefix
* @param array|bool|null $serializableClasses
*/
public function __construct(
protected DynamoDbClient $dynamo,
protected $table,
protected $keyAttribute = 'key',
protected $valueAttribute = 'value',
protected $expirationAttribute = 'expires_at',
$prefix = '',
$serializableClasses = null,
) {
$this->setPrefix($prefix);
$this->serializableClasses = $serializableClasses;
}
/**
* Retrieve an item from the cache by key.
*
* @param string $key
* @return mixed
*/
public function get($key)
{
$response = $this->dynamo->getItem([
'TableName' => $this->table,
'ConsistentRead' => false,
'Key' => [
$this->keyAttribute => [
'S' => $this->prefix.$key,
],
],
]);
if (! isset($response['Item'])) {
return;
}
if ($this->isExpired($response['Item'])) {
return;
}
if (isset($response['Item'][$this->valueAttribute])) {
return $this->unserialize(
$response['Item'][$this->valueAttribute]['S'] ??
$response['Item'][$this->valueAttribute]['N'] ??
null
);
}
}
/**
* Retrieve multiple items from the cache by key.
*
* Items not found in the cache will have a null value.
*
* @param array $keys
* @return array
*/
public function many(array $keys)
{
if ($keys === []) {
return [];
}
$prefixedKeys = array_map(function ($key) {
return $this->prefix.$key;
}, $keys);
$response = $this->dynamo->batchGetItem([
'RequestItems' => [
$this->table => [
'ConsistentRead' => false,
'Keys' => (new Collection($prefixedKeys))->map(fn ($key) => [
$this->keyAttribute => [
'S' => $key,
],
])->all(),
],
],
]);
$now = Carbon::now();
return array_merge(
Arr::mapWithKeys($keys, fn ($key) => [$key => null]),
(new Collection($response['Responses'][$this->table]))->mapWithKeys(function ($response) use ($now) {
if ($this->isExpired($response, $now)) {
$value = null;
} else {
$value = $this->unserialize(
$response[$this->valueAttribute]['S'] ??
$response[$this->valueAttribute]['N'] ??
null
);
}
return [Str::replaceFirst($this->prefix, '', $response[$this->keyAttribute]['S']) => $value];
})->all());
}
/**
* Determine if the given item is expired.
*
* @param array $item
* @param \DateTimeInterface|null $expiration
* @return bool
*/
protected function isExpired(array $item, $expiration = null)
{
$expiration = $expiration ?: Carbon::now();
return isset($item[$this->expirationAttribute]) &&
$expiration->getTimestamp() >= $item[$this->expirationAttribute]['N'];
}
/**
* Store an item in the cache for a given number of seconds.
*
* @param string $key
* @param mixed $value
* @param int $seconds
* @return bool
*/
public function put($key, $value, $seconds)
{
$this->dynamo->putItem([
'TableName' => $this->table,
'Item' => [
$this->keyAttribute => [
'S' => $this->prefix.$key,
],
$this->valueAttribute => [
$this->type($value) => $this->serialize($value),
],
$this->expirationAttribute => [
'N' => (string) $this->toTimestamp($seconds),
],
],
]);
return true;
}
/**
* Store multiple items in the cache for a given number of seconds.
*
* @param array $values
* @param int $seconds
* @return bool
*/
public function putMany(array $values, $seconds)
{
if ($values === []) {
return true;
}
$expiration = $this->toTimestamp($seconds);
$this->dynamo->batchWriteItem([
'RequestItems' => [
$this->table => (new Collection($values))->map(function ($value, $key) use ($expiration) {
return [
'PutRequest' => [
'Item' => [
$this->keyAttribute => [
'S' => $this->prefix.$key,
],
$this->valueAttribute => [
$this->type($value) => $this->serialize($value),
],
$this->expirationAttribute => [
'N' => (string) $expiration,
],
],
],
];
})->values()->all(),
],
]);
return true;
}
/**
* Store an item in the cache if the key doesn't exist.
*
* @param string $key
* @param mixed $value
* @param int $seconds
* @return bool
*
* @throws \Aws\DynamoDb\Exception\DynamoDbException
*/
public function add($key, $value, $seconds)
{
try {
$this->dynamo->putItem([
'TableName' => $this->table,
'Item' => [
$this->keyAttribute => [
'S' => $this->prefix.$key,
],
$this->valueAttribute => [
$this->type($value) => $this->serialize($value),
],
$this->expirationAttribute => [
'N' => (string) $this->toTimestamp($seconds),
],
],
'ConditionExpression' => 'attribute_not_exists(#key) OR #expires_at < :now',
'ExpressionAttributeNames' => [
'#key' => $this->keyAttribute,
'#expires_at' => $this->expirationAttribute,
],
'ExpressionAttributeValues' => [
':now' => [
'N' => (string) $this->currentTime(),
],
],
]);
return true;
} catch (DynamoDbException $e) {
if (str_contains($e->getMessage(), 'ConditionalCheckFailed')) {
return false;
}
throw $e;
}
}
/**
* Increment the value of an item in the cache.
*
* @param string $key
* @param mixed $value
* @return int|false
*
* @throws \Aws\DynamoDb\Exception\DynamoDbException
*/
public function increment($key, $value = 1)
{
try {
$response = $this->dynamo->updateItem([
'TableName' => $this->table,
'Key' => [
$this->keyAttribute => [
'S' => $this->prefix.$key,
],
],
'ConditionExpression' => 'attribute_exists(#key) AND #expires_at > :now',
'UpdateExpression' => 'SET #value = #value + :amount',
'ExpressionAttributeNames' => [
'#key' => $this->keyAttribute,
'#value' => $this->valueAttribute,
'#expires_at' => $this->expirationAttribute,
],
'ExpressionAttributeValues' => [
':now' => [
'N' => (string) $this->currentTime(),
],
':amount' => [
'N' => (string) $value,
],
],
'ReturnValues' => 'UPDATED_NEW',
]);
return (int) $response['Attributes'][$this->valueAttribute]['N'];
} catch (DynamoDbException $e) {
if (str_contains($e->getMessage(), 'ConditionalCheckFailed')) {
return false;
}
throw $e;
}
}
/**
* Decrement the value of an item in the cache.
*
* @param string $key
* @param mixed $value
* @return int|false
*
* @throws \Aws\DynamoDb\Exception\DynamoDbException
*/
public function decrement($key, $value = 1)
{
try {
$response = $this->dynamo->updateItem([
'TableName' => $this->table,
'Key' => [
$this->keyAttribute => [
'S' => $this->prefix.$key,
],
],
'ConditionExpression' => 'attribute_exists(#key) AND #expires_at > :now',
'UpdateExpression' => 'SET #value = #value - :amount',
'ExpressionAttributeNames' => [
'#key' => $this->keyAttribute,
'#value' => $this->valueAttribute,
'#expires_at' => $this->expirationAttribute,
],
'ExpressionAttributeValues' => [
':now' => [
'N' => (string) $this->currentTime(),
],
':amount' => [
'N' => (string) $value,
],
],
'ReturnValues' => 'UPDATED_NEW',
]);
return (int) $response['Attributes'][$this->valueAttribute]['N'];
} catch (DynamoDbException $e) {
if (str_contains($e->getMessage(), 'ConditionalCheckFailed')) {
return false;
}
throw $e;
}
}
/**
* Store an item in the cache indefinitely.
*
* @param string $key
* @param mixed $value
* @return bool
*/
public function forever($key, $value)
{
return $this->put($key, $value, Carbon::now()->addYears(5)->getTimestamp());
}
/**
* Get a lock instance.
*
* @param string $name
* @param int $seconds
* @param string|null $owner
* @return \Illuminate\Contracts\Cache\Lock
*/
public function lock($name, $seconds = 0, $owner = null)
{
return new DynamoDbLock($this, $name, $seconds, $owner);
}
/**
* Restore a lock instance using the owner identifier.
*
* @param string $name
* @param string $owner
* @return \Illuminate\Contracts\Cache\Lock
*/
public function restoreLock($name, $owner)
{
return $this->lock($name, 0, $owner);
}
/**
* Adjust the expiration time of a cached item.
*
* @param string $key
* @param int $seconds
* @return bool
*
* @throws DynamoDbException
*/
public function touch($key, $seconds)
{
try {
$this->dynamo->updateItem([
'TableName' => $this->table,
'Key' => [$this->keyAttribute => ['S' => $this->getPrefix().$key]],
'UpdateExpression' => 'SET #expiry = :expiry',
'ConditionExpression' => 'attribute_exists(#key) AND #expiry > :now',
'ExpressionAttributeNames' => [
'#key' => $this->keyAttribute,
'#expiry' => $this->expirationAttribute,
],
'ExpressionAttributeValues' => [
':expiry' => ['N' => (string) $this->toTimestamp($seconds)],
':now' => ['N' => (string) $this->currentTime()],
],
]);
} catch (DynamoDbException $e) {
if (str_contains($e->getMessage(), 'ConditionalCheckFailed')) {
return false;
}
throw $e;
}
return true;
}
/**
* Remove an item from the cache.
*
* @param string $key
* @return bool
*/
public function forget($key)
{
$this->dynamo->deleteItem([
'TableName' => $this->table,
'Key' => [
$this->keyAttribute => [
'S' => $this->prefix.$key,
],
],
]);
return true;
}
/**
* Remove all items from the cache.
*
* @return never
*
* @throws \RuntimeException
*/
public function flush()
{
throw new RuntimeException('DynamoDb does not support flushing an entire table. Please create a new table.');
}
/**
* Get the UNIX timestamp for the given number of seconds.
*
* @param int $seconds
* @return int
*/
protected function toTimestamp($seconds)
{
return $seconds > 0
? $this->availableAt($seconds)
: $this->currentTime();
}
/**
* Serialize the value.
*
* @param mixed $value
* @return mixed
*/
protected function serialize($value)
{
return is_numeric($value) ? (string) $value : serialize($value);
}
/**
* Unserialize the value.
*
* @param mixed $value
* @return mixed
*/
protected function unserialize($value)
{
if (filter_var($value, FILTER_VALIDATE_INT) !== false) {
return (int) $value;
}
if (is_numeric($value)) {
return (float) $value;
}
if ($this->serializableClasses !== null) {
return unserialize($value, ['allowed_classes' => $this->serializableClasses]);
}
return unserialize($value);
}
/**
* Get the DynamoDB type for the given value.
*
* @param mixed $value
* @return string
*/
protected function type($value)
{
return is_numeric($value) ? 'N' : 'S';
}
/**
* Get the cache key prefix.
*
* @return string
*/
public function getPrefix()
{
return $this->prefix;
}
/**
* Set the cache key prefix.
*
* @param string $prefix
* @return void
*/
public function setPrefix($prefix)
{
$this->prefix = $prefix;
}
/**
* Get the DynamoDb Client instance.
*
* @return \Aws\DynamoDb\DynamoDbClient
*/
public function getClient()
{
return $this->dynamo;
}
}
@@ -0,0 +1,54 @@
<?php
namespace Illuminate\Cache\Events;
abstract class CacheEvent
{
/**
* The name of the cache store.
*
* @var string|null
*/
public $storeName;
/**
* The key of the event.
*
* @var string
*/
public $key;
/**
* The tags that were assigned to the key.
*
* @var array
*/
public $tags;
/**
* Create a new event instance.
*
* @param string|null $storeName
* @param string $key
* @param array $tags
*/
public function __construct($storeName, $key, array $tags = [])
{
$this->storeName = $storeName;
$this->key = $key;
$this->tags = $tags;
}
/**
* Set the tags for the cache event.
*
* @param array $tags
* @return $this
*/
public function setTags($tags)
{
$this->tags = $tags;
return $this;
}
}
@@ -0,0 +1,20 @@
<?php
namespace Illuminate\Cache\Events;
use Throwable;
class CacheFailedOver
{
/**
* Create a new event instance.
*
* @param string|null $storeName The name of the cache store that failed.
* @param \Throwable $exception The exception that was thrown.
*/
public function __construct(
public ?string $storeName,
public Throwable $exception,
) {
}
}
@@ -0,0 +1,45 @@
<?php
namespace Illuminate\Cache\Events;
class CacheFlushFailed
{
/**
* The name of the cache store.
*
* @var string|null
*/
public $storeName;
/**
* The tags that were assigned to the key.
*
* @var array
*/
public $tags;
/**
* Create a new event instance.
*
* @param string|null $storeName
* @param array $tags
*/
public function __construct($storeName, array $tags = [])
{
$this->storeName = $storeName;
$this->tags = $tags;
}
/**
* Set the tags for the cache event.
*
* @param array $tags
* @return $this
*/
public function setTags($tags)
{
$this->tags = $tags;
return $this;
}
}
@@ -0,0 +1,45 @@
<?php
namespace Illuminate\Cache\Events;
class CacheFlushed
{
/**
* The name of the cache store.
*
* @var string|null
*/
public $storeName;
/**
* The tags that were assigned to the key.
*
* @var array
*/
public $tags;
/**
* Create a new event instance.
*
* @param string|null $storeName
* @param array $tags
*/
public function __construct($storeName, array $tags = [])
{
$this->storeName = $storeName;
$this->tags = $tags;
}
/**
* Set the tags for the cache event.
*
* @param array $tags
* @return $this
*/
public function setTags($tags)
{
$this->tags = $tags;
return $this;
}
}
@@ -0,0 +1,45 @@
<?php
namespace Illuminate\Cache\Events;
class CacheFlushing
{
/**
* The name of the cache store.
*
* @var string|null
*/
public $storeName;
/**
* The tags that were assigned to the key.
*
* @var array
*/
public $tags;
/**
* Create a new event instance.
*
* @param string|null $storeName
* @param array $tags
*/
public function __construct($storeName, array $tags = [])
{
$this->storeName = $storeName;
$this->tags = $tags;
}
/**
* Set the tags for the cache event.
*
* @param array $tags
* @return $this
*/
public function setTags($tags)
{
$this->tags = $tags;
return $this;
}
}
@@ -0,0 +1,28 @@
<?php
namespace Illuminate\Cache\Events;
class CacheHit extends CacheEvent
{
/**
* The value that was retrieved.
*
* @var mixed
*/
public $value;
/**
* Create a new event instance.
*
* @param string|null $storeName
* @param string $key
* @param mixed $value
* @param array $tags
*/
public function __construct($storeName, $key, $value, array $tags = [])
{
parent::__construct($storeName, $key, $tags);
$this->value = $value;
}
}
@@ -0,0 +1,23 @@
<?php
namespace Illuminate\Cache\Events;
class CacheLocksFlushFailed
{
/**
* The name of the cache store.
*
* @var string|null
*/
public ?string $storeName;
/**
* Create a new event instance.
*
* @param string|null $storeName
*/
public function __construct(?string $storeName)
{
$this->storeName = $storeName;
}
}
@@ -0,0 +1,23 @@
<?php
namespace Illuminate\Cache\Events;
class CacheLocksFlushed
{
/**
* The name of the cache store.
*
* @var string|null
*/
public ?string $storeName;
/**
* Create a new event instance.
*
* @param string|null $storeName
*/
public function __construct(?string $storeName)
{
$this->storeName = $storeName;
}
}
@@ -0,0 +1,23 @@
<?php
namespace Illuminate\Cache\Events;
class CacheLocksFlushing
{
/**
* The name of the cache store.
*
* @var string|null
*/
public ?string $storeName;
/**
* Create a new event instance.
*
* @param string|null $storeName
*/
public function __construct(?string $storeName)
{
$this->storeName = $storeName;
}
}
@@ -0,0 +1,8 @@
<?php
namespace Illuminate\Cache\Events;
class CacheMissed extends CacheEvent
{
//
}
@@ -0,0 +1,8 @@
<?php
namespace Illuminate\Cache\Events;
class ForgettingKey extends CacheEvent
{
//
}
@@ -0,0 +1,8 @@
<?php
namespace Illuminate\Cache\Events;
class KeyForgetFailed extends CacheEvent
{
//
}
@@ -0,0 +1,8 @@
<?php
namespace Illuminate\Cache\Events;
class KeyForgotten extends CacheEvent
{
//
}
@@ -0,0 +1,37 @@
<?php
namespace Illuminate\Cache\Events;
class KeyWriteFailed extends CacheEvent
{
/**
* The value that would have been written.
*
* @var mixed
*/
public $value;
/**
* The number of seconds the key should have been valid.
*
* @var int|null
*/
public $seconds;
/**
* Create a new event instance.
*
* @param string|null $storeName
* @param string $key
* @param mixed $value
* @param int|null $seconds
* @param array $tags
*/
public function __construct($storeName, $key, $value, $seconds = null, $tags = [])
{
parent::__construct($storeName, $key, $tags);
$this->value = $value;
$this->seconds = $seconds;
}
}
@@ -0,0 +1,37 @@
<?php
namespace Illuminate\Cache\Events;
class KeyWritten extends CacheEvent
{
/**
* The value that was written.
*
* @var mixed
*/
public $value;
/**
* The number of seconds the key should be valid.
*
* @var int|null
*/
public $seconds;
/**
* Create a new event instance.
*
* @param string|null $storeName
* @param string $key
* @param mixed $value
* @param int|null $seconds
* @param array $tags
*/
public function __construct($storeName, $key, $value, $seconds = null, $tags = [])
{
parent::__construct($storeName, $key, $tags);
$this->value = $value;
$this->seconds = $seconds;
}
}
@@ -0,0 +1,8 @@
<?php
namespace Illuminate\Cache\Events;
class RetrievingKey extends CacheEvent
{
//
}
@@ -0,0 +1,27 @@
<?php
namespace Illuminate\Cache\Events;
class RetrievingManyKeys extends CacheEvent
{
/**
* The keys that are being retrieved.
*
* @var array
*/
public $keys;
/**
* Create a new event instance.
*
* @param string|null $storeName
* @param array $keys
* @param array $tags
*/
public function __construct($storeName, $keys, array $tags = [])
{
parent::__construct($storeName, $keys[0] ?? '', $tags);
$this->keys = $keys;
}
}
@@ -0,0 +1,37 @@
<?php
namespace Illuminate\Cache\Events;
class WritingKey extends CacheEvent
{
/**
* The value that will be written.
*
* @var mixed
*/
public $value;
/**
* The number of seconds the key should be valid.
*
* @var int|null
*/
public $seconds;
/**
* Create a new event instance.
*
* @param string|null $storeName
* @param string $key
* @param mixed $value
* @param int|null $seconds
* @param array $tags
*/
public function __construct($storeName, $key, $value, $seconds = null, $tags = [])
{
parent::__construct($storeName, $key, $tags);
$this->value = $value;
$this->seconds = $seconds;
}
}
@@ -0,0 +1,45 @@
<?php
namespace Illuminate\Cache\Events;
class WritingManyKeys extends CacheEvent
{
/**
* The keys that are being written.
*
* @var mixed
*/
public $keys;
/**
* The value that is being written.
*
* @var mixed
*/
public $values;
/**
* The number of seconds the keys should be valid.
*
* @var int|null
*/
public $seconds;
/**
* Create a new event instance.
*
* @param string|null $storeName
* @param array $keys
* @param array $values
* @param int|null $seconds
* @param array $tags
*/
public function __construct($storeName, $keys, $values, $seconds = null, $tags = [])
{
parent::__construct($storeName, $keys[0], $tags);
$this->keys = $keys;
$this->values = $values;
$this->seconds = $seconds;
}
}
@@ -0,0 +1,286 @@
<?php
namespace Illuminate\Cache;
use Illuminate\Cache\Events\CacheFailedOver;
use Illuminate\Contracts\Cache\CanFlushLocks;
use Illuminate\Contracts\Cache\LockProvider;
use Illuminate\Contracts\Events\Dispatcher;
use RuntimeException;
use Throwable;
class FailoverStore extends TaggableStore implements CanFlushLocks, LockProvider
{
/**
* The caches which failed on the last action.
*
* @var list<string>
*/
protected array $failingCaches = [];
/**
* Create a new failover store.
*
* @param array<int, string> $stores
*/
public function __construct(
protected CacheManager $cache,
protected Dispatcher $events,
protected array $stores
) {
}
/**
* Retrieve an item from the cache by key.
*
* @param string $key
* @return mixed
*/
public function get($key)
{
return $this->attemptOnAllStores(__FUNCTION__, func_get_args());
}
/**
* Retrieve multiple items from the cache by key.
*
* Items not found in the cache will have a null value.
*
* @return array
*/
public function many(array $keys)
{
return $this->attemptOnAllStores(__FUNCTION__, func_get_args());
}
/**
* Store an item in the cache for a given number of seconds.
*
* @param string $key
* @param mixed $value
* @param int $seconds
* @return bool
*/
public function put($key, $value, $seconds)
{
return $this->attemptOnAllStores(__FUNCTION__, func_get_args());
}
/**
* Store multiple items in the cache for a given number of seconds.
*
* @param int $seconds
* @return bool
*/
public function putMany(array $values, $seconds)
{
return $this->attemptOnAllStores(__FUNCTION__, func_get_args());
}
/**
* Store an item in the cache if the key doesn't exist.
*
* @param string $key
* @param mixed $value
* @param int $seconds
* @return bool
*/
public function add($key, $value, $seconds)
{
return $this->attemptOnAllStores(__FUNCTION__, func_get_args());
}
/**
* Increment the value of an item in the cache.
*
* @param string $key
* @param mixed $value
* @return int|false
*/
public function increment($key, $value = 1)
{
return $this->attemptOnAllStores(__FUNCTION__, func_get_args());
}
/**
* Decrement the value of an item in the cache.
*
* @param string $key
* @param mixed $value
* @return int|false
*/
public function decrement($key, $value = 1)
{
return $this->attemptOnAllStores(__FUNCTION__, func_get_args());
}
/**
* Store an item in the cache indefinitely.
*
* @param string $key
* @param mixed $value
* @return bool
*/
public function forever($key, $value)
{
return $this->attemptOnAllStores(__FUNCTION__, func_get_args());
}
/**
* Get a lock instance.
*
* @param string $name
* @param int $seconds
* @param string|null $owner
* @return \Illuminate\Contracts\Cache\Lock
*/
public function lock($name, $seconds = 0, $owner = null)
{
return $this->attemptOnAllStores(__FUNCTION__, func_get_args());
}
/**
* Restore a lock instance using the owner identifier.
*
* @param string $name
* @param string $owner
* @return \Illuminate\Contracts\Cache\Lock
*/
public function restoreLock($name, $owner)
{
return $this->attemptOnAllStores(__FUNCTION__, func_get_args());
}
/**
* Adjust the expiration time of a cached item.
*
* @param string $key
* @param int $seconds
* @return bool
*/
public function touch($key, $seconds)
{
return $this->attemptOnAllStores(__FUNCTION__, func_get_args());
}
/**
* Remove an item from the cache.
*
* @param string $key
* @return bool
*/
public function forget($key)
{
return $this->attemptOnAllStores(__FUNCTION__, func_get_args());
}
/**
* Remove all items from the cache.
*
* @return bool
*/
public function flush()
{
return $this->attemptOnAllStores(__FUNCTION__, func_get_args());
}
/**
* Remove all expired tag set entries.
*
* @return void
*/
public function flushStaleTags()
{
foreach ($this->stores as $store) {
if ($this->store($store)->getStore() instanceof RedisStore) {
$this->store($store)->flushStaleTags();
break;
}
}
}
/**
* Flush all of the stale locks from every backing store.
*
* @return bool
*/
public function flushLocks(): bool
{
$result = true;
foreach ($this->stores as $store) {
$underlyingStore = $this->store($store)->getStore();
if ($underlyingStore instanceof CanFlushLocks) {
if (! $underlyingStore->flushLocks()) {
$result = false;
}
}
}
return $result;
}
/**
* Determine if the lock store is separate from the cache store.
*
* @return bool
*/
public function hasSeparateLockStore(): bool
{
return true;
}
/**
* Get the cache key prefix.
*
* @return string
*/
public function getPrefix()
{
return $this->attemptOnAllStores(__FUNCTION__, func_get_args());
}
/**
* Attempt the given method on all stores.
*
* @return mixed
*
* @throws \Throwable
*/
protected function attemptOnAllStores(string $method, array $arguments)
{
[$lastException, $failedCaches] = [null, []];
try {
foreach ($this->stores as $store) {
try {
return $this->store($store)->{$method}(...$arguments);
} catch (Throwable $e) {
$lastException = $e;
$failedCaches[] = $store;
if (! in_array($store, $this->failingCaches)) {
$this->events->dispatch(new CacheFailedOver($store, $e));
}
}
}
} finally {
$this->failingCaches = $failedCaches;
}
throw $lastException ?? new RuntimeException('All failover cache stores failed.');
}
/**
* Get the cache store for the given store name.
*
* @return \Illuminate\Contracts\Cache\Repository
*/
protected function store(string $store)
{
return $this->cache->store($store);
}
}
@@ -0,0 +1,16 @@
<?php
namespace Illuminate\Cache;
class FileLock extends CacheLock
{
/**
* Attempt to acquire the lock.
*
* @return bool
*/
public function acquire()
{
return $this->store->add($this->name, $this->owner, $this->seconds);
}
}
+503
View File
@@ -0,0 +1,503 @@
<?php
namespace Illuminate\Cache;
use Exception;
use Illuminate\Contracts\Cache\CanFlushLocks;
use Illuminate\Contracts\Cache\LockProvider;
use Illuminate\Contracts\Cache\Store;
use Illuminate\Contracts\Filesystem\LockTimeoutException;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Filesystem\LockableFile;
use Illuminate\Support\InteractsWithTime;
use RuntimeException;
class FileStore implements CanFlushLocks, LockProvider, Store
{
use InteractsWithTime, RetrievesMultipleKeys;
/**
* The Illuminate Filesystem instance.
*
* @var \Illuminate\Filesystem\Filesystem
*/
protected $files;
/**
* The file cache directory.
*
* @var string
*/
protected $directory;
/**
* The file cache lock directory.
*
* @var string|null
*/
protected $lockDirectory;
/**
* Octal representation of the cache file permissions.
*
* @var int|null
*/
protected $filePermission;
/**
* The classes that should be allowed during unserialization.
*
* @var array|bool|null
*/
protected $serializableClasses;
/**
* Create a new file cache store instance.
*
* @param \Illuminate\Filesystem\Filesystem $files
* @param string $directory
* @param int|null $filePermission
* @param array|bool|null $serializableClasses
*/
public function __construct(Filesystem $files, $directory, $filePermission = null, $serializableClasses = null)
{
$this->files = $files;
$this->directory = $directory;
$this->filePermission = $filePermission;
$this->serializableClasses = $serializableClasses;
}
/**
* Retrieve an item from the cache by key.
*
* @param string $key
* @return mixed
*/
public function get($key)
{
return $this->getPayload($key)['data'] ?? null;
}
/**
* Store an item in the cache for a given number of seconds.
*
* @param string $key
* @param mixed $value
* @param int $seconds
* @return bool
*/
public function put($key, $value, $seconds)
{
$this->ensureCacheDirectoryExists($path = $this->path($key));
$result = $this->files->put(
$path, $this->expiration($seconds).serialize($value), true
);
if ($result !== false && $result > 0) {
$this->ensurePermissionsAreCorrect($path);
return true;
}
return false;
}
/**
* Store an item in the cache if the key doesn't exist.
*
* @param string $key
* @param mixed $value
* @param int $seconds
* @return bool
*/
public function add($key, $value, $seconds)
{
$this->ensureCacheDirectoryExists($path = $this->path($key));
$file = new LockableFile($path, 'c+');
try {
$file->getExclusiveLock();
} catch (LockTimeoutException) {
$file->close();
return false;
}
$expire = $file->read(10);
if (empty($expire) || $this->currentTime() >= $expire) {
$file->truncate()
->write($this->expiration($seconds).serialize($value))
->close();
$this->ensurePermissionsAreCorrect($path);
return true;
}
$file->close();
return false;
}
/**
* Create the file cache directory if necessary.
*
* @param string $path
* @return void
*/
protected function ensureCacheDirectoryExists($path)
{
$directory = dirname($path);
if (! $this->files->exists($directory)) {
$this->files->makeDirectory($directory, 0777, true, true);
// We're creating two levels of directories (e.g. 7e/24), so we check them both...
$this->ensurePermissionsAreCorrect($directory);
$this->ensurePermissionsAreCorrect(dirname($directory));
}
}
/**
* Ensure the created node has the correct permissions.
*
* @param string $path
* @return void
*/
protected function ensurePermissionsAreCorrect($path)
{
if (is_null($this->filePermission) ||
intval($this->files->chmod($path), 8) == $this->filePermission) {
return;
}
$this->files->chmod($path, $this->filePermission);
}
/**
* Increment the value of an item in the cache.
*
* @param string $key
* @param mixed $value
* @return int
*/
public function increment($key, $value = 1)
{
$raw = $this->getPayload($key);
return tap(((int) $raw['data']) + $value, function ($newValue) use ($key, $raw) {
$this->put($key, $newValue, $raw['time'] ?? 0);
});
}
/**
* Decrement the value of an item in the cache.
*
* @param string $key
* @param mixed $value
* @return int
*/
public function decrement($key, $value = 1)
{
return $this->increment($key, $value * -1);
}
/**
* Store an item in the cache indefinitely.
*
* @param string $key
* @param mixed $value
* @return bool
*/
public function forever($key, $value)
{
return $this->put($key, $value, 0);
}
/**
* Get a lock instance.
*
* @param string $name
* @param int $seconds
* @param string|null $owner
* @return \Illuminate\Contracts\Cache\Lock
*/
public function lock($name, $seconds = 0, $owner = null)
{
$this->ensureCacheDirectoryExists($this->lockDirectory ?? $this->directory);
return new FileLock(
new static($this->files, $this->lockDirectory ?? $this->directory, $this->filePermission, $this->serializableClasses),
"file-store-lock:{$name}",
$seconds,
$owner
);
}
/**
* Restore a lock instance using the owner identifier.
*
* @param string $name
* @param string $owner
* @return \Illuminate\Contracts\Cache\Lock
*/
public function restoreLock($name, $owner)
{
return $this->lock($name, 0, $owner);
}
/**
* Adjust the expiration time of a cached item.
*
* @param string $key
* @param int $seconds
* @return bool
*/
public function touch($key, $seconds)
{
$payload = $this->getPayload($this->getPrefix().$key);
if (is_null($payload['data'])) {
return false;
}
return $this->put($key, $payload['data'], $seconds);
}
/**
* Remove an item from the cache.
*
* @param string $key
* @return bool
*/
public function forget($key)
{
if ($this->files->exists($file = $this->path($key))) {
return tap($this->files->delete($file), function ($forgotten) use ($key) {
if ($forgotten && $this->files->exists($file = $this->path("illuminate:cache:flexible:created:{$key}"))) {
$this->files->delete($file);
}
});
}
return false;
}
/**
* Remove all items from the cache.
*
* @return bool
*/
public function flush()
{
if (! $this->files->isDirectory($this->directory)) {
return false;
}
foreach ($this->files->directories($this->directory) as $directory) {
$deleted = $this->files->deleteDirectory($directory);
if (! $deleted || $this->files->exists($directory)) {
return false;
}
}
return true;
}
/**
* Remove all locks from the store.
*
* @return bool
*
* @throws \RuntimeException
*/
public function flushLocks(): bool
{
if (! $this->hasSeparateLockStore()) {
throw new RuntimeException('Flushing locks is only supported when the lock store is separate from the cache store.');
}
if (! $this->files->isDirectory($this->lockDirectory)) {
return false;
}
foreach ($this->files->directories($this->lockDirectory) as $lockDirectory) {
$deleted = $this->files->deleteDirectory($lockDirectory);
if (! $deleted || $this->files->exists($lockDirectory)) {
return false;
}
}
return true;
}
/**
* Retrieve an item and expiry time from the cache by key.
*
* @param string $key
* @return array
*/
protected function getPayload($key)
{
$path = $this->path($key);
// If the file doesn't exist, we obviously cannot return the cache so we will
// just return null. Otherwise, we'll get the contents of the file and get
// the expiration UNIX timestamps from the start of the file's contents.
try {
if (is_null($contents = $this->files->get($path, true))) {
return $this->emptyPayload();
}
$expire = substr($contents, 0, 10);
} catch (Exception) {
return $this->emptyPayload();
}
// If the current time is greater than expiration timestamps we will delete
// the file and return null. This helps clean up the old files and keeps
// this directory much cleaner for us as old files aren't hanging out.
if ($this->currentTime() >= $expire) {
$this->forget($key);
return $this->emptyPayload();
}
try {
$data = $this->unserialize(substr($contents, 10));
} catch (Exception) {
$this->forget($key);
return $this->emptyPayload();
}
// Next, we'll extract the number of seconds that are remaining for a cache
// so that we can properly retain the time for things like the increment
// operation that may be performed on this cache on a later operation.
$time = $expire - $this->currentTime();
return compact('data', 'time');
}
/**
* Unserialize the given value.
*
* @param string $value
* @return mixed
*/
protected function unserialize($value)
{
if ($this->serializableClasses !== null) {
return unserialize($value, ['allowed_classes' => $this->serializableClasses]);
}
return unserialize($value);
}
/**
* Get a default empty payload for the cache.
*
* @return array
*/
protected function emptyPayload()
{
return ['data' => null, 'time' => null];
}
/**
* Get the full path for the given cache key.
*
* @param string $key
* @return string
*/
public function path($key)
{
$parts = array_slice(str_split($hash = sha1($key), 2), 0, 2);
return $this->directory.'/'.implode('/', $parts).'/'.$hash;
}
/**
* Get the expiration time based on the given seconds.
*
* @param int $seconds
* @return int
*/
protected function expiration($seconds)
{
$time = $this->availableAt($seconds);
return $seconds === 0 || $time > 9999999999 ? 9999999999 : $time;
}
/**
* Get the Filesystem instance.
*
* @return \Illuminate\Filesystem\Filesystem
*/
public function getFilesystem()
{
return $this->files;
}
/**
* Get the working directory of the cache.
*
* @return string
*/
public function getDirectory()
{
return $this->directory;
}
/**
* Set the working directory of the cache.
*
* @param string $directory
* @return $this
*/
public function setDirectory($directory)
{
$this->directory = $directory;
return $this;
}
/**
* Set the cache directory where locks should be stored.
*
* @param string|null $lockDirectory
* @return $this
*/
public function setLockDirectory($lockDirectory)
{
$this->lockDirectory = $lockDirectory;
return $this;
}
/**
* Get the cache key prefix.
*
* @return string
*/
public function getPrefix()
{
return '';
}
/**
* Determine if the lock store is separate from the cache store.
*
* @return bool
*/
public function hasSeparateLockStore(): bool
{
return $this->lockDirectory !== null && $this->lockDirectory !== $this->directory;
}
}
@@ -0,0 +1,31 @@
<?php
namespace Illuminate\Cache;
trait HasCacheLock
{
/**
* Get a lock instance.
*
* @param string $name
* @param int $seconds
* @param string|null $owner
* @return \Illuminate\Contracts\Cache\Lock
*/
public function lock($name, $seconds = 0, $owner = null)
{
return new CacheLock($this, $name, $seconds, $owner);
}
/**
* Restore a lock instance using the owner identifier.
*
* @param string $name
* @param string $owner
* @return \Illuminate\Contracts\Cache\Lock
*/
public function restoreLock($name, $owner)
{
return $this->lock($name, 0, $owner);
}
}
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Taylor Otwell
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@@ -0,0 +1,124 @@
<?php
namespace Illuminate\Cache\Limiters;
use Illuminate\Support\Sleep;
use Illuminate\Support\Str;
use Throwable;
class ConcurrencyLimiter
{
/**
* The cache store instance.
*
* @var \Illuminate\Contracts\Cache\LockProvider
*/
protected $store;
/**
* The name of the limiter.
*
* @var string
*/
protected $name;
/**
* The allowed number of concurrent locks.
*
* @var int
*/
protected $maxLocks;
/**
* The number of seconds a slot should be maintained.
*
* @var int
*/
protected $releaseAfter;
/**
* Create a new concurrency limiter instance.
*
* @param \Illuminate\Contracts\Cache\LockProvider $store
* @param string $name
* @param int $maxLocks
* @param int $releaseAfter
*/
public function __construct($store, $name, $maxLocks, $releaseAfter)
{
$this->name = $name;
$this->store = $store;
$this->maxLocks = $maxLocks;
$this->releaseAfter = $releaseAfter;
}
/**
* Attempt to acquire the lock for the given number of seconds.
*
* @param int $timeout
* @param callable|null $callback
* @param int $sleep
* @return mixed
*
* @throws \Illuminate\Cache\Limiters\LimiterTimeoutException
* @throws \Throwable
*/
public function block($timeout, $callback = null, $sleep = 250)
{
$starting = time();
$id = Str::random(20);
while (! $slot = $this->acquire($id)) {
if (time() - $timeout >= $starting) {
throw new LimiterTimeoutException;
}
Sleep::usleep($sleep * 1000);
}
if (is_callable($callback)) {
try {
return tap($callback(), function () use ($slot) {
$this->release($slot);
});
} catch (Throwable $exception) {
$this->release($slot);
throw $exception;
}
}
return true;
}
/**
* Attempt to acquire a slot lock.
*
* @param string $id
* @return \Illuminate\Contracts\Cache\Lock|false
*/
protected function acquire($id)
{
for ($i = 1; $i <= $this->maxLocks; $i++) {
$lock = $this->store->lock($this->name.$i, $this->releaseAfter, $id);
if ($lock->acquire()) {
return $lock;
}
}
return false;
}
/**
* Release the lock.
*
* @param \Illuminate\Contracts\Cache\Lock $lock
* @return void
*/
protected function release($lock)
{
$lock->release();
}
}
@@ -0,0 +1,153 @@
<?php
namespace Illuminate\Cache\Limiters;
use Illuminate\Support\InteractsWithTime;
class ConcurrencyLimiterBuilder
{
use InteractsWithTime;
/**
* The cache repository or Redis connection.
*
* @var \Illuminate\Cache\Repository
*/
public $connection;
/**
* The name of the lock.
*
* @var string
*/
public $name;
/**
* The maximum number of entities that can hold the lock at the same time.
*
* @var int
*/
public $maxLocks;
/**
* The number of seconds to maintain the lock until it is automatically released.
*
* @var int
*/
public $releaseAfter = 60;
/**
* The number of seconds to block until a lock is available.
*
* @var int
*/
public $timeout = 3;
/**
* The number of milliseconds to wait between attempts to acquire the lock.
*
* @var int
*/
public $sleep = 250;
/**
* Create a new builder instance.
*
* @param mixed $connection
* @param string $name
*/
public function __construct($connection, $name)
{
$this->name = $name;
$this->connection = $connection;
}
/**
* Set the maximum number of locks that can be obtained per time window.
*
* @param int $maxLocks
* @return $this
*/
public function limit($maxLocks)
{
$this->maxLocks = $maxLocks;
return $this;
}
/**
* Set the number of seconds until the lock will be released.
*
* @param int $releaseAfter
* @return $this
*/
public function releaseAfter($releaseAfter)
{
$this->releaseAfter = $this->secondsUntil($releaseAfter);
return $this;
}
/**
* Set the number of seconds to block until a lock is available.
*
* @param int $timeout
* @return $this
*/
public function block($timeout)
{
$this->timeout = $timeout;
return $this;
}
/**
* The number of milliseconds to wait between lock acquisition attempts.
*
* @param int $sleep
* @return $this
*/
public function sleep($sleep)
{
$this->sleep = $sleep;
return $this;
}
/**
* Execute the given callback if a lock is obtained, otherwise call the failure callback.
*
* @param callable $callback
* @param callable|null $failure
* @return mixed
*
* @throws \Illuminate\Cache\Limiters\LimiterTimeoutException
*/
public function then(callable $callback, ?callable $failure = null)
{
try {
return $this->createLimiter()->block($this->timeout, $callback, $this->sleep);
} catch (LimiterTimeoutException $e) {
if ($failure) {
return $failure($e);
}
throw $e;
}
}
/**
* Create the concurrency limiter instance.
*
* @return \Illuminate\Cache\Limiters\ConcurrencyLimiter
*/
protected function createLimiter()
{
return new ConcurrencyLimiter(
$this->connection->getStore(),
$this->name,
$this->maxLocks,
$this->releaseAfter
);
}
}
@@ -0,0 +1,10 @@
<?php
namespace Illuminate\Cache\Limiters;
use Exception;
class LimiterTimeoutException extends Exception
{
//
}
+193
View File
@@ -0,0 +1,193 @@
<?php
namespace Illuminate\Cache;
use Illuminate\Contracts\Cache\Lock as LockContract;
use Illuminate\Contracts\Cache\LockTimeoutException;
use Illuminate\Support\Carbon;
use Illuminate\Support\InteractsWithTime;
use Illuminate\Support\Sleep;
use Illuminate\Support\Str;
abstract class Lock implements LockContract
{
use InteractsWithTime;
/**
* The name of the lock.
*
* @var string
*/
protected $name;
/**
* The number of seconds the lock should be maintained.
*
* @var int
*/
protected $seconds;
/**
* The scope identifier of this lock.
*
* @var string
*/
protected $owner;
/**
* The number of milliseconds to wait before re-attempting to acquire a lock while blocking.
*
* @var int
*/
protected $sleepMilliseconds = 250;
/**
* Create a new lock instance.
*
* @param string $name
* @param int $seconds
* @param string|null $owner
*/
public function __construct($name, $seconds, $owner = null)
{
if (is_null($owner)) {
$owner = Str::random();
}
$this->name = $name;
$this->owner = $owner;
$this->seconds = $seconds;
}
/**
* Attempt to acquire the lock.
*
* @return bool
*/
abstract public function acquire();
/**
* Release the lock.
*
* @return bool
*/
abstract public function release();
/**
* Returns the owner value written into the driver for this lock.
*
* @return string|null
*/
abstract protected function getCurrentOwner();
/**
* Attempt to acquire the lock.
*
* @param callable|null $callback
* @return mixed
*/
public function get($callback = null)
{
$result = $this->acquire();
if ($result && is_callable($callback)) {
try {
return $callback();
} finally {
$this->release();
}
}
return $result;
}
/**
* Attempt to acquire the lock for the given number of seconds.
*
* @param int $seconds
* @param callable|null $callback
* @return mixed
*
* @throws \Illuminate\Contracts\Cache\LockTimeoutException
*/
public function block($seconds, $callback = null)
{
$starting = ((int) Carbon::now()->format('Uu')) / 1000;
$milliseconds = $seconds * 1000;
while (! $this->acquire()) {
$now = ((int) Carbon::now()->format('Uu')) / 1000;
if (($now + $this->sleepMilliseconds - $milliseconds) >= $starting) {
throw new LockTimeoutException;
}
Sleep::usleep($this->sleepMilliseconds * 1000);
}
if (is_callable($callback)) {
try {
return $callback();
} finally {
$this->release();
}
}
return true;
}
/**
* Returns the current owner of the lock.
*
* @return string
*/
public function owner()
{
return $this->owner;
}
/**
* Determine if the lock is currently held by any process.
*
* @return bool
*/
public function isLocked(): bool
{
return $this->getCurrentOwner() !== null;
}
/**
* Determines whether this lock is allowed to release the lock in the driver.
*
* @return bool
*/
public function isOwnedByCurrentProcess()
{
return $this->isOwnedBy($this->owner);
}
/**
* Determine whether this lock is owned by the given identifier.
*
* @param string|null $owner
* @return bool
*/
public function isOwnedBy($owner)
{
return $this->getCurrentOwner() === $owner;
}
/**
* Specify the number of milliseconds to sleep in between blocked lock acquisition attempts.
*
* @param int $milliseconds
* @return $this
*/
public function betweenBlockedAttemptsSleepFor($milliseconds)
{
$this->sleepMilliseconds = $milliseconds;
return $this;
}
}
@@ -0,0 +1,41 @@
<?php
namespace Illuminate\Cache;
class LuaScripts
{
/**
* Get the Lua script that sets a key only when it does not yet exist.
*
* KEYS[1] - The name of the key
* ARGV[1] - The value of the key
* ARGV[2] - The number of seconds the key should be valid
*
* @return string
*/
public static function add()
{
return <<<'LUA'
return redis.call('exists',KEYS[1])<1 and redis.call('setex',KEYS[1],ARGV[2],ARGV[1])
LUA;
}
/**
* Get the Lua script to atomically release a lock.
*
* KEYS[1] - The name of the lock
* ARGV[1] - The owner key of the lock instance trying to release it
*
* @return string
*/
public static function releaseLock()
{
return <<<'LUA'
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
LUA;
}
}
+87
View File
@@ -0,0 +1,87 @@
<?php
namespace Illuminate\Cache;
use Memcached;
class MemcachedConnector
{
/**
* Create a new Memcached connection.
*
* @param array $servers
* @param string|null $connectionId
* @param array $options
* @param array $credentials
* @return \Memcached
*/
public function connect(array $servers, $connectionId = null, array $options = [], array $credentials = [])
{
$memcached = $this->getMemcached(
$connectionId, $credentials, $options
);
if (! $memcached->getServerList()) {
// For each server in the array, we'll just extract the configuration and add
// the server to the Memcached connection. Once we have added all of these
// servers we'll verify the connection is successful and return it back.
foreach ($servers as $server) {
$memcached->addServer(
$server['host'], $server['port'], $server['weight']
);
}
}
return $memcached;
}
/**
* Get a new Memcached instance.
*
* @param string|null $connectionId
* @param array $credentials
* @param array $options
* @return \Memcached
*/
protected function getMemcached($connectionId, array $credentials, array $options)
{
$memcached = $this->createMemcachedInstance($connectionId);
if (count($credentials) === 2) {
$this->setCredentials($memcached, $credentials);
}
if ($options !== []) {
$memcached->setOptions($options);
}
return $memcached;
}
/**
* Create the Memcached instance.
*
* @param string|null $connectionId
* @return \Memcached
*/
protected function createMemcachedInstance($connectionId)
{
return empty($connectionId) ? new Memcached : new Memcached($connectionId);
}
/**
* Set the SASL credentials on the Memcached connection.
*
* @param \Memcached $memcached
* @param array $credentials
* @return void
*/
protected function setCredentials($memcached, $credentials)
{
[$username, $password] = $credentials;
$memcached->setOption(Memcached::OPT_BINARY_PROTOCOL, true);
$memcached->setSaslAuthData($username, $password);
}
}
@@ -0,0 +1,74 @@
<?php
namespace Illuminate\Cache;
class MemcachedLock extends Lock
{
/**
* The Memcached instance.
*
* @var \Memcached
*/
protected $memcached;
/**
* Create a new lock instance.
*
* @param \Memcached $memcached
* @param string $name
* @param int $seconds
* @param string|null $owner
*/
public function __construct($memcached, $name, $seconds, $owner = null)
{
parent::__construct($name, $seconds, $owner);
$this->memcached = $memcached;
}
/**
* Attempt to acquire the lock.
*
* @return bool
*/
public function acquire()
{
return $this->memcached->add(
$this->name, $this->owner, $this->seconds
);
}
/**
* Release the lock.
*
* @return bool
*/
public function release()
{
if ($this->isOwnedByCurrentProcess()) {
return $this->memcached->delete($this->name);
}
return false;
}
/**
* Releases this lock in disregard of ownership.
*
* @return void
*/
public function forceRelease()
{
$this->memcached->delete($this->name);
}
/**
* Returns the owner value written into the driver for this lock.
*
* @return mixed
*/
protected function getCurrentOwner()
{
return $this->memcached->get($this->name);
}
}
+290
View File
@@ -0,0 +1,290 @@
<?php
namespace Illuminate\Cache;
use Illuminate\Contracts\Cache\LockProvider;
use Illuminate\Support\InteractsWithTime;
use Memcached;
use ReflectionMethod;
class MemcachedStore extends TaggableStore implements LockProvider
{
use InteractsWithTime;
/**
* The Memcached instance.
*
* @var \Memcached
*/
protected $memcached;
/**
* A string that should be prepended to keys.
*
* @var string
*/
protected $prefix;
/**
* Indicates whether we are using Memcached version >= 3.0.0.
*
* @var bool
*/
protected $onVersionThree;
/**
* Create a new Memcached store.
*
* @param \Memcached $memcached
* @param string $prefix
*/
public function __construct($memcached, $prefix = '')
{
$this->setPrefix($prefix);
$this->memcached = $memcached;
$this->onVersionThree = (new ReflectionMethod('Memcached', 'getMulti'))
->getNumberOfParameters() == 2;
}
/**
* Retrieve an item from the cache by key.
*
* @param string $key
* @return mixed
*/
public function get($key)
{
$value = $this->memcached->get($this->prefix.$key);
if ($this->memcached->getResultCode() == 0) {
return $value;
}
}
/**
* Retrieve multiple items from the cache by key.
*
* Items not found in the cache will have a null value.
*
* @param array $keys
* @return array
*/
public function many(array $keys)
{
$prefixedKeys = array_map(function ($key) {
return $this->prefix.$key;
}, $keys);
if ($this->onVersionThree) {
$values = $this->memcached->getMulti($prefixedKeys, Memcached::GET_PRESERVE_ORDER);
} else {
$null = null;
$values = $this->memcached->getMulti($prefixedKeys, $null, Memcached::GET_PRESERVE_ORDER);
}
if ($this->memcached->getResultCode() != 0) {
return array_fill_keys($keys, null);
}
return array_combine($keys, $values);
}
/**
* Store an item in the cache for a given number of seconds.
*
* @param string $key
* @param mixed $value
* @param int $seconds
* @return bool
*/
public function put($key, $value, $seconds)
{
return $this->memcached->set(
$this->prefix.$key, $value, $this->calculateExpiration($seconds)
);
}
/**
* Store multiple items in the cache for a given number of seconds.
*
* @param array $values
* @param int $seconds
* @return bool
*/
public function putMany(array $values, $seconds)
{
$prefixedValues = [];
foreach ($values as $key => $value) {
$prefixedValues[$this->prefix.$key] = $value;
}
return $this->memcached->setMulti(
$prefixedValues, $this->calculateExpiration($seconds)
);
}
/**
* Store an item in the cache if the key doesn't exist.
*
* @param string $key
* @param mixed $value
* @param int $seconds
* @return bool
*/
public function add($key, $value, $seconds)
{
return $this->memcached->add(
$this->prefix.$key, $value, $this->calculateExpiration($seconds)
);
}
/**
* Increment the value of an item in the cache.
*
* @param string $key
* @param mixed $value
* @return int|false
*/
public function increment($key, $value = 1)
{
return $this->memcached->increment($this->prefix.$key, $value);
}
/**
* Decrement the value of an item in the cache.
*
* @param string $key
* @param mixed $value
* @return int|false
*/
public function decrement($key, $value = 1)
{
return $this->memcached->decrement($this->prefix.$key, $value);
}
/**
* Store an item in the cache indefinitely.
*
* @param string $key
* @param mixed $value
* @return bool
*/
public function forever($key, $value)
{
return $this->put($key, $value, 0);
}
/**
* Get a lock instance.
*
* @param string $name
* @param int $seconds
* @param string|null $owner
* @return \Illuminate\Contracts\Cache\Lock
*/
public function lock($name, $seconds = 0, $owner = null)
{
return new MemcachedLock($this->memcached, $this->prefix.$name, $seconds, $owner);
}
/**
* Restore a lock instance using the owner identifier.
*
* @param string $name
* @param string $owner
* @return \Illuminate\Contracts\Cache\Lock
*/
public function restoreLock($name, $owner)
{
return $this->lock($name, 0, $owner);
}
/**
* Adjust the expiration time of a cached item.
*
* @param string $key
* @param int $seconds
* @return bool
*/
public function touch($key, $seconds)
{
return $this->memcached->touch($this->getPrefix().$key, $this->calculateExpiration($seconds));
}
/**
* Remove an item from the cache.
*
* @param string $key
* @return bool
*/
public function forget($key)
{
return $this->memcached->delete($this->prefix.$key);
}
/**
* Remove all items from the cache.
*
* @return bool
*/
public function flush()
{
return $this->memcached->flush();
}
/**
* Get the expiration time of the key.
*
* @param int $seconds
* @return int
*/
protected function calculateExpiration($seconds)
{
return $this->toTimestamp($seconds);
}
/**
* Get the UNIX timestamp for the given number of seconds.
*
* @param int $seconds
* @return int
*/
protected function toTimestamp($seconds)
{
return $seconds > 0 ? $this->availableAt($seconds) : 0;
}
/**
* Get the underlying Memcached connection.
*
* @return \Memcached
*/
public function getMemcached()
{
return $this->memcached;
}
/**
* Get the cache key prefix.
*
* @return string
*/
public function getPrefix()
{
return $this->prefix;
}
/**
* Set the cache key prefix.
*
* @param string $prefix
* @return void
*/
public function setPrefix($prefix)
{
$this->prefix = $prefix;
}
}
@@ -0,0 +1,285 @@
<?php
namespace Illuminate\Cache;
use BadMethodCallException;
use Illuminate\Contracts\Cache\CanFlushLocks;
use Illuminate\Contracts\Cache\LockProvider;
use Illuminate\Contracts\Cache\Store;
class MemoizedStore implements CanFlushLocks, LockProvider, Store
{
/**
* The memoized cache values.
*
* @var array<string, mixed>
*/
protected $cache = [];
/**
* Create a new memoized cache instance.
*
* @param string $name
* @param \Illuminate\Cache\Repository $repository
*/
public function __construct(
protected $name,
protected $repository,
) {
//
}
/**
* Retrieve an item from the cache by key.
*
* @param string $key
* @return mixed
*/
public function get($key)
{
$prefixedKey = $this->prefix($key);
if (array_key_exists($prefixedKey, $this->cache)) {
return $this->cache[$prefixedKey];
}
return $this->cache[$prefixedKey] = $this->repository->get($key);
}
/**
* Retrieve multiple items from the cache by key.
*
* Items not found in the cache will have a null value.
*
* @return array
*/
public function many(array $keys)
{
[$memoized, $retrieved, $missing] = [[], [], []];
foreach ($keys as $key) {
$prefixedKey = $this->prefix($key);
if (array_key_exists($prefixedKey, $this->cache)) {
$memoized[$key] = $this->cache[$prefixedKey];
} else {
$missing[] = $key;
}
}
if ($missing !== []) {
$retrieved = tap($this->repository->many($missing), function ($values) {
foreach ($values as $key => $value) {
$this->cache[$this->prefix($key)] = $value;
}
});
}
$result = [];
foreach ($keys as $key) {
if (array_key_exists($key, $memoized)) {
$result[$key] = $memoized[$key];
} else {
$result[$key] = $retrieved[$key];
}
}
return $result;
}
/**
* Store an item in the cache for a given number of seconds.
*
* @param string $key
* @param mixed $value
* @param int $seconds
* @return bool
*/
public function put($key, $value, $seconds)
{
unset($this->cache[$this->prefix($key)]);
return $this->repository->put($key, $value, $seconds);
}
/**
* Store multiple items in the cache for a given number of seconds.
*
* @param int $seconds
* @return bool
*/
public function putMany(array $values, $seconds)
{
foreach ($values as $key => $value) {
unset($this->cache[$this->prefix($key)]);
}
return $this->repository->putMany($values, $seconds);
}
/**
* Increment the value of an item in the cache.
*
* @param string $key
* @param mixed $value
* @return int|bool
*/
public function increment($key, $value = 1)
{
unset($this->cache[$this->prefix($key)]);
return $this->repository->increment($key, $value);
}
/**
* Decrement the value of an item in the cache.
*
* @param string $key
* @param mixed $value
* @return int|bool
*/
public function decrement($key, $value = 1)
{
unset($this->cache[$this->prefix($key)]);
return $this->repository->decrement($key, $value);
}
/**
* Store an item in the cache indefinitely.
*
* @param string $key
* @param mixed $value
* @return bool
*/
public function forever($key, $value)
{
unset($this->cache[$this->prefix($key)]);
return $this->repository->forever($key, $value);
}
/**
* Get a lock instance.
*
* @param string $name
* @param int $seconds
* @param string|null $owner
* @return \Illuminate\Contracts\Cache\Lock
*
* @throws \BadMethodCallException
*/
public function lock($name, $seconds = 0, $owner = null)
{
if (! $this->repository->getStore() instanceof LockProvider) {
throw new BadMethodCallException('This cache store does not support locks.');
}
return $this->repository->getStore()->lock(...func_get_args());
}
/**
* Restore a lock instance using the owner identifier.
*
* @param string $name
* @param string $owner
* @return \Illuminate\Contracts\Cache\Lock
*
* @throws \BadMethodCallException
*/
public function restoreLock($name, $owner)
{
if (! $this->repository->getStore() instanceof LockProvider) {
throw new BadMethodCallException('This cache store does not support locks.');
}
return $this->repository->getStore()->restoreLock(...func_get_args());
}
/**
* Flush all locks managed by the store.
*
* @throws \BadMethodCallException
*/
public function flushLocks(): bool
{
$store = $this->repository->getStore();
if (! $store instanceof CanFlushLocks) {
throw new BadMethodCallException('This cache store does not support flushing locks.');
}
return $store->flushLocks();
}
/**
* Determine if the lock store is separate from the cache store.
*/
public function hasSeparateLockStore(): bool
{
$store = $this->repository->getStore();
return $store instanceof CanFlushLocks && $store->hasSeparateLockStore();
}
/**
* Adjust the expiration time of a cached item.
*
* @param string $key
* @param int $seconds
* @return bool
*/
public function touch($key, $seconds)
{
unset($this->cache[$this->prefix($key)]);
return $this->repository->touch($key, $seconds);
}
/**
* Remove an item from the cache.
*
* @param string $key
* @return bool
*/
public function forget($key)
{
unset($this->cache[$this->prefix($key)]);
return $this->repository->forget($key);
}
/**
* Remove all items from the cache.
*
* @return bool
*/
public function flush()
{
$this->cache = [];
return $this->repository->flush();
}
/**
* Get the cache key prefix.
*
* @return string
*/
public function getPrefix()
{
return $this->repository->getPrefix();
}
/**
* Prefix the given key.
*
* @param string $key
* @return string
*/
protected function prefix($key)
{
return $this->getPrefix().$key;
}
}
@@ -0,0 +1,56 @@
<?php
namespace Illuminate\Cache;
class NoLock extends Lock
{
/**
* Attempt to acquire the lock.
*
* @return bool
*/
public function acquire()
{
return true;
}
/**
* Release the lock.
*
* @return bool
*/
public function release()
{
return true;
}
/**
* Releases this lock in disregard of ownership.
*
* @return void
*/
public function forceRelease()
{
//
}
/**
* Determine if the lock is currently held by any process.
*
* @return bool
*/
public function isLocked(): bool
{
return false;
}
/**
* Returns the owner value written into the driver for this lock.
*
* @return mixed
*/
protected function getCurrentOwner()
{
return $this->owner;
}
}
+155
View File
@@ -0,0 +1,155 @@
<?php
namespace Illuminate\Cache;
use Illuminate\Contracts\Cache\CanFlushLocks;
use Illuminate\Contracts\Cache\LockProvider;
class NullStore extends TaggableStore implements CanFlushLocks, LockProvider
{
use RetrievesMultipleKeys;
/**
* Retrieve an item from the cache by key.
*
* @param string $key
* @return void
*/
public function get($key)
{
//
}
/**
* Store an item in the cache for a given number of seconds.
*
* @param string $key
* @param mixed $value
* @param int $seconds
* @return bool
*/
public function put($key, $value, $seconds)
{
return false;
}
/**
* Increment the value of an item in the cache.
*
* @param string $key
* @param mixed $value
* @return false
*/
public function increment($key, $value = 1)
{
return false;
}
/**
* Decrement the value of an item in the cache.
*
* @param string $key
* @param mixed $value
* @return false
*/
public function decrement($key, $value = 1)
{
return false;
}
/**
* Store an item in the cache indefinitely.
*
* @param string $key
* @param mixed $value
* @return bool
*/
public function forever($key, $value)
{
return false;
}
/**
* Get a lock instance.
*
* @param string $name
* @param int $seconds
* @param string|null $owner
* @return \Illuminate\Contracts\Cache\Lock
*/
public function lock($name, $seconds = 0, $owner = null)
{
return new NoLock($name, $seconds, $owner);
}
/**
* Restore a lock instance using the owner identifier.
*
* @param string $name
* @param string $owner
* @return \Illuminate\Contracts\Cache\Lock
*/
public function restoreLock($name, $owner)
{
return $this->lock($name, 0, $owner);
}
/**
* Flush all locks managed by the store.
*/
public function flushLocks(): bool
{
return true;
}
/**
* Determine if the lock store is separate from the cache store.
*/
public function hasSeparateLockStore(): bool
{
return false;
}
/**
* Adjust the expiration time of a cached item.
*
* @param string $key
* @param int $seconds
* @return bool
*/
public function touch($key, $seconds)
{
return false;
}
/**
* Remove an item from the cache.
*
* @param string $key
* @return bool
*/
public function forget($key)
{
return true;
}
/**
* Remove all items from the cache.
*
* @return bool
*/
public function flush()
{
return true;
}
/**
* Get the cache key prefix.
*
* @return string
*/
public function getPrefix()
{
return '';
}
}
@@ -0,0 +1,34 @@
<?php
namespace Illuminate\Cache;
use Illuminate\Redis\Connections\PhpRedisConnection;
class PhpRedisLock extends RedisLock
{
/**
* Create a new phpredis lock instance.
*
* @param \Illuminate\Redis\Connections\PhpRedisConnection $redis
* @param string $name
* @param int $seconds
* @param string|null $owner
*/
public function __construct(PhpRedisConnection $redis, string $name, int $seconds, ?string $owner = null)
{
parent::__construct($redis, $name, $seconds, $owner);
}
/**
* {@inheritDoc}
*/
public function release()
{
return (bool) $this->redis->eval(
LuaScripts::releaseLock(),
1,
$this->name,
...$this->redis->pack([$this->owner])
);
}
}
@@ -0,0 +1,322 @@
<?php
namespace Illuminate\Cache;
use Closure;
use Illuminate\Contracts\Cache\Repository as Cache;
use Illuminate\Redis\Connections\PhpRedisConnection;
use Illuminate\Support\Collection;
use Illuminate\Support\InteractsWithTime;
use function Illuminate\Support\enum_value;
class RateLimiter
{
use InteractsWithTime;
/**
* The cache store implementation.
*
* @var \Illuminate\Contracts\Cache\Repository
*/
protected $cache;
/**
* The configured limit object resolvers.
*
* @var array
*/
protected $limiters = [];
/**
* Create a new rate limiter instance.
*
* @param \Illuminate\Contracts\Cache\Repository $cache
*/
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
/**
* Register a named rate limiter configuration.
*
* @param \UnitEnum|string $name
* @param \Closure $callback
* @return $this
*/
public function for($name, Closure $callback)
{
$resolvedName = $this->resolveLimiterName($name);
$this->limiters[$resolvedName] = $callback;
return $this;
}
/**
* Get the given named rate limiter.
*
* @param \UnitEnum|string $name
* @return \Closure|null
*/
public function limiter($name)
{
$resolvedName = $this->resolveLimiterName($name);
$limiter = $this->limiters[$resolvedName] ?? null;
if (! is_callable($limiter)) {
return;
}
return function (...$args) use ($limiter) {
$result = $limiter(...$args);
if (! is_array($result)) {
return $result;
}
$duplicates = (new Collection($result))->duplicates('key');
if ($duplicates->isEmpty()) {
return $result;
}
foreach ($result as $limit) {
if ($duplicates->contains($limit->key)) {
$limit->key = $limit->fallbackKey();
}
}
return $result;
};
}
/**
* Attempts to execute a callback if it's not limited.
*
* @param string $key
* @param int $maxAttempts
* @param \Closure $callback
* @param \DateTimeInterface|\DateInterval|int $decaySeconds
* @return mixed
*/
public function attempt($key, $maxAttempts, Closure $callback, $decaySeconds = 60)
{
if ($this->tooManyAttempts($key, $maxAttempts)) {
return false;
}
if (is_null($result = $callback())) {
$result = true;
}
return tap($result, function () use ($key, $decaySeconds) {
$this->hit($key, $decaySeconds);
});
}
/**
* Determine if the given key has been "accessed" too many times.
*
* @param string $key
* @param int $maxAttempts
* @return bool
*/
public function tooManyAttempts($key, $maxAttempts)
{
if ($this->attempts($key) >= $maxAttempts) {
if ($this->cache->has($this->cleanRateLimiterKey($key).':timer')) {
return true;
}
$this->resetAttempts($key);
}
return false;
}
/**
* Increment (by 1) the counter for a given key for a given decay time.
*
* @param string $key
* @param \DateTimeInterface|\DateInterval|int $decaySeconds
* @return int
*/
public function hit($key, $decaySeconds = 60)
{
return $this->increment($key, $decaySeconds);
}
/**
* Increment the counter for a given key for a given decay time by a given amount.
*
* @param string $key
* @param \DateTimeInterface|\DateInterval|int $decaySeconds
* @param int $amount
* @return int
*/
public function increment($key, $decaySeconds = 60, $amount = 1)
{
$key = $this->cleanRateLimiterKey($key);
$this->cache->add(
$key.':timer', $this->availableAt($decaySeconds), $decaySeconds
);
$added = $this->withoutSerializationOrCompression(
fn () => $this->cache->add($key, 0, $decaySeconds)
);
$hits = (int) $this->cache->increment($key, $amount);
if (! $added && $hits == $amount) {
$this->withoutSerializationOrCompression(
fn () => $this->cache->put($key, $amount, $decaySeconds)
);
}
return $hits;
}
/**
* Decrement the counter for a given key for a given decay time by a given amount.
*
* @param string $key
* @param \DateTimeInterface|\DateInterval|int $decaySeconds
* @param int $amount
* @return int
*/
public function decrement($key, $decaySeconds = 60, $amount = 1)
{
return $this->increment($key, $decaySeconds, $amount * -1);
}
/**
* Get the number of attempts for the given key.
*
* @param string $key
* @return mixed
*/
public function attempts($key)
{
$key = $this->cleanRateLimiterKey($key);
return $this->withoutSerializationOrCompression(fn () => $this->cache->get($key, 0));
}
/**
* Reset the number of attempts for the given key.
*
* @param string $key
* @return bool
*/
public function resetAttempts($key)
{
$key = $this->cleanRateLimiterKey($key);
return $this->cache->forget($key);
}
/**
* Get the number of retries left for the given key.
*
* @param string $key
* @param int $maxAttempts
* @return int
*/
public function remaining($key, $maxAttempts)
{
$key = $this->cleanRateLimiterKey($key);
$attempts = $this->attempts($key);
return max(0, $maxAttempts - $attempts);
}
/**
* Get the number of retries left for the given key.
*
* @param string $key
* @param int $maxAttempts
* @return int
*/
public function retriesLeft($key, $maxAttempts)
{
return $this->remaining($key, $maxAttempts);
}
/**
* Clear the hits and lockout timer for the given key.
*
* @param string $key
* @return void
*/
public function clear($key)
{
$key = $this->cleanRateLimiterKey($key);
$this->resetAttempts($key);
$this->cache->forget($key.':timer');
}
/**
* Get the number of seconds until the "key" is accessible again.
*
* @param string $key
* @return int
*/
public function availableIn($key)
{
$key = $this->cleanRateLimiterKey($key);
return max(0, $this->cache->get($key.':timer') - $this->currentTime());
}
/**
* Clean the rate limiter key from unicode characters.
*
* @param string $key
* @return string
*/
public function cleanRateLimiterKey($key)
{
return preg_replace('/&([a-z])[a-z]+;/i', '$1', htmlentities($key));
}
/**
* Execute the given callback without serialization or compression when applicable.
*
* @param callable $callback
* @return mixed
*/
protected function withoutSerializationOrCompression(callable $callback)
{
$store = $this->cache->getStore();
if (! $store instanceof RedisStore) {
return $callback();
}
$connection = $store->connection();
if (! $connection instanceof PhpRedisConnection) {
return $callback();
}
return $connection->withoutSerializationOrCompression($callback);
}
/**
* Resolve the rate limiter name.
*
* @param \UnitEnum|string $name
* @return string
*/
private function resolveLimiterName($name): string
{
return (string) enum_value($name);
}
}
@@ -0,0 +1,17 @@
<?php
namespace Illuminate\Cache\RateLimiting;
class GlobalLimit extends Limit
{
/**
* Create a new limit instance.
*
* @param int $maxAttempts
* @param int $decaySeconds
*/
public function __construct(int $maxAttempts, int $decaySeconds = 60)
{
parent::__construct('', $maxAttempts, $decaySeconds);
}
}
@@ -0,0 +1,176 @@
<?php
namespace Illuminate\Cache\RateLimiting;
class Limit
{
/**
* The rate limit signature key.
*
* @var mixed
*/
public $key;
/**
* The maximum number of attempts allowed within the given number of seconds.
*
* @var int
*/
public $maxAttempts;
/**
* The number of seconds until the rate limit is reset.
*
* @var int
*/
public $decaySeconds;
/**
* The after callback used to determine if the limiter should be hit.
*
* @var ?callable
*/
public $afterCallback = null;
/**
* The response generator callback.
*
* @var callable
*/
public $responseCallback;
/**
* Create a new limit instance.
*
* @param mixed $key
* @param int $maxAttempts
* @param int $decaySeconds
*/
public function __construct($key = '', int $maxAttempts = 60, int $decaySeconds = 60)
{
$this->key = $key;
$this->maxAttempts = $maxAttempts;
$this->decaySeconds = $decaySeconds;
}
/**
* Create a new rate limit.
*
* @param int $maxAttempts
* @param int $decaySeconds
* @return static
*/
public static function perSecond($maxAttempts, $decaySeconds = 1)
{
return new static('', $maxAttempts, $decaySeconds);
}
/**
* Create a new rate limit.
*
* @param int $maxAttempts
* @param int $decayMinutes
* @return static
*/
public static function perMinute($maxAttempts, $decayMinutes = 1)
{
return new static('', $maxAttempts, 60 * $decayMinutes);
}
/**
* Create a new rate limit using minutes as decay time.
*
* @param int $decayMinutes
* @param int $maxAttempts
* @return static
*/
public static function perMinutes($decayMinutes, $maxAttempts)
{
return new static('', $maxAttempts, 60 * $decayMinutes);
}
/**
* Create a new rate limit using hours as decay time.
*
* @param int $maxAttempts
* @param int $decayHours
* @return static
*/
public static function perHour($maxAttempts, $decayHours = 1)
{
return new static('', $maxAttempts, 60 * 60 * $decayHours);
}
/**
* Create a new rate limit using days as decay time.
*
* @param int $maxAttempts
* @param int $decayDays
* @return static
*/
public static function perDay($maxAttempts, $decayDays = 1)
{
return new static('', $maxAttempts, 60 * 60 * 24 * $decayDays);
}
/**
* Create a new unlimited rate limit.
*
* @return \Illuminate\Cache\RateLimiting\Unlimited
*/
public static function none()
{
return new Unlimited;
}
/**
* Set the key of the rate limit.
*
* @param mixed $key
* @return $this
*/
public function by($key)
{
$this->key = $key;
return $this;
}
/**
* Set the callback to determine if the limiter should be hit.
*
* @param callable $callback
* @return $this
*/
public function after($callback)
{
$this->afterCallback = $callback;
return $this;
}
/**
* Set the callback that should generate the response when the limit is exceeded.
*
* @param callable $callback
* @return $this
*/
public function response(callable $callback)
{
$this->responseCallback = $callback;
return $this;
}
/**
* Get a potential fallback key for the limit.
*
* @return string
*/
public function fallbackKey()
{
$prefix = $this->key ? "{$this->key}:" : '';
return "{$prefix}attempts:{$this->maxAttempts}:decay:{$this->decaySeconds}";
}
}
@@ -0,0 +1,14 @@
<?php
namespace Illuminate\Cache\RateLimiting;
class Unlimited extends GlobalLimit
{
/**
* Create a new limit instance.
*/
public function __construct()
{
parent::__construct(PHP_INT_MAX);
}
}
@@ -0,0 +1,82 @@
<?php
namespace Illuminate\Cache;
class RedisLock extends Lock
{
/**
* The Redis factory implementation.
*
* @var \Illuminate\Redis\Connections\Connection
*/
protected $redis;
/**
* Create a new lock instance.
*
* @param \Illuminate\Redis\Connections\Connection $redis
* @param string $name
* @param int $seconds
* @param string|null $owner
*/
public function __construct($redis, $name, $seconds, $owner = null)
{
parent::__construct($name, $seconds, $owner);
$this->redis = $redis;
}
/**
* Attempt to acquire the lock.
*
* @return bool
*/
public function acquire()
{
if ($this->seconds > 0) {
return $this->redis->set($this->name, $this->owner, 'EX', $this->seconds, 'NX') == true;
}
return $this->redis->setnx($this->name, $this->owner) === 1;
}
/**
* Release the lock.
*
* @return bool
*/
public function release()
{
return (bool) $this->redis->eval(LuaScripts::releaseLock(), 1, $this->name, $this->owner);
}
/**
* Releases this lock in disregard of ownership.
*
* @return void
*/
public function forceRelease()
{
$this->redis->del($this->name);
}
/**
* Returns the owner value written into the driver for this lock.
*
* @return string|null
*/
protected function getCurrentOwner()
{
return $this->redis->get($this->name);
}
/**
* Get the name of the Redis connection being used to manage the lock.
*
* @return string
*/
public function getConnectionName()
{
return $this->redis->getName();
}
}
+581
View File
@@ -0,0 +1,581 @@
<?php
namespace Illuminate\Cache;
use Illuminate\Contracts\Cache\CanFlushLocks;
use Illuminate\Contracts\Cache\LockProvider;
use Illuminate\Contracts\Redis\Factory as Redis;
use Illuminate\Redis\Connections\PhpRedisClusterConnection;
use Illuminate\Redis\Connections\PhpRedisConnection;
use Illuminate\Redis\Connections\PredisClusterConnection;
use Illuminate\Redis\Connections\PredisConnection;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use RuntimeException;
class RedisStore extends TaggableStore implements CanFlushLocks, LockProvider
{
use RetrievesMultipleKeys {
many as private manyAlias;
putMany as private putManyAlias;
}
/**
* The Redis factory implementation.
*
* @var \Illuminate\Contracts\Redis\Factory
*/
protected $redis;
/**
* A string that should be prepended to keys.
*
* @var string
*/
protected $prefix;
/**
* The Redis connection instance that should be used to manage locks.
*
* @var string
*/
protected $connection;
/**
* The name of the connection that should be used for locks.
*
* @var string
*/
protected $lockConnection;
/**
* The classes that should be allowed during unserialization.
*
* @var array|bool|null
*/
protected $serializableClasses;
/**
* Create a new Redis store.
*
* @param \Illuminate\Contracts\Redis\Factory $redis
* @param string $prefix
* @param string $connection
* @param array|bool|null $serializableClasses
*/
public function __construct(Redis $redis, $prefix = '', $connection = 'default', $serializableClasses = null)
{
$this->redis = $redis;
$this->setPrefix($prefix);
$this->setConnection($connection);
$this->serializableClasses = $serializableClasses;
}
/**
* Retrieve an item from the cache by key.
*
* @param string $key
* @return mixed
*/
public function get($key)
{
$connection = $this->connection();
$value = $connection->get($this->prefix.$key);
return ! is_null($value) ? $this->connectionAwareUnserialize($value, $connection) : null;
}
/**
* Retrieve multiple items from the cache by key.
*
* Items not found in the cache will have a null value.
*
* @param array $keys
* @return array
*/
public function many(array $keys)
{
if ($keys === []) {
return [];
}
$results = [];
$connection = $this->connection();
// PredisClusterConnection does not support reading multiple values if the keys hash differently...
if ($connection instanceof PredisClusterConnection) {
return $this->manyAlias($keys);
}
$values = $connection->mget(array_map(function ($key) {
return $this->prefix.$key;
}, $keys));
foreach ($values as $index => $value) {
$results[$keys[$index]] = ! is_null($value) ? $this->connectionAwareUnserialize($value, $connection) : null;
}
return $results;
}
/**
* Store an item in the cache for a given number of seconds.
*
* @param string $key
* @param mixed $value
* @param int $seconds
* @return bool
*/
public function put($key, $value, $seconds)
{
$connection = $this->connection();
return (bool) $connection->setex(
$this->prefix.$key, (int) max(1, $seconds), $this->connectionAwareSerialize($value, $connection)
);
}
/**
* Store multiple items in the cache for a given number of seconds.
*
* @param array $values
* @param int $seconds
* @return bool
*/
public function putMany(array $values, $seconds)
{
$connection = $this->connection();
// Cluster connections do not support writing multiple values if the keys hash differently...
if ($connection instanceof PhpRedisClusterConnection ||
$connection instanceof PredisClusterConnection) {
return $this->putManyAlias($values, $seconds);
}
$serializedValues = [];
foreach ($values as $key => $value) {
$serializedValues[$this->prefix.$key] = $this->connectionAwareSerialize($value, $connection);
}
$connection->multi();
$manyResult = null;
foreach ($serializedValues as $key => $value) {
$result = (bool) $connection->setex(
$key, (int) max(1, $seconds), $value
);
$manyResult = is_null($manyResult) ? $result : $result && $manyResult;
}
$connection->exec();
return $manyResult ?: false;
}
/**
* Store an item in the cache if the key doesn't exist.
*
* @param string $key
* @param mixed $value
* @param int $seconds
* @return bool
*/
public function add($key, $value, $seconds)
{
$connection = $this->connection();
return (bool) $connection->eval(
LuaScripts::add(), 1, $this->prefix.$key, $this->pack($value, $connection), (int) max(1, $seconds)
);
}
/**
* Increment the value of an item in the cache.
*
* @param string $key
* @param mixed $value
* @return int
*/
public function increment($key, $value = 1)
{
return $this->connection()->incrby($this->prefix.$key, $value);
}
/**
* Decrement the value of an item in the cache.
*
* @param string $key
* @param mixed $value
* @return int
*/
public function decrement($key, $value = 1)
{
return $this->connection()->decrby($this->prefix.$key, $value);
}
/**
* Store an item in the cache indefinitely.
*
* @param string $key
* @param mixed $value
* @return bool
*/
public function forever($key, $value)
{
$connection = $this->connection();
return (bool) $connection->set($this->prefix.$key, $this->connectionAwareSerialize($value, $connection));
}
/**
* Get a lock instance.
*
* @param string $name
* @param int $seconds
* @param string|null $owner
* @return \Illuminate\Contracts\Cache\Lock
*/
public function lock($name, $seconds = 0, $owner = null)
{
$lockName = $this->prefix.$name;
$lockConnection = $this->lockConnection();
if ($lockConnection instanceof PhpRedisConnection) {
return new PhpRedisLock($lockConnection, $lockName, $seconds, $owner);
}
return new RedisLock($lockConnection, $lockName, $seconds, $owner);
}
/**
* Restore a lock instance using the owner identifier.
*
* @param string $name
* @param string $owner
* @return \Illuminate\Contracts\Cache\Lock
*/
public function restoreLock($name, $owner)
{
return $this->lock($name, 0, $owner);
}
/**
* Adjust the expiration time of a cached item.
*
* @param string $key
* @param int $seconds
* @return bool
*/
public function touch($key, $seconds)
{
return (bool) $this->connection()->expire($this->getPrefix().$key, (int) max(1, $seconds));
}
/**
* Remove an item from the cache.
*
* @param string $key
* @return bool
*/
public function forget($key)
{
return (bool) $this->connection()->del($this->prefix.$key);
}
/**
* Remove all items from the cache.
*
* @return bool
*/
public function flush()
{
$this->connection()->flushdb();
return true;
}
/**
* Remove all locks from the store.
*
* @return bool
*
* @throws \RuntimeException
*/
public function flushLocks(): bool
{
if (! $this->hasSeparateLockStore()) {
throw new RuntimeException('Flushing locks is only supported when the lock store is separate from the cache store.');
}
$this->lockConnection()->flushdb();
return true;
}
/**
* Remove all expired tag set entries.
*
* @return void
*/
public function flushStaleTags()
{
foreach ($this->currentTags()->chunk(1000) as $tags) {
$this->tags($tags->all())->flushStale();
}
}
/**
* Begin executing a new tags operation.
*
* @param mixed $names
* @return \Illuminate\Cache\RedisTaggedCache
*/
public function tags($names)
{
return new RedisTaggedCache(
$this, new RedisTagSet($this, is_array($names) ? $names : func_get_args())
);
}
/**
* Get a collection of all of the cache tags currently being used.
*
* @param int $chunkSize
* @return \Illuminate\Support\LazyCollection
*/
protected function currentTags($chunkSize = 1000)
{
$connection = $this->connection();
// Connections can have a global prefix...
$connectionPrefix = match (true) {
$connection instanceof PhpRedisConnection => $connection->_prefix(''),
$connection instanceof PredisConnection => $connection->getOptions()->prefix ?: '',
default => '',
};
$defaultCursorValue = match (true) {
$connection instanceof PhpRedisConnection && version_compare(phpversion('redis'), '6.1.0', '>=') => null,
default => '0',
};
$prefix = $connectionPrefix.$this->getPrefix();
return (new LazyCollection(function () use ($connection, $chunkSize, $prefix, $defaultCursorValue) {
$cursor = $defaultCursorValue;
do {
$scanResult = $connection->scan(
$cursor,
['match' => $prefix.'tag:*:entries', 'count' => $chunkSize]
);
if (! is_array($scanResult)) {
break;
}
[$cursor, $tagsChunk] = $scanResult;
if (! is_array($tagsChunk)) {
break;
}
$tagsChunk = array_unique($tagsChunk);
if (empty($tagsChunk)) {
continue;
}
foreach ($tagsChunk as $tag) {
yield $tag;
}
} while (((string) $cursor) !== $defaultCursorValue);
}))->map(fn (string $tagKey) => Str::match('/^'.preg_quote($prefix, '/').'tag:(.*):entries$/', $tagKey));
}
/**
* Get the Redis connection instance.
*
* @return \Illuminate\Redis\Connections\Connection
*/
public function connection()
{
return $this->redis->connection($this->connection);
}
/**
* Get the Redis connection instance that should be used to manage locks.
*
* @return \Illuminate\Redis\Connections\Connection
*/
public function lockConnection()
{
return $this->redis->connection($this->lockConnection ?? $this->connection);
}
/**
* Specify the name of the connection that should be used to store data.
*
* @param string $connection
* @return void
*/
public function setConnection($connection)
{
$this->connection = $connection;
}
/**
* Specify the name of the connection that should be used to manage locks.
*
* @param string $connection
* @return $this
*/
public function setLockConnection($connection)
{
$this->lockConnection = $connection;
return $this;
}
/**
* Get the Redis database instance.
*
* @return \Illuminate\Contracts\Redis\Factory
*/
public function getRedis()
{
return $this->redis;
}
/**
* Get the cache key prefix.
*
* @return string
*/
public function getPrefix()
{
return $this->prefix;
}
/**
* Set the cache key prefix.
*
* @param string $prefix
* @return void
*/
public function setPrefix($prefix)
{
$this->prefix = $prefix;
}
/**
* Prepare a value to be used with the Redis cache store when used by eval scripts.
*
* @param mixed $value
* @param \Illuminate\Redis\Connections\Connection $connection
* @return mixed
*/
protected function pack($value, $connection)
{
if ($connection instanceof PhpRedisConnection) {
if ($connection->serialized()) {
return $connection->pack([$value])[0];
}
if ($connection->compressed()) {
return $connection->pack([$this->serialize($value)])[0];
}
}
return $this->serialize($value);
}
/**
* Serialize the value.
*
* @param mixed $value
* @return mixed
*/
protected function serialize($value)
{
return $this->shouldBeStoredWithoutSerialization($value) ? $value : serialize($value);
}
/**
* Determine if the given value should be stored as plain value.
*
* @param mixed $value
* @return bool
*/
protected function shouldBeStoredWithoutSerialization($value): bool
{
return is_numeric($value) && is_finite($value);
}
/**
* Unserialize the value.
*
* @param mixed $value
* @return mixed
*/
protected function unserialize($value)
{
if (is_numeric($value)) {
return $value;
}
if ($this->serializableClasses !== null) {
return unserialize($value, ['allowed_classes' => $this->serializableClasses]);
}
return unserialize($value);
}
/**
* Handle connection specific considerations when a value needs to be serialized.
*
* @param mixed $value
* @param \Illuminate\Redis\Connections\Connection $connection
* @return mixed
*/
protected function connectionAwareSerialize($value, $connection)
{
if ($connection instanceof PhpRedisConnection && $connection->serialized()) {
return $value;
}
return $this->serialize($value);
}
/**
* Handle connection specific considerations when a value needs to be unserialized.
*
* @param mixed $value
* @param \Illuminate\Redis\Connections\Connection $connection
* @return mixed
*/
protected function connectionAwareUnserialize($value, $connection)
{
if ($connection instanceof PhpRedisConnection && $connection->serialized()) {
return $value;
}
return $this->unserialize($value);
}
/**
* Determine if the lock store is separate from the cache store.
*
* @return bool
*/
public function hasSeparateLockStore(): bool
{
return $this->lockConnection !== $this->connection;
}
}
@@ -0,0 +1,148 @@
<?php
namespace Illuminate\Cache;
use Illuminate\Redis\Connections\PhpRedisConnection;
use Illuminate\Support\Carbon;
use Illuminate\Support\LazyCollection;
class RedisTagSet extends TagSet
{
/**
* Add a reference entry to the tag set's underlying sorted set.
*
* @param string $key
* @param int|null $ttl
* @param string|null $updateWhen
* @return void
*/
public function addEntry(string $key, ?int $ttl = null, $updateWhen = null)
{
$ttl = is_null($ttl) ? -1 : Carbon::now()->addSeconds($ttl)->getTimestamp();
foreach ($this->tagIds() as $tagKey) {
if ($updateWhen) {
$this->store->connection()->zadd($this->store->getPrefix().$tagKey, $updateWhen, $ttl, $key);
} else {
$this->store->connection()->zadd($this->store->getPrefix().$tagKey, $ttl, $key);
}
}
}
/**
* Get all of the cache entry keys for the tag set.
*
* @return \Illuminate\Support\LazyCollection
*/
public function entries()
{
$connection = $this->store->connection();
$defaultCursorValue = match (true) {
$connection instanceof PhpRedisConnection && version_compare(phpversion('redis'), '6.1.0', '>=') => null,
default => '0',
};
return new LazyCollection(function () use ($connection, $defaultCursorValue) {
foreach ($this->tagIds() as $tagKey) {
$cursor = $defaultCursorValue;
do {
$results = $connection->zscan(
$this->store->getPrefix().$tagKey,
$cursor,
['match' => '*', 'count' => 1000]
);
if (! is_array($results)) {
break;
}
[$cursor, $entries] = $results;
if (! is_array($entries)) {
break;
}
$entries = array_unique(array_keys($entries));
if ($entries === []) {
continue;
}
foreach ($entries as $entry) {
yield $entry;
}
} while (((string) $cursor) !== $defaultCursorValue);
}
});
}
/**
* Remove the stale entries from the tag set.
*
* @return void
*/
public function flushStaleEntries()
{
$flushStaleEntries = function ($pipe) {
foreach ($this->tagIds() as $tagKey) {
$pipe->zremrangebyscore($this->store->getPrefix().$tagKey, 0, Carbon::now()->getTimestamp());
}
};
$connection = $this->store->connection();
if ($connection instanceof PhpRedisConnection) {
$flushStaleEntries($connection);
} else {
$connection->pipeline($flushStaleEntries);
}
}
/**
* Flush the tag from the cache.
*
* @param string $name
* @return string
*/
public function flushTag($name)
{
return $this->resetTag($name);
}
/**
* Reset the tag and return the new tag identifier.
*
* @param string $name
* @return string
*/
public function resetTag($name)
{
$this->store->forget($this->tagKey($name));
return $this->tagId($name);
}
/**
* Get the unique tag identifier for a given tag.
*
* @param string $name
* @return string
*/
public function tagId($name)
{
return "tag:{$name}:entries";
}
/**
* Get the tag identifier key for a given tag.
*
* @param string $name
* @return string
*/
public function tagKey($name)
{
return "tag:{$name}:entries";
}
}
@@ -0,0 +1,232 @@
<?php
namespace Illuminate\Cache;
use Illuminate\Cache\Events\CacheFlushed;
use Illuminate\Cache\Events\CacheFlushing;
use Illuminate\Redis\Connections\PhpRedisClusterConnection;
use Illuminate\Redis\Connections\PhpRedisConnection;
use Illuminate\Redis\Connections\PredisClusterConnection;
use Illuminate\Redis\Connections\PredisConnection;
use function Illuminate\Support\enum_value;
class RedisTaggedCache extends TaggedCache
{
/**
* Store an item in the cache if the key does not exist.
*
* @param \UnitEnum|string $key
* @param mixed $value
* @param \DateTimeInterface|\DateInterval|int|null $ttl
* @return bool
*/
public function add($key, $value, $ttl = null)
{
$key = enum_value($key);
$seconds = null;
if ($ttl !== null) {
$seconds = $this->getSeconds($ttl);
if ($seconds > 0) {
$this->tags->addEntry(
$this->itemKey($key),
$seconds
);
}
}
return parent::add($key, $value, $ttl);
}
/**
* Store an item in the cache.
*
* @param \UnitEnum|string $key
* @param mixed $value
* @param \DateTimeInterface|\DateInterval|int|null $ttl
* @return bool
*/
public function put($key, $value, $ttl = null)
{
$key = enum_value($key);
if (is_null($ttl)) {
return $this->forever($key, $value);
}
$seconds = $this->getSeconds($ttl);
if ($seconds > 0) {
$this->tags->addEntry(
$this->itemKey($key),
$seconds
);
}
return parent::put($key, $value, $ttl);
}
/**
* Increment the value of an item in the cache.
*
* @param \UnitEnum|string $key
* @param mixed $value
* @return int|bool
*/
public function increment($key, $value = 1)
{
$key = enum_value($key);
$this->tags->addEntry($this->itemKey($key), updateWhen: 'NX');
return parent::increment($key, $value);
}
/**
* Decrement the value of an item in the cache.
*
* @param \UnitEnum|string $key
* @param mixed $value
* @return int|bool
*/
public function decrement($key, $value = 1)
{
$this->tags->addEntry($this->itemKey($key), updateWhen: 'NX');
return parent::decrement($key, $value);
}
/**
* Store an item in the cache indefinitely.
*
* @param \UnitEnum|string $key
* @param mixed $value
* @return bool
*/
public function forever($key, $value)
{
$key = enum_value($key);
$this->tags->addEntry($this->itemKey($key));
return parent::forever($key, $value);
}
/**
* Remove all items from the cache.
*
* @return bool
*/
public function flush()
{
$connection = $this->store->connection();
if ($connection instanceof PredisClusterConnection ||
$connection instanceof PhpRedisClusterConnection) {
return $this->flushClusteredConnection();
}
$this->event(new CacheFlushing($this->getName()));
$redisPrefix = match (true) {
$connection instanceof PhpRedisConnection => $connection->client()->getOption(\Redis::OPT_PREFIX),
$connection instanceof PredisConnection => $connection->client()->getOptions()->prefix,
};
$cachePrefix = $redisPrefix.$this->store->getPrefix();
$cacheTags = [];
foreach ($this->tags->getNames() as $name) {
$cacheTags[] = $cachePrefix.$this->tags->tagId($name);
}
$script = <<<'LUA'
local prefix = table.remove(ARGV, 1)
for i, key in ipairs(KEYS) do
redis.call('DEL', key)
for j, arg in ipairs(ARGV) do
local zkey = string.gsub(key, prefix, "")
redis.call('ZREM', arg, zkey)
end
end
LUA;
$entries = $this->tags->entries()
->map(fn (string $key) => $this->store->getPrefix().$key)
->chunk(1000);
foreach ($entries as $keysToBeDeleted) {
$connection->eval(
$script,
count($keysToBeDeleted),
...$keysToBeDeleted,
...[str_replace('-', '%-', $cachePrefix), ...$cacheTags]
);
}
$this->event(new CacheFlushed($this->getName()));
return true;
}
/**
* Remove all items from the cache.
*
* @return bool
*/
protected function flushClusteredConnection()
{
$this->event(new CacheFlushing($this->getName()));
$this->flushValues();
$this->tags->flush();
$this->event(new CacheFlushed($this->getName()));
return true;
}
/**
* Flush the individual cache entries for the tags.
*
* @return void
*/
protected function flushValues()
{
$entries = $this->tags->entries()
->map(fn (string $key) => $this->store->getPrefix().$key)
->chunk(1000);
$connection = $this->store->connection();
foreach ($entries as $cacheKeys) {
if ($connection instanceof PredisClusterConnection) {
$connection->pipeline(function ($connection) use ($cacheKeys) {
foreach ($cacheKeys as $cacheKey) {
$connection->del($cacheKey);
}
});
} else {
$connection->del(...$cacheKeys);
}
}
}
/**
* Remove all stale reference entries from the tag set.
*
* @return bool
*/
public function flushStale()
{
$this->tags->flushStaleEntries();
return true;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,52 @@
<?php
namespace Illuminate\Cache;
use Illuminate\Support\Collection;
trait RetrievesMultipleKeys
{
/**
* Retrieve multiple items from the cache by key.
*
* Items not found in the cache will have a null value.
*
* @param array $keys
* @return array
*/
public function many(array $keys)
{
$return = [];
$keys = (new Collection($keys))
->mapWithKeys(fn ($value, $key) => [is_string($key) ? $key : $value => is_string($key) ? $value : null])
->all();
foreach ($keys as $key => $default) {
/** @phpstan-ignore arguments.count (some clients don't accept a default) */
$return[$key] = $this->get($key, $default);
}
return $return;
}
/**
* Store multiple items in the cache for a given number of seconds.
*
* @param array $values
* @param int $seconds
* @return bool
*/
public function putMany(array $values, $seconds)
{
$manyResult = null;
foreach ($values as $key => $value) {
$result = $this->put($key, $value, $seconds);
$manyResult = is_null($manyResult) ? $result : $result && $manyResult;
}
return $manyResult ?: false;
}
}
@@ -0,0 +1,226 @@
<?php
namespace Illuminate\Cache;
use Illuminate\Contracts\Cache\Store;
use Illuminate\Support\Carbon;
use Illuminate\Support\InteractsWithTime;
class SessionStore implements Store
{
use InteractsWithTime, RetrievesMultipleKeys;
/**
* The key for cache items.
*
* @var string
*/
public $key;
/**
* The session instance.
*
* @var \Illuminate\Contracts\Session\Session
*/
public $session;
/**
* Create a new session cache store.
*
* @param \Illuminate\Contracts\Session\Session $session
* @param string $key
*/
public function __construct($session, $key = '_cache')
{
$this->key = $key;
$this->session = $session;
}
/**
* Get all of the cached values and their expiration times.
*
* @return array<string, array{value: mixed, expiresAt: float}>
*/
public function all()
{
return $this->session->get($this->key, []);
}
/**
* Retrieve an item from the cache by key.
*
* @param string $key
* @return mixed
*/
public function get($key)
{
if (! $this->session->exists($this->itemKey($key))) {
return;
}
$item = $this->session->get($this->itemKey($key));
$expiresAt = $item['expiresAt'] ?? 0;
if ($this->isExpired($expiresAt)) {
$this->forget($key);
return;
}
return $item['value'];
}
/**
* Determine if the given expiration time is expired.
*
* @param int|float $expiresAt
* @return bool
*/
protected function isExpired($expiresAt)
{
return $expiresAt !== 0 && (Carbon::now()->getPreciseTimestamp(3) / 1000) >= $expiresAt;
}
/**
* Store an item in the cache for a given number of seconds.
*
* @param string $key
* @param mixed $value
* @param int $seconds
* @return bool
*/
public function put($key, $value, $seconds)
{
$this->session->put($this->itemKey($key), [
'value' => $value,
'expiresAt' => $this->toTimestamp($seconds),
]);
return true;
}
/**
* Get the UNIX timestamp, with milliseconds, for the given number of seconds in the future.
*
* @param int $seconds
* @return float
*/
protected function toTimestamp($seconds)
{
return $seconds > 0 ? (Carbon::now()->getPreciseTimestamp(3) / 1000) + $seconds : 0;
}
/**
* Increment the value of an item in the cache.
*
* @param string $key
* @param mixed $value
* @return int
*/
public function increment($key, $value = 1)
{
if (! is_null($existing = $this->get($key))) {
return tap(((int) $existing) + $value, function ($incremented) use ($key) {
$this->session->put($this->itemKey("{$key}.value"), $incremented);
});
}
$this->forever($key, $value);
return $value;
}
/**
* Decrement the value of an item in the cache.
*
* @param string $key
* @param mixed $value
* @return int
*/
public function decrement($key, $value = 1)
{
return $this->increment($key, $value * -1);
}
/**
* Store an item in the cache indefinitely.
*
* @param string $key
* @param mixed $value
* @return bool
*/
public function forever($key, $value)
{
return $this->put($key, $value, 0);
}
/**
* Adjust the expiration time of a cached item.
*
* @param string $key
* @param int $seconds
* @return bool
*/
public function touch($key, $seconds)
{
$value = $this->get($key);
if (is_null($value)) {
return false;
}
$this->put($key, $value, $seconds);
return true;
}
/**
* Remove an item from the cache.
*
* @param string $key
* @return bool
*/
public function forget($key)
{
if ($this->session->exists($this->itemKey($key))) {
$this->session->forget($this->itemKey($key));
return true;
}
return false;
}
/**
* Remove all items from the cache.
*
* @return bool
*/
public function flush()
{
$this->session->put($this->key, []);
return true;
}
/**
* Get the cache key prefix.
*
* @return string
*/
public function itemKey($key)
{
return "{$this->key}.{$key}";
}
/**
* Get the cache key prefix.
*
* @return string
*/
public function getPrefix()
{
return '';
}
}
+129
View File
@@ -0,0 +1,129 @@
<?php
namespace Illuminate\Cache;
use Illuminate\Contracts\Cache\Store;
class TagSet
{
/**
* The cache store implementation.
*
* @var \Illuminate\Contracts\Cache\Store
*/
protected $store;
/**
* The tag names.
*
* @var array
*/
protected $names = [];
/**
* Create a new TagSet instance.
*
* @param \Illuminate\Contracts\Cache\Store $store
* @param array $names
*/
public function __construct(Store $store, array $names = [])
{
$this->store = $store;
$this->names = $names;
}
/**
* Reset all tags in the set.
*
* @return void
*/
public function reset()
{
array_walk($this->names, $this->resetTag(...));
}
/**
* Reset the tag and return the new tag identifier.
*
* @param string $name
* @return string
*/
public function resetTag($name)
{
$this->store->forever($this->tagKey($name), $id = str_replace('.', '', uniqid('', true)));
return $id;
}
/**
* Flush all the tags in the set.
*
* @return void
*/
public function flush()
{
array_walk($this->names, $this->flushTag(...));
}
/**
* Flush the tag from the cache.
*
* @param string $name
*/
public function flushTag($name)
{
$this->store->forget($this->tagKey($name));
}
/**
* Get a unique namespace that changes when any of the tags are flushed.
*
* @return string
*/
public function getNamespace()
{
return implode('|', $this->tagIds());
}
/**
* Get an array of tag identifiers for all of the tags in the set.
*
* @return array
*/
protected function tagIds()
{
return array_map($this->tagId(...), $this->names);
}
/**
* Get the unique tag identifier for a given tag.
*
* @param string $name
* @return string
*/
public function tagId($name)
{
return $this->store->get($this->tagKey($name)) ?: $this->resetTag($name);
}
/**
* Get the tag identifier key for a given tag.
*
* @param string $name
* @return string
*/
public function tagKey($name)
{
return 'tag:'.$name.':key';
}
/**
* Get all of the tag names in the set.
*
* @return array
*/
public function getNames()
{
return $this->names;
}
}
@@ -0,0 +1,19 @@
<?php
namespace Illuminate\Cache;
use Illuminate\Contracts\Cache\Store;
abstract class TaggableStore implements Store
{
/**
* Begin executing a new tags operation.
*
* @param mixed $names
* @return \Illuminate\Cache\TaggedCache
*/
public function tags($names)
{
return new TaggedCache($this, new TagSet($this, is_array($names) ? $names : func_get_args()));
}
}
@@ -0,0 +1,136 @@
<?php
namespace Illuminate\Cache;
use Illuminate\Cache\Events\CacheFlushed;
use Illuminate\Cache\Events\CacheFlushing;
use Illuminate\Contracts\Cache\Store;
use function Illuminate\Support\enum_value;
class TaggedCache extends Repository
{
use RetrievesMultipleKeys {
putMany as putManyAlias;
}
/**
* The tag set instance.
*
* @var \Illuminate\Cache\TagSet
*/
protected $tags;
/**
* Create a new tagged cache instance.
*
* @param \Illuminate\Contracts\Cache\Store $store
* @param \Illuminate\Cache\TagSet $tags
*/
public function __construct(Store $store, TagSet $tags)
{
parent::__construct($store);
$this->tags = $tags;
}
/**
* Store multiple items in the cache for a given number of seconds.
*
* @param array $values
* @param int|null $ttl
* @return bool
*/
public function putMany(array $values, $ttl = null)
{
if ($ttl === null) {
return $this->putManyForever($values);
}
return $this->putManyAlias($values, $ttl);
}
/**
* Increment the value of an item in the cache.
*
* @param \UnitEnum|string $key
* @param mixed $value
* @return int|bool
*/
public function increment($key, $value = 1)
{
return $this->store->increment($this->itemKey(enum_value($key)), $value);
}
/**
* Decrement the value of an item in the cache.
*
* @param \UnitEnum|string $key
* @param mixed $value
* @return int|bool
*/
public function decrement($key, $value = 1)
{
return $this->store->decrement($this->itemKey(enum_value($key)), $value);
}
/**
* Remove all items from the cache.
*
* @return bool
*/
public function flush()
{
$this->event(new CacheFlushing($this->getName()));
$this->tags->reset();
$this->event(new CacheFlushed($this->getName()));
return true;
}
/**
* {@inheritdoc}
*/
protected function itemKey($key)
{
return $this->taggedItemKey($key);
}
/**
* Get a fully-qualified key for a tagged item.
*
* @param string $key
* @return string
*/
public function taggedItemKey($key)
{
return sha1($this->tags->getNamespace()).':'.$key;
}
/**
* Fire an event for this cache instance.
*
* @param object $event
* @return void
*/
protected function event($event)
{
if (method_exists($event, 'setTags')) {
$event->setTags($this->tags->getNames());
}
parent::event($event);
}
/**
* Get the tag set instance.
*
* @return \Illuminate\Cache\TagSet
*/
public function getTags()
{
return $this->tags;
}
}
+49
View File
@@ -0,0 +1,49 @@
{
"name": "illuminate/cache",
"description": "The Illuminate Cache package.",
"license": "MIT",
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"homepage": "https://laravel.com",
"support": {
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"require": {
"php": "^8.3",
"illuminate/collections": "^13.0",
"illuminate/contracts": "^13.0",
"illuminate/macroable": "^13.0",
"illuminate/support": "^13.0"
},
"provide": {
"psr/simple-cache-implementation": "1.0 || 2.0 || 3.0"
},
"suggest": {
"ext-apcu": "Required to use the APC cache driver.",
"ext-filter": "Required to use the DynamoDb cache driver.",
"ext-memcached": "Required to use the memcache cache driver.",
"illuminate/database": "Required to use the database cache driver (^13.0).",
"illuminate/filesystem": "Required to use the file cache driver (^13.0).",
"illuminate/redis": "Required to use the redis cache driver (^13.0).",
"symfony/cache": "Required to use PSR-6 cache bridge (^7.4 || ^8.0)."
},
"minimum-stability": "dev",
"autoload": {
"psr-4": {
"Illuminate\\Cache\\": ""
}
},
"config": {
"sort-packages": true
},
"extra": {
"branch-alias": {
"dev-master": "13.0.x-dev"
}
}
}