123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546 |
- <?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;
- }
- }
|