Repository: defstudio/telegraph (php)

Issue:
Storing Information
I can place user related information in there and also easily access stored elements for the current chatbot user, that i are interacting with. For example:

`$this->userStorage()->save(['key' => $value]);`
`$this->userStorage()->get('key');`
`$this->userStorage()->all();`
`$this->userStorage()->delete();`

Data can be stored in json files. With this tool, it is very convenient to store intermediate data related to a specific user.


Pull request:
implements storage system
This PR implements a data storage system, bounded to Bots, Chats and Users 


I'll paste here the draft from the documentation, to show how it's intended to work:

## Overview

### Bot Storage

[TelegraphBots](models/telegraph-bot) data storage is available through its `->storage()` method  

```php
/** @var \DefStudio\Telegraph\Models\TelegraphBot $telegraphBot */
$telegraphBot->storage()->set('last_chat', '1254824');

$lastChatId = $telegraphBot->storage()->get('last_chat'); // 1254824

$telegraphBot->forget('last_chat') //stored data is deleted
```

### Chat Storage

[TelegraphChats](models/telegraph-chat) data storage is available through its `->storage()` method  

```php
/** @var \DefStudio\Telegraph\Models\TelegraphChat $telegraphChat */
$telegraphChat->storage()->set('last_message', 'hello!');

$lastMessage = $telegraphChat->storage()->get('last_message'); // 1254824

$lastMessage->forget('last_message') //stored data is deleted
```

### User DTO Storage

[User DTO](webhooks/dto#defstudio-telegraph-dto-user) data storage is available through its `->storage()` method  

```php
/** @var \App\Models\User $user */
/** @var \DefStudio\Telegraph\DTO\User $telegraphUser */
$telegraphUser->storage()->set('laravel_user_id', $user->id);

$userId = $telegraphUser->storage()->get('laravel_user_id'); // 99

$userId->forget('laravel_user_id') //stored data is deleted
```

## Using different Storage drivers

Depending on the application needs, multiple drivers can be [configured](installation#configuration) and used. 

### Default driver

A default storage driver can be set in `telegraph.storage.default` config and it will be used when `->storage()` is invoked without arguments:

```php
// /config/telegraph.php

return [
    // ...
    
    'storage' => [
        'default' => 'file'
    ]
]
```

```php
$telegraphUser->storage() // \DefStudio\Telegraph\Storage\FileStorageDriver
```

### File Storage Driver

A file storage driver saves data in json files inside the disk/folder defined in Telegraph [configuration file](installation#configuration)

```php
// /config/telegraph.php

return [
    // ...
    
    'storage' => [
        'stores' => [
            'file' => [
                'driver' => \DefStudio\Telegraph\Storage\FileStorageDriver::class,
                'disk' => 'local',
                'root' => 'telegraph',
            ]
        ]       
    ]
]
```

with the above configuration this code will store the `name`
value in a `/[laravel local disk path]/telegraph/User/<telegraph_user_id>.json` file

```php
$telegraphUser->storage('file')->put('name', 'Daniele');
```


### Cache Storage Driver

A cache storage driver saves data using Laravel Cache system following Telegraph [configuration file](installation#configuration)

```php
// /config/telegraph.php

return [
    // ...
    
    'storage' => [
        'stores' => [
            'cache' => [
                'driver' => \DefStudio\Telegraph\Storage\CacheStorageDriver::class,
                'store' => 'redis',
                'key_prefix' => 'tgph',
            ]
        ]       
    ]
]
```

with the above configuration this code will store the `name`
value inside Laravel Redis Cache

```php
$telegraphUser->storage('cache')->put('name', 'Daniele');
```

closes #257 

Patch:
diff --git a/composer.json b/composer.json
index 9b57b80c..b86fdadc 100644
--- a/composer.json
+++ b/composer.json
@@ -23,22 +23,22 @@
     },
     "require-dev": {
         "ext-sqlite3": "*",
-        "defstudio/pest-plugin-laravel-expectations": "^1.10",
-        "friendsofphp/php-cs-fixer": "^v3.11",
-        "guzzlehttp/guzzle": "^7.5",
-        "nunomaduro/collision": "^5.10|^v6.3",
-        "nunomaduro/larastan": "^1.0",
-        "orchestra/testbench": "^6.22|^v7.7",
-        "pestphp/pest": "^1.22",
-        "pestphp/pest-plugin-laravel": "^1.2",
-        "pestphp/pest-plugin-mock": "^1.0",
-        "phpstan/extension-installer": "^1.1",
-        "phpstan/phpstan-deprecation-rules": "^1.0",
-        "phpstan/phpstan-phpunit": "^1.1",
-        "phpunit/phpunit": "^9.5",
-        "spatie/laravel-ray": "^1.30",
-        "spatie/pest-plugin-snapshots": "^1.1",
-        "spatie/pest-plugin-test-time": "^1.1",
+        "defstudio/pest-plugin-laravel-expectations": "^v1.10.3",
+        "friendsofphp/php-cs-fixer": "^v3.13.0",
+        "guzzlehttp/guzzle": "^7.5.0",
+        "nunomaduro/collision": "^v5.11.0|^v6.3.1",
+        "nunomaduro/larastan": "^1.0.4",
+        "orchestra/testbench": "^v6.25.1|^v7.13.0",
+        "pestphp/pest": "^v1.22.2",
+        "pestphp/pest-plugin-laravel": "^v1.3.0",
+        "pestphp/pest-plugin-mock": "^v1.0.3",
+        "phpstan/extension-installer": "^1.2.0",
+        "phpstan/phpstan-deprecation-rules": "^1.0.0",
+        "phpstan/phpstan-phpunit": "^1.2.2",
+        "phpunit/phpunit": "^9.5.26",
+        "spatie/laravel-ray": "^1.31.0",
+        "spatie/pest-plugin-snapshots": "^1.1.0",
+        "spatie/pest-plugin-test-time": "^1.1.1",
         "spatie/x-ray": "dev-main"
     },
     "autoload": {
diff --git a/config/telegraph.php b/config/telegraph.php
index cce5a7ed..2fe798f9 100644
--- a/config/telegraph.php
+++ b/config/telegraph.php
@@ -71,4 +71,50 @@ return [
         'bot' => DefStudio\Telegraph\Models\TelegraphBot::class,
         'chat' => DefStudio\Telegraph\Models\TelegraphChat::class,
     ],
+
+    'storage' => [
+        /**
+         * Default storage driver to be used for Telegraph data
+         */
+        'default' => 'file',
+
+        'stores' => [
+            'file' => [
+                /**
+                 * Telegraph cache driver to be used, must implement
+                 * DefStudio\Telegraph\Contracts\StorageDriver contract
+                 */
+                'driver' => \DefStudio\Telegraph\Storage\FileStorageDriver::class,
+
+                /*
+                 * Laravel Storage disk to use. See /config/filesystems/disks for available disks
+                 * If 'null', Laravel default store will be used,
+                 */
+                'disk' => 'local',
+
+                /**
+                 * Folder inside filesystem to be used as root for Telegraph storage
+                 */
+                'root' => 'telegraph',
+            ],
+            'cache' => [
+                /**
+                 * Telegraph cache driver to be used, must implement
+                 * DefStudio\Telegraph\Contracts\StorageDriver contract
+                 */
+                'driver' => \DefStudio\Telegraph\Storage\CacheStorageDriver::class,
+
+                /*
+                 * Laravel Cache store to use. See /config/cache/stores for available stores
+                 * If 'null', Laravel default store will be used,
+                 */
+                'store' => null,
+
+                /*
+                 * Prefix to be prepended to cache keys
+                 */
+                'key_prefix' => 'tgph',
+            ],
+        ],
+    ],
 ];
diff --git a/docs/content/en/installation.md b/docs/content/en/installation.md
index 46df3c0f..a8614f35 100644
--- a/docs/content/en/installation.md
+++ b/docs/content/en/installation.md
@@ -53,8 +53,8 @@ return [
      *
      * For reference, see https://defstudio.github.io/telegraph/webhooks/overview
      */
-    'webhook_handler' => EmptyWebhookHandler::class,
-    
+    'webhook_handler' => DefStudio\Telegraph\Handlers\EmptyWebhookHandler::class,
+
     /*
      * Sets a custom domain when registering a webhook. This will allow a loca telegram bot api server
      * to reach the webhook. Disabled by default
@@ -68,14 +68,13 @@ return [
      * webhook messages to logs
      */
     'debug_mode' => false,
-    
-    
+
     /*
      * If enabled, unknown webhook commands are
      * reported as exception in application logs
      */
     'report_unknown_webhook_commands' => true,
-    
+
     'security' => [
         /*
          * if enabled, allows callback queries from unregistered chats
@@ -88,8 +87,7 @@ return [
         'allow_messages_from_unknown_chats' => false,
 
         /*
-         * if enabled, store unknown chats as
-         * new TelegraphChat models
+         * if enabled, store unknown chats as new TelegraphChat models
          */
         'store_unknown_chats_in_db' => false,
     ],
@@ -105,6 +103,52 @@ return [
         'bot' => DefStudio\Telegraph\Models\TelegraphBot::class,
         'chat' => DefStudio\Telegraph\Models\TelegraphChat::class,
     ],
+
+    'storage' => [
+        /**
+         * Default storage driver to be used for Telegraph data
+         */
+        'default' => 'file',
+
+        'stores' => [
+            'file' => [
+                /**
+                 * Telegraph cache driver to be used, must implement
+                 * DefStudio\Telegraph\Contracts\StorageDriver contract
+                 */
+                'driver' => \DefStudio\Telegraph\Storage\FileStorageDriver::class,
+
+                /*
+                 * Laravel Storage disk to use. See /config/filesystems/disks for available disks
+                 * If 'null', Laravel default store will be used,
+                 */
+                'disk' => 'local',
+
+                /**
+                 * Folder inside filesystem to be used as root for Telegraph storage
+                 */
+                'root' => 'telegraph',
+            ],
+            'cache' => [
+                /**
+                 * Telegraph cache driver to be used, must implement
+                 * DefStudio\Telegraph\Contracts\StorageDriver contract
+                 */
+                'driver' => \DefStudio\Telegraph\Storage\CacheStorageDriver::class,
+
+                /*
+                 * Laravel Cache store to use. See /config/cache/stores for available stores
+                 * If 'null', Laravel default store will be used,
+                 */
+                'store' => null,
+
+                /*
+                 * Prefix to be prepended to cache keys
+                 */
+                'key_prefix' => 'tgph',
+            ],
+        ],
+    ],
 ];
 ```
 
diff --git a/docs/content/en/storage/entities_storage.md b/docs/content/en/storage/entities_storage.md
new file mode 100644
index 00000000..3f2e89dc
--- /dev/null
+++ b/docs/content/en/storage/entities_storage.md
@@ -0,0 +1,135 @@
+---
+title: 'Telegraph Entities Storage'
+menuTitle: 'Entities Storage'
+description: ''
+category: 'Storage'
+fullscreen: false 
+position: 70
+---
+
+**Telegraph** offers a quick, multi driver, data storage solution to save and retrieve information about Bots, Chats and Users. A fine grained configuration is available in the `storage` section of Telegraph [configuration file](installation#configuration) 
+
+## Overview
+
+### Bot Storage
+
+[TelegraphBots](models/telegraph-bot) data storage is available through its `->storage()` method  
+
+```php
+/** @var \DefStudio\Telegraph\Models\TelegraphBot $telegraphBot */
+$telegraphBot->storage()->set('last_chat', '1254824');
+
+$lastChatId = $telegraphBot->storage()->get('last_chat'); // 1254824
+
+$telegraphBot->forget('last_chat') //stored data is deleted
+```
+
+### Chat Storage
+
+[TelegraphChats](models/telegraph-chat) data storage is available through its `->storage()` method  
+
+```php
+/** @var \DefStudio\Telegraph\Models\TelegraphChat $telegraphChat */
+$telegraphChat->storage()->set('last_message', 'hello!');
+
+$lastMessage = $telegraphChat->storage()->get('last_message'); // 1254824
+
+$lastMessage->forget('last_message') //stored data is deleted
+```
+
+### User DTO Storage
+
+[User DTO](webhooks/dto#defstudio-telegraph-dto-user) data storage is available through its `->storage()` method  
+
+```php
+/** @var \App\Models\User $user */
+/** @var \DefStudio\Telegraph\DTO\User $telegraphUser */
+$telegraphUser->storage()->set('laravel_user_id', $user->id);
+
+$userId = $telegraphUser->storage()->get('laravel_user_id'); // 99
+
+$userId->forget('laravel_user_id') //stored data is deleted
+```
+
+## Using different Storage drivers
+
+Depending on the application needs, multiple drivers can be [configured](installation#configuration) and used. 
+
+### Default driver
+
+A default storage driver can be set in `telegraph.storage.default` config and it will be used when `->storage()` is invoked without arguments:
+
+```php
+// /config/telegraph.php
+
+return [
+    // ...
+    
+    'storage' => [
+        'default' => 'file'
+    ]
+]
+```
+
+```php
+$telegraphUser->storage() // \DefStudio\Telegraph\Storage\FileStorageDriver
+```
+
+### File Storage Driver
+
+A file storage driver saves data in json files inside the disk/folder defined in Telegraph [configuration file](installation#configuration)
+
+```php
+// /config/telegraph.php
+
+return [
+    // ...
+    
+    'storage' => [
+        'stores' => [
+            'file' => [
+                'driver' => \DefStudio\Telegraph\Storage\FileStorageDriver::class,
+                'disk' => 'local',
+                'root' => 'telegraph',
+            ]
+        ]       
+    ]
+]
+```
+
+with the above configuration this code will store the `name`
+value in a `/[laravel local disk path]/telegraph/User/<telegraph_user_id>.json` file
+
+```php
+$telegraphUser->storage('file')->put('name', 'Daniele');
+```
+
+
+### Cache Storage Driver
+
+A cache storage driver saves data using Laravel Cache system following Telegraph [configuration file](installation#configuration)
+
+```php
+// /config/telegraph.php
+
+return [
+    // ...
+    
+    'storage' => [
+        'stores' => [
+            'cache' => [
+                'driver' => \DefStudio\Telegraph\Storage\CacheStorageDriver::class,
+                'store' => 'redis',
+                'key_prefix' => 'tgph',
+            ]
+        ]       
+    ]
+]
+```
+
+with the above configuration this code will store the `name`
+value inside Laravel Redis Cache
+
+```php
+$telegraphUser->storage('cache')->put('name', 'Daniele');
+```
diff --git a/src/Concerns/HasStorage.php b/src/Concerns/HasStorage.php
new file mode 100644
index 00000000..1b13b2ab
--- /dev/null
+++ b/src/Concerns/HasStorage.php
@@ -0,0 +1,32 @@
+<?php
+
+/** @noinspection PhpUnhandledExceptionInspection */
+
+namespace DefStudio\Telegraph\Concerns;
+
+use DefStudio\Telegraph\Contracts\StorageDriver;
+use DefStudio\Telegraph\Exceptions\StorageException;
+
+trait HasStorage
+{
+    public function storage(string $driver = null): StorageDriver
+    {
+        $driver ??= config('telegraph.storage.default');
+
+        /** @var string|null $driver */
+        if ($driver === null) {
+            throw StorageException::noDefaultDriver();
+        }
+
+
+        $config = config("telegraph.storage.stores.$driver");
+
+
+        if ($config === null) {
+            throw StorageException::driverNotFound($driver);
+        }
+
+        /** @phpstan-ignore-next-line  */
+        return app()->make($config['driver'], ['itemClass' => static::class, 'itemKey' => $this->storageKey(), 'configuration' => $config]);
+    }
+}
diff --git a/src/Contracts/Storable.php b/src/Contracts/Storable.php
new file mode 100644
index 00000000..901c483f
--- /dev/null
+++ b/src/Contracts/Storable.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace DefStudio\Telegraph\Contracts;
+
+interface Storable
+{
+    public function storage(string $driver = null): StorageDriver;
+
+    public function storageKey(): string|int;
+}
diff --git a/src/Contracts/StorageDriver.php b/src/Contracts/StorageDriver.php
new file mode 100644
index 00000000..f525bddb
--- /dev/null
+++ b/src/Contracts/StorageDriver.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace DefStudio\Telegraph\Contracts;
+
+interface StorageDriver
+{
+    /**
+     * @param string[] $configuration
+     */
+    public function __construct(string $itemClass, string $itemKey, array $configuration);
+
+    public function set(string $key, mixed $value): static;
+
+    public function get(string $key, mixed $default = null): mixed;
+
+    public function forget(string $key): static;
+}
diff --git a/src/DTO/User.php b/src/DTO/User.php
index 684f756d..79406f23 100644
--- a/src/DTO/User.php
+++ b/src/DTO/User.php
@@ -4,10 +4,14 @@
 
 namespace DefStudio\Telegraph\DTO;
 
+use DefStudio\Telegraph\Concerns\HasStorage;
+use DefStudio\Telegraph\Contracts\Storable;
 use Illuminate\Contracts\Support\Arrayable;
 
-class User implements Arrayable
+class User implements Arrayable, Storable
 {
+    use HasStorage;
+
     private int $id;
     private bool $isBot;
     private string $firstName;
@@ -35,6 +39,11 @@ class User implements Arrayable
         return $user;
     }
 
+    public function storageKey(): string|int
+    {
+        return $this->id;
+    }
+
     public function id(): int
     {
         return $this->id;
@@ -63,11 +72,11 @@ class User implements Arrayable
     public function toArray(): array
     {
         return array_filter([
-           'id' => $this->id,
-           'is_bot' => $this->isBot,
-           'first_name' => $this->firstName,
-           'last_name' => $this->lastName,
-           'username' => $this->username,
+            'id' => $this->id,
+            'is_bot' => $this->isBot,
+            'first_name' => $this->firstName,
+            'last_name' => $this->lastName,
+            'username' => $this->username,
         ]);
     }
 }
diff --git a/src/Exceptions/StorageException.php b/src/Exceptions/StorageException.php
new file mode 100644
index 00000000..7321a8ef
--- /dev/null
+++ b/src/Exceptions/StorageException.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace DefStudio\Telegraph\Exceptions;
+
+use Exception;
+
+class StorageException extends Exception
+{
+    public static function noDefaultDriver(): StorageException
+    {
+        return new self("No default driver defined in telegraph.storage.default config");
+    }
+
+    public static function driverNotFound(string $driver): StorageException
+    {
+        return new self("No [$driver] driver configuration defined telegraph.storage.stores.$driver config");
+    }
+}
diff --git a/src/Models/TelegraphBot.php b/src/Models/TelegraphBot.php
index a82c4d44..d8c5f64a 100644
--- a/src/Models/TelegraphBot.php
+++ b/src/Models/TelegraphBot.php
@@ -7,7 +7,9 @@
 
 namespace DefStudio\Telegraph\Models;
 
+use DefStudio\Telegraph\Concerns\HasStorage;
 use DefStudio\Telegraph\Contracts\Downloadable;
+use DefStudio\Telegraph\Contracts\Storable;
 use DefStudio\Telegraph\Database\Factories\TelegraphBotFactory;
 use DefStudio\Telegraph\DTO\InlineQueryResult;
 use DefStudio\Telegraph\DTO\TelegramUpdate;
@@ -31,9 +33,10 @@ use Illuminate\Support\Carbon;
  * @property Carbon $updated_at
  * @property-read Collection<TelegraphChat> $chats
  */
-class TelegraphBot extends Model
+class TelegraphBot extends Model implements Storable
 {
     use HasFactory;
+    use HasStorage;
 
     protected $fillable = [
         'token',
@@ -55,6 +58,11 @@ class TelegraphBot extends Model
         });
     }
 
+    public function storageKey(): string|int
+    {
+        return $this->id;
+    }
+
     public function getRouteKeyName(): string
     {
         return 'token';
diff --git a/src/Models/TelegraphChat.php b/src/Models/TelegraphChat.php
index 64662e4d..0c977401 100644
--- a/src/Models/TelegraphChat.php
+++ b/src/Models/TelegraphChat.php
@@ -6,6 +6,8 @@
 
 namespace DefStudio\Telegraph\Models;
 
+use DefStudio\Telegraph\Concerns\HasStorage;
+use DefStudio\Telegraph\Contracts\Storable;
 use DefStudio\Telegraph\Database\Factories\TelegraphChatFactory;
 use DefStudio\Telegraph\Exceptions\TelegraphException;
 use DefStudio\Telegraph\Facades\Telegraph as TelegraphFacade;
@@ -28,9 +30,10 @@ use Illuminate\Support\Carbon;
  * @property Carbon $updated_at
  * @property-read TelegraphBot $bot
  */
-class TelegraphChat extends Model
+class TelegraphChat extends Model implements Storable
 {
     use HasFactory;
+    use HasStorage;
 
     protected $fillable = [
         'chat_id',
@@ -52,6 +55,11 @@ class TelegraphChat extends Model
         });
     }
 
+    public function storageKey(): string|int
+    {
+        return $this->id;
+    }
+
     public function bot(): BelongsTo
     {
         /** @phpstan-ignore-next-line */
diff --git a/src/Storage/CacheStorageDriver.php b/src/Storage/CacheStorageDriver.php
new file mode 100644
index 00000000..15f01043
--- /dev/null
+++ b/src/Storage/CacheStorageDriver.php
@@ -0,0 +1,47 @@
+<?php
+
+/** @noinspection PhpUnhandledExceptionInspection */
+
+namespace DefStudio\Telegraph\Storage;
+
+use DefStudio\Telegraph\Contracts\StorageDriver;
+use Illuminate\Contracts\Cache\Repository;
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Str;
+
+class CacheStorageDriver implements StorageDriver
+{
+    private string $key;
+    private Repository $cache;
+
+    /**
+     * @param string[] $configuration
+     */
+    public function __construct(string $itemClass, string $itemKey, array $configuration)
+    {
+        $this->key = (string) Str::of($configuration['key_prefix'])
+            ->append("_", class_basename($itemClass))
+            ->append("_", $itemKey);
+
+        $this->cache = Cache::store($configuration['store'] ?? null);
+    }
+
+    public function set(string $key, mixed $value): static
+    {
+        $this->cache->set("{$this->key}_$key", $value);
+
+        return $this;
+    }
+
+    public function get(string $key, mixed $default = null): mixed
+    {
+        return $this->cache->get("{$this->key}_$key", $default);
+    }
+
+    public function forget(string $key): static
+    {
+        $this->cache->forget("{$this->key}_$key");
+
+        return $this;
+    }
+}
diff --git a/src/Storage/FileStorageDriver.php b/src/Storage/FileStorageDriver.php
new file mode 100644
index 00000000..52ac9dc0
--- /dev/null
+++ b/src/Storage/FileStorageDriver.php
@@ -0,0 +1,81 @@
+<?php
+
+namespace DefStudio\Telegraph\Storage;
+
+use DefStudio\Telegraph\Contracts\StorageDriver;
+use Illuminate\Contracts\Filesystem\FileNotFoundException;
+use Illuminate\Contracts\Filesystem\Filesystem;
+use Illuminate\Support\Arr;
+use Illuminate\Support\Facades\Storage;
+use Illuminate\Support\Str;
+use JsonException;
+
+class FileStorageDriver implements StorageDriver
+{
+    private string $file;
+
+    private Filesystem $disk;
+
+    /**
+     * @param string[] $configuration
+     */
+    public function __construct(string $itemClass, string $itemKey, array $configuration)
+    {
+        $this->file = (string) Str::of($configuration['root'])
+            ->append('/', class_basename($itemClass))
+            ->append("/", $itemKey, '.json');
+
+        $this->disk = Storage::disk($configuration['disk'] ?? null);
+    }
+
+    /**
+     * @return array<array-key, mixed>
+     */
+    private function getData(): array
+    {
+        try {
+            $json = $this->disk->get($this->file);
+
+            /** @phpstan-ignore-next-line */
+            return rescue(fn () => json_decode($json, true, flags: JSON_THROW_ON_ERROR), []);
+        } catch (FileNotFoundException|JsonException) {
+            return [];
+        }
+    }
+
+    /**
+     * @param array<array-key, mixed> $data
+     */
+    private function storeData(array $data): void
+    {
+        $json = json_encode($data);
+
+        /** @phpstan-ignore-next-line */
+        $this->disk->put($this->file, $json);
+    }
+
+    public function set(string $key, mixed $value): static
+    {
+        $data = $this->getData();
+        Arr::set($data, $key, $value);
+        $this->storeData($data);
+
+        return $this;
+    }
+
+    public function get(string $key, mixed $default = null): mixed
+    {
+        $data = $this->getData();
+
+        return Arr::get($data, $key, $default);
+    }
+
+    public function forget(string $key): static
+    {
+        $data = $this->getData();
+        Arr::forget($data, $key);
+        $this->storeData($data);
+
+        return $this;
+    }
+}


Test patch:
diff --git a/docs/content/en/testing/assertions.md b/docs/content/en/testing/assertions.md
index 5c6df0e8..bf02b00b 100644
--- a/docs/content/en/testing/assertions.md
+++ b/docs/content/en/testing/assertions.md
@@ -4,7 +4,7 @@ menuTitle: 'Assertions'
 description: ''
 category: 'Testing'
 fullscreen: false
-position: 71
+position: 101
 ---
 
 The `::fake()` facade helper enables a number of assertions for checking its activity
diff --git a/docs/content/en/testing/fake.md b/docs/content/en/testing/fake.md
index b5d5dd82..51d59835 100644
--- a/docs/content/en/testing/fake.md
+++ b/docs/content/en/testing/fake.md
@@ -4,7 +4,7 @@ menuTitle: 'Fake'
 description: ''
 category: 'Testing'
 fullscreen: false 
-position: 70
+position: 100
 ---
 
 In order to avoid sending real messages to Telegram when testing, the `Telegraph` facade offers a `::fake()` method to start registering sent messages internally instead of sending them to Telegram APIs.
diff --git a/tests/Unit/Concerns/HasStorageTest.php b/tests/Unit/Concerns/HasStorageTest.php
new file mode 100644
index 00000000..e9585657
--- /dev/null
+++ b/tests/Unit/Concerns/HasStorageTest.php
@@ -0,0 +1,64 @@
+<?php
+
+use DefStudio\Telegraph\Concerns\HasStorage;
+use DefStudio\Telegraph\Contracts\Storable;
+use DefStudio\Telegraph\Exceptions\StorageException;
+use DefStudio\Telegraph\Storage\FileStorageDriver;
+
+it('can return default store driver', function () {
+    $class = new class () implements Storable {
+        use HasStorage;
+
+        public function storageKey(): string
+        {
+            return 'foo';
+        }
+    };
+
+    expect($class->storage())->toBeInstanceOf(FileStorageDriver::class);
+});
+
+it('can return custom store drivers', function (string $store) {
+    $class = new class () implements Storable {
+        use HasStorage;
+
+        public function storageKey(): string
+        {
+            return 'foo';
+        }
+    };
+
+    expect($class->storage($store))->toBeInstanceOf(config("telegraph.storage.stores.$store.driver"));
+})->with([
+    'file',
+    'cache',
+]);
+
+it('requires a default store', function () {
+    config()->set('telegraph.storage.default');
+    $class = new class () implements Storable {
+        use HasStorage;
+
+        public function storageKey(): string
+        {
+            return 'foo';
+        }
+    };
+
+    $class->storage();
+})->throws(StorageException::class, 'No default driver defined in telegraph.storage.default config');
+
+
+it('requires a driver configuration to exist', function () {
+    config()->set('telegraph.storage.stores.file');
+    $class = new class () implements Storable {
+        use HasStorage;
+
+        public function storageKey(): string
+        {
+            return 'foo';
+        }
+    };
+
+    $class->storage();
+})->throws(StorageException::class, 'No [file] driver configuration defined telegraph.storage.stores.file config');
diff --git a/tests/Unit/Storage/CacheStoreDriverTest.php b/tests/Unit/Storage/CacheStoreDriverTest.php
new file mode 100644
index 00000000..c0804cea
--- /dev/null
+++ b/tests/Unit/Storage/CacheStoreDriverTest.php
@@ -0,0 +1,35 @@
+<?php
+
+use DefStudio\Telegraph\Concerns\HasStorage;
+use DefStudio\Telegraph\Contracts\Storable;
+use Illuminate\Support\Str;
+
+it('can store and retrieve data data', function (string $key, mixed $value) {
+    $class = new class () implements Storable {
+        use HasStorage;
+
+        public function storageKey(): string
+        {
+            return 'foo';
+        }
+    };
+
+    config()->set('cache.default', 'file');
+    $class->storage('cache')->set($key, $value);
+
+    $cacheKey = Str::of('tgph')
+        ->append("_", class_basename($class::class))
+        ->append("_", 'foo');
+
+    expect(Cache::get("{$cacheKey}_$key"))->toBe($value);
+    expect($class->storage('cache')->get($key))->toBe($value);
+
+    $class->storage('cache')->forget($key);
+    expect($class->storage('cache')->get($key))->toBeNull();
+})->with([
+    'string' => ['key' => 'foo.bar', 'value' => 'baz'],
+    'int' => ['key' => 'foo.bar', 'value' => 1],
+    'float' => ['key' => 'foo.bar', 'value' => 1.994],
+    'bool' => ['key' => 'foo.bar', 'value' => true],
+    'array' => ['key' => 'foo.bar', 'value' => ['baz', 'qux']],
+]);
diff --git a/tests/Unit/Storage/FileStoreDriverTest.php b/tests/Unit/Storage/FileStoreDriverTest.php
new file mode 100644
index 00000000..6e30e48f
--- /dev/null
+++ b/tests/Unit/Storage/FileStoreDriverTest.php
@@ -0,0 +1,42 @@
+<?php
+
+use DefStudio\Telegraph\Concerns\HasStorage;
+use DefStudio\Telegraph\Contracts\Storable;
+use Illuminate\Support\Str;
+
+beforeEach(function () {
+    Storage::fake();
+});
+
+it('can store and retrieve data data', function (string $key, mixed $value) {
+    $class = new class () implements Storable {
+        use HasStorage;
+
+        public function storageKey(): string
+        {
+            return 'foo';
+        }
+    };
+
+    $class->storage('file')->set($key, $value);
+
+    $file = Str::of('telegraph')
+        ->append('/', class_basename($class::class))
+        ->append("/", 'foo', '.json');
+
+    expect(Storage::exists($file))->toBeTrue();
+
+    $data = json_decode(Storage::get($file));
+    expect(data_get($data, $key))->toBe($value);
+
+    expect($class->storage('file')->get($key))->toBe($value);
+
+    $class->storage('file')->forget($key);
+    expect($class->storage('file')->get($key))->toBeNull();
+})->with([
+    'string' => ['key' => 'foo.bar', 'value' => 'baz'],
+    'int' => ['key' => 'foo.bar', 'value' => 1],
+    'float' => ['key' => 'foo.bar', 'value' => 1.994],
+    'bool' => ['key' => 'foo.bar', 'value' => true],
+    'array' => ['key' => 'foo.bar', 'value' => ['baz', 'qux']],
+]);