first commit

This commit is contained in:
Sampanna Rimal
2024-08-27 17:48:06 +05:45
commit 53c0140f58
10839 changed files with 1125847 additions and 0 deletions

View File

@ -0,0 +1,126 @@
<?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
* @return void
*/
public function __construct(ApcWrapper $apc, $prefix = '')
{
$this->apc = $apc;
$this->prefix = $prefix;
}
/**
* Retrieve an item from the cache by key.
*
* @param string|array $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 mixed $value
* @return int|bool
*/
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 mixed $value
* @return int|bool
*/
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);
}
/**
* 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;
}
}

View File

@ -0,0 +1,94 @@
<?php
namespace Illuminate\Cache;
class ApcWrapper
{
/**
* Indicates if APCu is supported.
*
* @var bool
*/
protected $apcu = false;
/**
* Create a new APC wrapper instance.
*
* @return void
*/
public function __construct()
{
$this->apcu = function_exists('apcu_fetch');
}
/**
* Get an item from the cache.
*
* @param string $key
* @return mixed
*/
public function get($key)
{
$fetchedValue = $this->apcu ? apcu_fetch($key, $success) : apc_fetch($key, $success);
return $success ? $fetchedValue : null;
}
/**
* Store an item in the cache.
*
* @param string $key
* @param mixed $value
* @param int $seconds
* @return array|bool
*/
public function put($key, $value, $seconds)
{
return $this->apcu ? apcu_store($key, $value, $seconds) : apc_store($key, $value, $seconds);
}
/**
* Increment the value of an item in the cache.
*
* @param string $key
* @param mixed $value
* @return int|bool
*/
public function increment($key, $value)
{
return $this->apcu ? apcu_inc($key, $value) : apc_inc($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)
{
return $this->apcu ? apcu_dec($key, $value) : apc_dec($key, $value);
}
/**
* Remove an item from the cache.
*
* @param string $key
* @return bool
*/
public function delete($key)
{
return $this->apcu ? apcu_delete($key) : apc_delete($key);
}
/**
* Remove all items from the cache.
*
* @return bool
*/
public function flush()
{
return $this->apcu ? apcu_clear_cache() : apc_clear_cache('user');
}
}

View File

@ -0,0 +1,106 @@
<?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
* @return void
*/
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
*/
protected function getCurrentOwner()
{
if (! $this->exists()) {
return null;
}
return $this->store->locks[$this->name]['owner'];
}
/**
* Releases this lock in disregard of ownership.
*
* @return void
*/
public function forceRelease()
{
unset($this->store->locks[$this->name]);
}
}

View File

@ -0,0 +1,219 @@
<?php
namespace Illuminate\Cache;
use Illuminate\Contracts\Cache\LockProvider;
use Illuminate\Support\Carbon;
use Illuminate\Support\InteractsWithTime;
class ArrayStore extends TaggableStore implements LockProvider
{
use InteractsWithTime, RetrievesMultipleKeys;
/**
* The array of stored values.
*
* @var array
*/
protected $storage = [];
/**
* The array of locks.
*
* @var array
*/
public $locks = [];
/**
* Indicates if values are serialized within the store.
*
* @var bool
*/
protected $serializesValues;
/**
* Create a new Array store.
*
* @param bool $serializesValues
* @return void
*/
public function __construct($serializesValues = false)
{
$this->serializesValues = $serializesValues;
}
/**
* Retrieve an item from the cache by key.
*
* @param string|array $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 ? 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);
}
/**
* 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;
}
/**
* 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);
}
}

View File

@ -0,0 +1,85 @@
<?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
* @return void
*/
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);
}
}

View File

@ -0,0 +1,431 @@
<?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 InvalidArgumentException;
/**
* @mixin \Illuminate\Cache\Repository
* @mixin \Illuminate\Contracts\Cache\LockProvider
*/
class CacheManager implements FactoryContract
{
/**
* 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
* @return void
*/
public function __construct($app)
{
$this->app = $app;
}
/**
* Get a cache store instance by name, wrapped in a repository.
*
* @param string|null $name
* @return \Illuminate\Contracts\Cache\Repository
*/
public function store($name = null)
{
$name = $name ?: $this->getDefaultDriver();
return $this->stores[$name] ??= $this->resolve($name);
}
/**
* Get a cache driver instance.
*
* @param string|null $driver
* @return \Illuminate\Contracts\Cache\Repository
*/
public function driver($driver = null)
{
return $this->store($driver);
}
/**
* 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.");
}
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));
}
/**
* 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));
}
/**
* 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))
->setLockDirectory($config['lock_path'] ?? null)
);
}
/**
* 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));
}
/**
* 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);
return $this->repository(
$store->setLockConnection($config['lock_connection'] ?? $connection)
);
}
/**
* 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,
);
return $this->repository($store->setLockConnection(
$this->app['db']->connection($config['lock_connection'] ?? $config['connection'] ?? null)
));
}
/**
* 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)
)
);
}
/**
* 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 a new cache repository with the given implementation.
*
* @param \Illuminate\Contracts\Cache\Store $store
* @return \Illuminate\Cache\Repository
*/
public function repository(Store $store)
{
return tap(new Repository($store), function ($repository) {
$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 cache connection configuration.
*
* @param string $name
* @return array|null
*/
protected function getConfig($name)
{
if (! is_null($name) && $name !== 'null') {
return $this->app['config']["cache.stores.{$name}"];
}
return ['driver' => 'null'];
}
/**
* Get the default cache driver name.
*
* @return string
*/
public function getDefaultDriver()
{
return $this->app['config']['cache.default'];
}
/**
* Set the default cache driver name.
*
* @param string $name
* @return void
*/
public function setDefaultDriver($name)
{
$this->app['config']['cache.default'] = $name;
}
/**
* Unset the given driver instances.
*
* @param array|string|null $name
* @return $this
*/
public function forgetDriver($name = null)
{
$name ??= $this->getDefaultDriver();
foreach ((array) $name as $cacheName) {
if (isset($this->stores[$cacheName])) {
unset($this->stores[$cacheName]);
}
}
return $this;
}
/**
* Disconnect the given driver and remove from local cache.
*
* @param string|null $name
* @return void
*/
public function purge($name = null)
{
$name ??= $this->getDefaultDriver();
unset($this->stores[$name]);
}
/**
* Register a custom driver creator Closure.
*
* @param string $driver
* @param \Closure $callback
* @return $this
*/
public function extend($driver, Closure $callback)
{
$this->customCreators[$driver] = $callback->bindTo($this, $this);
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;
}
/**
* Dynamically call the default driver instance.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
return $this->store()->$method(...$parameters);
}
}

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,
];
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace Illuminate\Cache\Console;
use Illuminate\Console\MigrationGeneratorCommand;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'cache:table')]
class CacheTableCommand extends MigrationGeneratorCommand
{
/**
* The console command name.
*
* @var string
*/
protected $name = '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';
}
}

View File

@ -0,0 +1,147 @@
<?php
namespace Illuminate\Cache\Console;
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
* @return void
*/
public function __construct(CacheManager $cache, Filesystem $files)
{
parent::__construct();
$this->cache = $cache;
$this->files = $files;
}
/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
$this->laravel['events']->dispatch(
'cache:clearing', [$this->argument('store'), $this->tags()]
);
$successful = $this->cache()->flush();
$this->flushFacades();
if (! $successful) {
return $this->components->error('Failed to clear cache. Make sure you have the appropriate permissions.');
}
$this->laravel['events']->dispatch(
'cache:cleared', [$this->argument('store'), $this->tags()]
);
$this->components->info('Application cache cleared successfully.');
}
/**
* 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],
];
}
}

View File

@ -0,0 +1,59 @@
<?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
* @return void
*/
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.');
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace Illuminate\Cache\Console;
use Illuminate\Cache\CacheManager;
use Illuminate\Cache\RedisStore;
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 (! $cache->getStore() instanceof RedisStore) {
$this->components->error('Pruning cache tags is only necessary when using Redis.');
return 1;
}
$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'],
];
}
}

View File

@ -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->integer('expiration');
});
Schema::create('cache_locks', function (Blueprint $table) {
$table->string('key')->primary();
$table->string('owner');
$table->integer('expiration');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('cache');
Schema::dropIfExists('cache_locks');
}
};

View File

@ -0,0 +1,156 @@
<?php
namespace Illuminate\Cache;
use Illuminate\Database\Connection;
use Illuminate\Database\QueryException;
class DatabaseLock extends Lock
{
/**
* The database connection instance.
*
* @var \Illuminate\Database\Connection
*/
protected $connection;
/**
* The database table name.
*
* @var string
*/
protected $table;
/**
* The prune probability odds.
*
* @var array
*/
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 $lottery
* @return void
*/
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
*/
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', '<=', time());
})->update([
'owner' => $this->owner,
'expiration' => $this->expiresAt(),
]);
$acquired = $updated >= 1;
}
if (random_int(1, $this->lottery[1]) <= $this->lottery[0]) {
$this->connection->table($this->table)->where('expiration', '<=', time())->delete();
}
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 time() + $lockTimeout;
}
/**
* Release the lock.
*
* @return bool
*/
public function release()
{
if ($this->isOwnedByCurrentProcess()) {
$this->connection->table($this->table)
->where('key', $this->name)
->where('owner', $this->owner)
->delete();
return true;
}
return false;
}
/**
* Releases this lock in disregard of ownership.
*
* @return void
*/
public function forceRelease()
{
$this->connection->table($this->table)
->where('key', $this->name)
->delete();
}
/**
* Returns the owner value written into the driver for this lock.
*
* @return string
*/
protected function getCurrentOwner()
{
return optional($this->connection->table($this->table)->where('key', $this->name)->first())->owner;
}
/**
* Get the name of the database connection being used to manage the lock.
*
* @return string
*/
public function getConnectionName()
{
return $this->connection->getName();
}
}

View File

@ -0,0 +1,421 @@
<?php
namespace Illuminate\Cache;
use Closure;
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\SqlServerConnection;
use Illuminate\Support\InteractsWithTime;
use Illuminate\Support\Str;
class DatabaseStore implements LockProvider, Store
{
use InteractsWithTime, RetrievesMultipleKeys;
/**
* 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;
/**
* Create a new database store.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param string $table
* @param string $prefix
* @param string $lockTable
* @param array $lockLottery
* @return void
*/
public function __construct(ConnectionInterface $connection,
$table,
$prefix = '',
$lockTable = 'cache_locks',
$lockLottery = [2, 100],
$defaultLockTimeoutInSeconds = 86400)
{
$this->table = $table;
$this->prefix = $prefix;
$this->connection = $connection;
$this->lockTable = $lockTable;
$this->lockLottery = $lockLottery;
$this->defaultLockTimeoutInSeconds = $defaultLockTimeoutInSeconds;
}
/**
* Retrieve an item from the cache by key.
*
* @param string|array $key
* @return mixed
*/
public function get($key)
{
$prefixed = $this->prefix.$key;
$cache = $this->table()->where('key', '=', $prefixed)->first();
// If we have a cache record we will check the expiration time against current
// time on the system and see if the record has expired. If it has, we will
// remove the records from the database table so it isn't returned again.
if (is_null($cache)) {
return;
}
$cache = is_array($cache) ? (object) $cache : $cache;
// 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.
if ($this->currentTime() >= $cache->expiration) {
$this->forgetIfExpired($key);
return;
}
return $this->unserialize($cache->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)
{
$key = $this->prefix.$key;
$value = $this->serialize($value);
$expiration = $this->getTime() + $seconds;
return $this->table()->upsert(compact('key', 'value', 'expiration'), '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;
$doesntSupportInsertOrIgnore = [SqlServerConnection::class];
if (! in_array(get_class($this->getConnection()), $doesntSupportInsertOrIgnore)) {
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 mixed $value
* @return int|bool
*/
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 mixed $value
* @return int|bool
*/
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 mixed $value
* @param \Closure $callback
* @return int|bool
*/
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);
}
/**
* Remove an item from the cache.
*
* @param string $key
* @return bool
*/
public function forget($key)
{
$this->table()->where('key', '=', $this->prefix.$key)->delete();
return true;
}
/**
* Remove an item from the cache if it is expired.
*
* @param string $key
* @return bool
*/
public function forgetIfExpired($key)
{
$this->table()
->where('key', '=', $this->prefix.$key)
->where('expiration', '<=', $this->getTime())
->delete();
return true;
}
/**
* Remove all items from the cache.
*
* @return bool
*/
public function flush()
{
$this->table()->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 the underlying database connection.
*
* @return \Illuminate\Database\ConnectionInterface
*/
public function getConnection()
{
return $this->connection;
}
/**
* Specify the name of 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;
}
/**
* Serialize the given value.
*
* @param mixed $value
* @return string
*/
protected function serialize($value)
{
$result = serialize($value);
if ($this->connection instanceof PostgresConnection && 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 && ! Str::contains($value, [':', ';'])) {
$value = base64_decode($value);
}
return unserialize($value);
}
}

View File

@ -0,0 +1,77 @@
<?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
* @return void
*/
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);
}
}

View File

@ -0,0 +1,546 @@
<?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\Carbon;
use Illuminate\Support\InteractsWithTime;
use Illuminate\Support\Str;
use RuntimeException;
class DynamoDbStore implements LockProvider, Store
{
use InteractsWithTime;
/**
* The DynamoDB client instance.
*
* @var \Aws\DynamoDb\DynamoDbClient
*/
protected $dynamo;
/**
* The table name.
*
* @var string
*/
protected $table;
/**
* The name of the attribute that should hold the key.
*
* @var string
*/
protected $keyAttribute;
/**
* The name of the attribute that should hold the value.
*
* @var string
*/
protected $valueAttribute;
/**
* The name of the attribute that should hold the expiration timestamp.
*
* @var string
*/
protected $expirationAttribute;
/**
* A string that should be prepended to keys.
*
* @var string
*/
protected $prefix;
/**
* Create a new store instance.
*
* @param \Aws\DynamoDb\DynamoDbClient $dynamo
* @param string $table
* @param string $keyAttribute
* @param string $valueAttribute
* @param string $expirationAttribute
* @param string $prefix
* @return void
*/
public function __construct(DynamoDbClient $dynamo,
$table,
$keyAttribute = 'key',
$valueAttribute = 'value',
$expirationAttribute = 'expires_at',
$prefix = '')
{
$this->table = $table;
$this->dynamo = $dynamo;
$this->keyAttribute = $keyAttribute;
$this->valueAttribute = $valueAttribute;
$this->expirationAttribute = $expirationAttribute;
$this->setPrefix($prefix);
}
/**
* 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 (count($keys) === 0) {
return [];
}
$prefixedKeys = array_map(function ($key) {
return $this->prefix.$key;
}, $keys);
$response = $this->dynamo->batchGetItem([
'RequestItems' => [
$this->table => [
'ConsistentRead' => false,
'Keys' => collect($prefixedKeys)->map(function ($key) {
return [
$this->keyAttribute => [
'S' => $key,
],
];
})->all(),
],
],
]);
$now = Carbon::now();
return array_merge(collect(array_flip($keys))->map(function () {
//
})->all(), collect($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 (count($values) === 0) {
return true;
}
$expiration = $this->toTimestamp($seconds);
$this->dynamo->batchWriteItem([
'RequestItems' => [
$this->table => collect($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
*/
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|bool
*/
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|bool
*/
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, $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);
}
/**
* 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 bool
*
* @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;
}
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 = ! empty($prefix) ? $prefix.':' : '';
}
/**
* Get the DynamoDb Client instance.
*
* @return \Aws\DynamoDb\DynamoDbClient
*/
public function getClient()
{
return $this->dynamo;
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace Illuminate\Cache\Events;
abstract class CacheEvent
{
/**
* 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 $key
* @param array $tags
* @return void
*/
public function __construct($key, array $tags = [])
{
$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;
}
}

View File

@ -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 $key
* @param mixed $value
* @param array $tags
* @return void
*/
public function __construct($key, $value, array $tags = [])
{
parent::__construct($key, $tags);
$this->value = $value;
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace Illuminate\Cache\Events;
class CacheMissed extends CacheEvent
{
//
}

View File

@ -0,0 +1,8 @@
<?php
namespace Illuminate\Cache\Events;
class KeyForgotten extends CacheEvent
{
//
}

View File

@ -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 $key
* @param mixed $value
* @param int|null $seconds
* @param array $tags
* @return void
*/
public function __construct($key, $value, $seconds = null, $tags = [])
{
parent::__construct($key, $tags);
$this->value = $value;
$this->seconds = $seconds;
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,405 @@
<?php
namespace Illuminate\Cache;
use Exception;
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;
class FileStore implements Store, LockProvider
{
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;
/**
* Create a new file cache store instance.
*
* @param \Illuminate\Filesystem\Filesystem $files
* @param string $directory
* @param int|null $filePermission
* @return void
*/
public function __construct(Filesystem $files, $directory, $filePermission = null)
{
$this->files = $files;
$this->directory = $directory;
$this->filePermission = $filePermission;
}
/**
* Retrieve an item from the cache by key.
*
* @param string|array $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),
$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);
}
/**
* Remove an item from the cache.
*
* @param string $key
* @return bool
*/
public function forget($key)
{
if ($this->files->exists($file = $this->path($key))) {
return $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;
}
/**
* 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 = 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');
}
/**
* 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 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 '';
}
}

View File

@ -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);
}
}

View File

@ -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.

View File

@ -0,0 +1,168 @@
<?php
namespace Illuminate\Cache;
use Illuminate\Contracts\Cache\Lock as LockContract;
use Illuminate\Contracts\Cache\LockTimeoutException;
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
* @return void
*/
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
*/
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 = $this->currentTime();
while (! $this->acquire()) {
Sleep::usleep($this->sleepMilliseconds * 1000);
if ($this->currentTime() - $seconds >= $starting) {
throw new LockTimeoutException;
}
}
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;
}
/**
* Determines whether this lock is allowed to release the lock in the driver.
*
* @return bool
*/
public function isOwnedByCurrentProcess()
{
return $this->getCurrentOwner() === $this->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;
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace Illuminate\Cache;
class LuaScripts
{
/**
* 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;
}
}

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 (count($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);
}
}

View File

@ -0,0 +1,75 @@
<?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
* @return void
*/
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);
}
}

View File

@ -0,0 +1,279 @@
<?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
* @return void
*/
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|bool
*/
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|bool
*/
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);
}
/**
* 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 = ! empty($prefix) ? $prefix.':' : '';
}
}

View File

@ -0,0 +1,46 @@
<?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()
{
//
}
/**
* Returns the owner value written into the driver for this lock.
*
* @return mixed
*/
protected function getCurrentOwner()
{
return $this->owner;
}
}

View File

@ -0,0 +1,126 @@
<?php
namespace Illuminate\Cache;
use Illuminate\Contracts\Cache\LockProvider;
class NullStore extends TaggableStore implements 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 bool
*/
public function increment($key, $value = 1)
{
return false;
}
/**
* Decrement the value of an item in the cache.
*
* @param string $key
* @param mixed $value
* @return bool
*/
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);
}
/**
* 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 '';
}
}

View File

@ -0,0 +1,35 @@
<?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
* @return void
*/
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])
);
}
}

View File

@ -0,0 +1,238 @@
<?php
namespace Illuminate\Cache;
use Closure;
use Illuminate\Contracts\Cache\Repository as Cache;
use Illuminate\Support\InteractsWithTime;
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
* @return void
*/
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
/**
* Register a named limiter configuration.
*
* @param string $name
* @param \Closure $callback
* @return $this
*/
public function for(string $name, Closure $callback)
{
$this->limiters[$name] = $callback;
return $this;
}
/**
* Get the given named rate limiter.
*
* @param string $name
* @return \Closure|null
*/
public function limiter(string $name)
{
return $this->limiters[$name] ?? null;
}
/**
* Attempts to execute a callback if it's not limited.
*
* @param string $key
* @param int $maxAttempts
* @param \Closure $callback
* @param 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 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 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->cache->add($key, 0, $decaySeconds);
$hits = (int) $this->cache->increment($key, $amount);
if (! $added && $hits == 1) {
$this->cache->put($key, 1, $decaySeconds);
}
return $hits;
}
/**
* Get the number of attempts for the given key.
*
* @param string $key
* @return mixed
*/
public function attempts($key)
{
$key = $this->cleanRateLimiterKey($key);
return $this->cache->get($key, 0);
}
/**
* Reset the number of attempts for the given key.
*
* @param string $key
* @return mixed
*/
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 $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));
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Illuminate\Cache\RateLimiting;
class GlobalLimit extends Limit
{
/**
* Create a new limit instance.
*
* @param int $maxAttempts
* @param int $decayMinutes
* @return void
*/
public function __construct(int $maxAttempts, int $decayMinutes = 1)
{
parent::__construct('', $maxAttempts, $decayMinutes);
}
}

View File

@ -0,0 +1,132 @@
<?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 minutes.
*
* @var int
*/
public $maxAttempts;
/**
* The number of minutes until the rate limit is reset.
*
* @var int
*/
public $decayMinutes;
/**
* The response generator callback.
*
* @var callable
*/
public $responseCallback;
/**
* Create a new limit instance.
*
* @param mixed $key
* @param int $maxAttempts
* @param int $decayMinutes
* @return void
*/
public function __construct($key = '', int $maxAttempts = 60, int $decayMinutes = 1)
{
$this->key = $key;
$this->maxAttempts = $maxAttempts;
$this->decayMinutes = $decayMinutes;
}
/**
* Create a new rate limit.
*
* @param int $maxAttempts
* @return static
*/
public static function perMinute($maxAttempts)
{
return new static('', $maxAttempts);
}
/**
* 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, $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 * $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 * 24 * $decayDays);
}
/**
* Create a new unlimited rate limit.
*
* @return static
*/
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 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;
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace Illuminate\Cache\RateLimiting;
class Unlimited extends GlobalLimit
{
/**
* Create a new limit instance.
*
* @return void
*/
public function __construct()
{
parent::__construct(PHP_INT_MAX);
}
}

View File

@ -0,0 +1,83 @@
<?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
* @return void
*/
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
*/
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();
}
}

View File

@ -0,0 +1,419 @@
<?php
namespace Illuminate\Cache;
use Illuminate\Contracts\Cache\LockProvider;
use Illuminate\Contracts\Redis\Factory as Redis;
use Illuminate\Redis\Connections\PhpRedisConnection;
use Illuminate\Redis\Connections\PredisConnection;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
class RedisStore extends TaggableStore implements LockProvider
{
/**
* 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;
/**
* Create a new Redis store.
*
* @param \Illuminate\Contracts\Redis\Factory $redis
* @param string $prefix
* @param string $connection
* @return void
*/
public function __construct(Redis $redis, $prefix = '', $connection = 'default')
{
$this->redis = $redis;
$this->setPrefix($prefix);
$this->setConnection($connection);
}
/**
* Retrieve an item from the cache by key.
*
* @param string|array $key
* @return mixed
*/
public function get($key)
{
$value = $this->connection()->get($this->prefix.$key);
return ! is_null($value) ? $this->unserialize($value) : 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 (count($keys) === 0) {
return [];
}
$results = [];
$values = $this->connection()->mget(array_map(function ($key) {
return $this->prefix.$key;
}, $keys));
foreach ($values as $index => $value) {
$results[$keys[$index]] = ! is_null($value) ? $this->unserialize($value) : 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)
{
return (bool) $this->connection()->setex(
$this->prefix.$key, (int) max(1, $seconds), $this->serialize($value)
);
}
/**
* 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 = [];
foreach ($values as $key => $value) {
$serializedValues[$this->prefix.$key] = $this->serialize($value);
}
$this->connection()->multi();
$manyResult = null;
foreach ($serializedValues as $key => $value) {
$result = (bool) $this->connection()->setex(
$key, (int) max(1, $seconds), $value
);
$manyResult = is_null($manyResult) ? $result : $result && $manyResult;
}
$this->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)
{
$lua = "return redis.call('exists',KEYS[1])<1 and redis.call('setex',KEYS[1],ARGV[2],ARGV[1])";
return (bool) $this->connection()->eval(
$lua, 1, $this->prefix.$key, $this->serialize($value), (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)
{
return (bool) $this->connection()->set($this->prefix.$key, $this->serialize($value));
}
/**
* 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);
}
/**
* 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 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 array|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 => '',
};
$prefix = $connectionPrefix.$this->getPrefix();
return LazyCollection::make(function () use ($connection, $chunkSize, $prefix) {
$cursor = $defaultCursorValue = '0';
do {
[$cursor, $tagsChunk] = $connection->scan(
$cursor,
['match' => $prefix.'tag:*:entries', 'count' => $chunkSize]
);
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 = ! empty($prefix) ? $prefix.':' : '';
}
/**
* Serialize the value.
*
* @param mixed $value
* @return mixed
*/
protected function serialize($value)
{
return is_numeric($value) && ! in_array($value, [INF, -INF]) && ! is_nan($value) ? $value : serialize($value);
}
/**
* Unserialize the value.
*
* @param mixed $value
* @return mixed
*/
protected function unserialize($value)
{
return is_numeric($value) ? $value : unserialize($value);
}
}

View File

@ -0,0 +1,125 @@
<?php
namespace Illuminate\Cache;
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 $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()
{
return LazyCollection::make(function () {
foreach ($this->tagIds() as $tagKey) {
$cursor = $defaultCursorValue = '0';
do {
[$cursor, $entries] = $this->store->connection()->zscan(
$this->store->getPrefix().$tagKey,
$cursor,
['match' => '*', 'count' => 1000]
);
if (! is_array($entries)) {
break;
}
$entries = array_unique(array_keys($entries));
if (count($entries) === 0) {
continue;
}
foreach ($entries as $entry) {
yield $entry;
}
} while (((string) $cursor) !== $defaultCursorValue);
}
});
}
/**
* Remove the stale entries from the tag set.
*
* @return void
*/
public function flushStaleEntries()
{
$this->store->connection()->pipeline(function ($pipe) {
foreach ($this->tagIds() as $tagKey) {
$pipe->zremrangebyscore($this->store->getPrefix().$tagKey, 0, Carbon::now()->getTimestamp());
}
});
}
/**
* Flush the tag from the cache.
*
* @param string $name
*/
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";
}
}

View File

@ -0,0 +1,141 @@
<?php
namespace Illuminate\Cache;
class RedisTaggedCache extends TaggedCache
{
/**
* Store an item in the cache if the key does not exist.
*
* @param string $key
* @param mixed $value
* @param \DateTimeInterface|\DateInterval|int|null $ttl
* @return bool
*/
public function add($key, $value, $ttl = null)
{
$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 string $key
* @param mixed $value
* @param \DateTimeInterface|\DateInterval|int|null $ttl
* @return bool
*/
public function put($key, $value, $ttl = null)
{
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 string $key
* @param mixed $value
* @return int|bool
*/
public function increment($key, $value = 1)
{
$this->tags->addEntry($this->itemKey($key), updateWhen: 'NX');
return parent::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)
{
$this->tags->addEntry($this->itemKey($key), updateWhen: 'NX');
return parent::decrement($key, $value);
}
/**
* Store an item in the cache indefinitely.
*
* @param string $key
* @param mixed $value
* @return bool
*/
public function forever($key, $value)
{
$this->tags->addEntry($this->itemKey($key));
return parent::forever($key, $value);
}
/**
* Remove all items from the cache.
*
* @return bool
*/
public function flush()
{
$this->flushValues();
$this->tags->flush();
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);
foreach ($entries as $cacheKeys) {
$this->store->connection()->del(...$cacheKeys);
}
}
/**
* Remove all stale reference entries from the tag set.
*
* @return bool
*/
public function flushStale()
{
$this->tags->flushStaleEntries();
return true;
}
}

View File

@ -0,0 +1,704 @@
<?php
namespace Illuminate\Cache;
use ArrayAccess;
use BadMethodCallException;
use Closure;
use DateTimeInterface;
use Illuminate\Cache\Events\CacheHit;
use Illuminate\Cache\Events\CacheMissed;
use Illuminate\Cache\Events\KeyForgotten;
use Illuminate\Cache\Events\KeyWritten;
use Illuminate\Contracts\Cache\Repository as CacheContract;
use Illuminate\Contracts\Cache\Store;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Support\Carbon;
use Illuminate\Support\InteractsWithTime;
use Illuminate\Support\Traits\Macroable;
/**
* @mixin \Illuminate\Contracts\Cache\Store
*/
class Repository implements ArrayAccess, CacheContract
{
use InteractsWithTime, Macroable {
__call as macroCall;
}
/**
* The cache store implementation.
*
* @var \Illuminate\Contracts\Cache\Store
*/
protected $store;
/**
* The event dispatcher implementation.
*
* @var \Illuminate\Contracts\Events\Dispatcher
*/
protected $events;
/**
* The default number of seconds to store items.
*
* @var int|null
*/
protected $default = 3600;
/**
* Create a new cache repository instance.
*
* @param \Illuminate\Contracts\Cache\Store $store
* @return void
*/
public function __construct(Store $store)
{
$this->store = $store;
}
/**
* Determine if an item exists in the cache.
*
* @param array|string $key
* @return bool
*/
public function has($key): bool
{
return ! is_null($this->get($key));
}
/**
* Determine if an item doesn't exist in the cache.
*
* @param string $key
* @return bool
*/
public function missing($key)
{
return ! $this->has($key);
}
/**
* Retrieve an item from the cache by key.
*
* @template TCacheValue
*
* @param array|string $key
* @param TCacheValue|(\Closure(): TCacheValue) $default
* @return (TCacheValue is null ? mixed : TCacheValue)
*/
public function get($key, $default = null): mixed
{
if (is_array($key)) {
return $this->many($key);
}
$value = $this->store->get($this->itemKey($key));
// If we could not find the cache value, we will fire the missed event and get
// the default value for this cache value. This default could be a callback
// so we will execute the value function which will resolve it if needed.
if (is_null($value)) {
$this->event(new CacheMissed($key));
$value = value($default);
} else {
$this->event(new CacheHit($key, $value));
}
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)
{
$values = $this->store->many(collect($keys)->map(function ($value, $key) {
return is_string($key) ? $key : $value;
})->values()->all());
return collect($values)->map(function ($value, $key) use ($keys) {
return $this->handleManyResult($keys, $key, $value);
})->all();
}
/**
* {@inheritdoc}
*
* @return iterable
*/
public function getMultiple($keys, $default = null): iterable
{
$defaults = [];
foreach ($keys as $key) {
$defaults[$key] = $default;
}
return $this->many($defaults);
}
/**
* Handle a result for the "many" method.
*
* @param array $keys
* @param string $key
* @param mixed $value
* @return mixed
*/
protected function handleManyResult($keys, $key, $value)
{
// If we could not find the cache value, we will fire the missed event and get
// the default value for this cache value. This default could be a callback
// so we will execute the value function which will resolve it if needed.
if (is_null($value)) {
$this->event(new CacheMissed($key));
return (isset($keys[$key]) && ! array_is_list($keys)) ? value($keys[$key]) : null;
}
// If we found a valid value we will fire the "hit" event and return the value
// back from this function. The "hit" event gives developers an opportunity
// to listen for every possible cache "hit" throughout this applications.
$this->event(new CacheHit($key, $value));
return $value;
}
/**
* Retrieve an item from the cache and delete it.
*
* @template TCacheValue
*
* @param array|string $key
* @param TCacheValue|(\Closure(): TCacheValue) $default
* @return (TCacheValue is null ? mixed : TCacheValue)
*/
public function pull($key, $default = null)
{
return tap($this->get($key, $default), function () use ($key) {
$this->forget($key);
});
}
/**
* Store an item in the cache.
*
* @param array|string $key
* @param mixed $value
* @param \DateTimeInterface|\DateInterval|int|null $ttl
* @return bool
*/
public function put($key, $value, $ttl = null)
{
if (is_array($key)) {
return $this->putMany($key, $value);
}
if ($ttl === null) {
return $this->forever($key, $value);
}
$seconds = $this->getSeconds($ttl);
if ($seconds <= 0) {
return $this->forget($key);
}
$result = $this->store->put($this->itemKey($key), $value, $seconds);
if ($result) {
$this->event(new KeyWritten($key, $value, $seconds));
}
return $result;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function set($key, $value, $ttl = null): bool
{
return $this->put($key, $value, $ttl);
}
/**
* Store multiple items in the cache for a given number of seconds.
*
* @param array $values
* @param \DateTimeInterface|\DateInterval|int|null $ttl
* @return bool
*/
public function putMany(array $values, $ttl = null)
{
if ($ttl === null) {
return $this->putManyForever($values);
}
$seconds = $this->getSeconds($ttl);
if ($seconds <= 0) {
return $this->deleteMultiple(array_keys($values));
}
$result = $this->store->putMany($values, $seconds);
if ($result) {
foreach ($values as $key => $value) {
$this->event(new KeyWritten($key, $value, $seconds));
}
}
return $result;
}
/**
* Store multiple items in the cache indefinitely.
*
* @param array $values
* @return bool
*/
protected function putManyForever(array $values)
{
$result = true;
foreach ($values as $key => $value) {
if (! $this->forever($key, $value)) {
$result = false;
}
}
return $result;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function setMultiple($values, $ttl = null): bool
{
return $this->putMany(is_array($values) ? $values : iterator_to_array($values), $ttl);
}
/**
* Store an item in the cache if the key does not exist.
*
* @param string $key
* @param mixed $value
* @param \DateTimeInterface|\DateInterval|int|null $ttl
* @return bool
*/
public function add($key, $value, $ttl = null)
{
$seconds = null;
if ($ttl !== null) {
$seconds = $this->getSeconds($ttl);
if ($seconds <= 0) {
return false;
}
// If the store has an "add" method we will call the method on the store so it
// has a chance to override this logic. Some drivers better support the way
// this operation should work with a total "atomic" implementation of it.
if (method_exists($this->store, 'add')) {
return $this->store->add(
$this->itemKey($key), $value, $seconds
);
}
}
// If the value did not exist in the cache, we will put the value in the cache
// so it exists for subsequent requests. Then, we will return true so it is
// easy to know if the value gets added. Otherwise, we will return false.
if (is_null($this->get($key))) {
return $this->put($key, $value, $seconds);
}
return false;
}
/**
* Increment the value of an item in the cache.
*
* @param string $key
* @param mixed $value
* @return int|bool
*/
public function increment($key, $value = 1)
{
return $this->store->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)
{
return $this->store->decrement($key, $value);
}
/**
* Store an item in the cache indefinitely.
*
* @param string $key
* @param mixed $value
* @return bool
*/
public function forever($key, $value)
{
$result = $this->store->forever($this->itemKey($key), $value);
if ($result) {
$this->event(new KeyWritten($key, $value));
}
return $result;
}
/**
* Get an item from the cache, or execute the given Closure and store the result.
*
* @template TCacheValue
*
* @param string $key
* @param \Closure|\DateTimeInterface|\DateInterval|int|null $ttl
* @param \Closure(): TCacheValue $callback
* @return TCacheValue
*/
public function remember($key, $ttl, Closure $callback)
{
$value = $this->get($key);
// If the item exists in the cache we will just return this immediately and if
// not we will execute the given Closure and cache the result of that for a
// given number of seconds so it's available for all subsequent requests.
if (! is_null($value)) {
return $value;
}
$value = $callback();
$this->put($key, $value, value($ttl, $value));
return $value;
}
/**
* Get an item from the cache, or execute the given Closure and store the result forever.
*
* @template TCacheValue
*
* @param string $key
* @param \Closure(): TCacheValue $callback
* @return TCacheValue
*/
public function sear($key, Closure $callback)
{
return $this->rememberForever($key, $callback);
}
/**
* Get an item from the cache, or execute the given Closure and store the result forever.
*
* @template TCacheValue
*
* @param string $key
* @param \Closure(): TCacheValue $callback
* @return TCacheValue
*/
public function rememberForever($key, Closure $callback)
{
$value = $this->get($key);
// If the item exists in the cache we will just return this immediately
// and if not we will execute the given Closure and cache the result
// of that forever so it is available for all subsequent requests.
if (! is_null($value)) {
return $value;
}
$this->forever($key, $value = $callback());
return $value;
}
/**
* Remove an item from the cache.
*
* @param string $key
* @return bool
*/
public function forget($key)
{
return tap($this->store->forget($this->itemKey($key)), function ($result) use ($key) {
if ($result) {
$this->event(new KeyForgotten($key));
}
});
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function delete($key): bool
{
return $this->forget($key);
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function deleteMultiple($keys): bool
{
$result = true;
foreach ($keys as $key) {
if (! $this->forget($key)) {
$result = false;
}
}
return $result;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function clear(): bool
{
return $this->store->flush();
}
/**
* Begin executing a new tags operation if the store supports it.
*
* @param array|mixed $names
* @return \Illuminate\Cache\TaggedCache
*
* @throws \BadMethodCallException
*/
public function tags($names)
{
if (! $this->supportsTags()) {
throw new BadMethodCallException('This cache store does not support tagging.');
}
$cache = $this->store->tags(is_array($names) ? $names : func_get_args());
if (! is_null($this->events)) {
$cache->setEventDispatcher($this->events);
}
return $cache->setDefaultCacheTime($this->default);
}
/**
* Format the key for a cache item.
*
* @param string $key
* @return string
*/
protected function itemKey($key)
{
return $key;
}
/**
* Calculate the number of seconds for the given TTL.
*
* @param \DateTimeInterface|\DateInterval|int $ttl
* @return int
*/
protected function getSeconds($ttl)
{
$duration = $this->parseDateInterval($ttl);
if ($duration instanceof DateTimeInterface) {
$duration = Carbon::now()->diffInRealSeconds($duration, false);
}
return (int) ($duration > 0 ? $duration : 0);
}
/**
* Determine if the current store supports tags.
*
* @return bool
*/
public function supportsTags()
{
return method_exists($this->store, 'tags');
}
/**
* Get the default cache time.
*
* @return int|null
*/
public function getDefaultCacheTime()
{
return $this->default;
}
/**
* Set the default cache time in seconds.
*
* @param int|null $seconds
* @return $this
*/
public function setDefaultCacheTime($seconds)
{
$this->default = $seconds;
return $this;
}
/**
* Get the cache store implementation.
*
* @return \Illuminate\Contracts\Cache\Store
*/
public function getStore()
{
return $this->store;
}
/**
* Set the cache store implementation.
*
* @param \Illuminate\Contracts\Cache\Store $store
* @return static
*/
public function setStore($store)
{
$this->store = $store;
return $this;
}
/**
* Fire an event for this cache instance.
*
* @param object|string $event
* @return void
*/
protected function event($event)
{
$this->events?->dispatch($event);
}
/**
* Get the event dispatcher instance.
*
* @return \Illuminate\Contracts\Events\Dispatcher
*/
public function getEventDispatcher()
{
return $this->events;
}
/**
* Set the event dispatcher instance.
*
* @param \Illuminate\Contracts\Events\Dispatcher $events
* @return void
*/
public function setEventDispatcher(Dispatcher $events)
{
$this->events = $events;
}
/**
* Determine if a cached value exists.
*
* @param string $key
* @return bool
*/
public function offsetExists($key): bool
{
return $this->has($key);
}
/**
* Retrieve an item from the cache by key.
*
* @param string $key
* @return mixed
*/
public function offsetGet($key): mixed
{
return $this->get($key);
}
/**
* Store an item in the cache for the default time.
*
* @param string $key
* @param mixed $value
* @return void
*/
public function offsetSet($key, $value): void
{
$this->put($key, $value, $this->default);
}
/**
* Remove an item from the cache.
*
* @param string $key
* @return void
*/
public function offsetUnset($key): void
{
$this->forget($key);
}
/**
* Handle dynamic calls into macros or pass missing methods to the store.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
if (static::hasMacro($method)) {
return $this->macroCall($method, $parameters);
}
return $this->store->$method(...$parameters);
}
/**
* Clone cache repository instance.
*
* @return void
*/
public function __clone()
{
$this->store = clone $this->store;
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace Illuminate\Cache;
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 = collect($keys)->mapWithKeys(function ($value, $key) {
return [is_string($key) ? $key : $value => is_string($key) ? $value : null];
})->all();
foreach ($keys as $key => $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;
}
}

View File

@ -0,0 +1,130 @@
<?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
* @return void
*/
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;
}
}

View File

@ -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 array|mixed $names
* @return \Illuminate\Cache\TaggedCache
*/
public function tags($names)
{
return new TaggedCache($this, new TagSet($this, is_array($names) ? $names : func_get_args()));
}
}

View File

@ -0,0 +1,125 @@
<?php
namespace Illuminate\Cache;
use Illuminate\Contracts\Cache\Store;
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
* @return void
*/
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 string $key
* @param mixed $value
* @return int|bool
*/
public function increment($key, $value = 1)
{
return $this->store->increment($this->itemKey($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)
{
return $this->store->decrement($this->itemKey($key), $value);
}
/**
* Remove all items from the cache.
*
* @return bool
*/
public function flush()
{
$this->tags->reset();
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 \Illuminate\Cache\Events\CacheEvent $event
* @return void
*/
protected function event($event)
{
parent::event($event->setTags($this->tags->getNames()));
}
/**
* Get the tag set instance.
*
* @return \Illuminate\Cache\TagSet
*/
public function getTags()
{
return $this->tags;
}
}

View File

@ -0,0 +1,49 @@
{
"name": "illuminate/cache",
"description": "The Illuminate Cache package.",
"license": "MIT",
"homepage": "https://laravel.com",
"support": {
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"require": {
"php": "^8.1",
"illuminate/collections": "^10.0",
"illuminate/contracts": "^10.0",
"illuminate/macroable": "^10.0",
"illuminate/support": "^10.0"
},
"provide": {
"psr/simple-cache-implementation": "1.0|2.0|3.0"
},
"autoload": {
"psr-4": {
"Illuminate\\Cache\\": ""
}
},
"extra": {
"branch-alias": {
"dev-master": "10.x-dev"
}
},
"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 (^10.0).",
"illuminate/filesystem": "Required to use the file cache driver (^10.0).",
"illuminate/redis": "Required to use the redis cache driver (^10.0).",
"symfony/cache": "Required to use PSR-6 cache bridge (^6.2)."
},
"config": {
"sort-packages": true
},
"minimum-stability": "dev"
}