diff --git a/.github/workflows/run-tests-l11.yml b/.github/workflows/run-tests-l11.yml
index 367f943..119319e 100644
--- a/.github/workflows/run-tests-l11.yml
+++ b/.github/workflows/run-tests-l11.yml
@@ -11,6 +11,18 @@ jobs:
tests:
runs-on: ubuntu-latest
+
+ services:
+ mongodb:
+ image: mongo:7
+ ports:
+ - 27017:27017
+ options: >-
+ --health-cmd="mongosh --quiet --eval 'db.runCommand({ping:1})'"
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 3
+
strategy:
fail-fast: false
matrix:
@@ -18,22 +30,39 @@ jobs:
laravel: [ 11.* ]
include:
- laravel: 11.*
- testbench: 8.*
+ testbench: 9.*
name: P${{ matrix.php }} - L${{ matrix.laravel }}
steps:
- name: Checkout code
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
- extensions: pdo, sqlite, pdo_sqlite
+ extensions: pdo, sqlite, pdo_sqlite, mongodb
+ coverage: none
+
+ - name: Get composer cache directory
+ id: composer-cache
+ run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
+
+ - name: Cache dependencies
+ uses: actions/cache@v3
+ with:
+ path: ${{ steps.composer-cache.outputs.dir }}
+ key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
+ restore-keys: ${{ runner.os }}-composer-
- name: Install Dependencies
- run: composer install
+ run: composer install --prefer-dist --no-interaction --no-progress
+
- name: Execute tests
run: vendor/bin/phpunit
+ env:
+ MONGODB_HOST: 127.0.0.1
+ MONGODB_PORT: 27017
+ MONGODB_DATABASE: laravel_mongodb_cache_test
diff --git a/.gitignore b/.gitignore
index 0794651..c2d7942 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,3 @@
/vendor/
composer.lock
-.phpunit.result.cache
+.phpunit.cache
diff --git a/.phprc b/.phprc
index 6e3833a..a5a984f 100644
--- a/.phprc
+++ b/.phprc
@@ -1 +1 @@
-php8.1
+php8.2
diff --git a/README.md b/README.md
index 13015cf..c5f0f9e 100644
--- a/README.md
+++ b/README.md
@@ -67,6 +67,59 @@ Advantages
php artisan mongodb:cache:dropindex
+Testing
+-------
+
+This package includes tests that interact with a real MongoDB database to verify the functionality of the cache driver. The tests require a MongoDB instance to run successfully.
+
+To run the tests:
+
+1. Make sure you have MongoDB installed and running on your local machine
+2. The test configuration is set in `phpunit.xml`:
+
+```xml
+
+
+
+
+
+
+
+```
+
+3. Run the tests with:
+
+```
+composer test
+```
+
+or
+
+```
+vendor/bin/phpunit
+```
+
+### Test Structure
+
+The test suite is organized into multiple files to test various aspects of the MongoDB cache driver:
+
+- `StoreTest.php`: Tests basic Store class functionality (get, put, forget, flush)
+- `AdvancedCacheFeaturesTest.php`: Tests advanced Store features like increment/decrement, forever storage, and handling arrays/objects
+- `TaggedCacheTest.php`: Tests tagged cache functionality
+- `LaravelIntegrationTest.php`: Tests integration with Laravel's Cache facade
+
+Some functionality (like increment/decrement, forever storage) is intentionally tested in multiple contexts:
+1. At the low-level Store implementation
+2. Through Laravel's Cache facade
+3. With tagged cache operations
+
+This multi-layered approach ensures that all feature functionality works correctly at all levels of integration.
+
+GitHub Actions
+-------------
+
+The package includes GitHub Actions workflows that automatically run tests against a MongoDB service. The MongoDB service is started as part of the CI workflow, ensuring tests are executed in an environment with a real MongoDB database.
+
Warning
-------
diff --git a/composer.json b/composer.json
index c1768ad..9c9a644 100644
--- a/composer.json
+++ b/composer.json
@@ -2,11 +2,11 @@
"name": "1ff/laravel-mongodb-cache",
"description": "A mongodb cache driver for laravel",
"type": "library",
- "version": "7.0.0",
"require": {
"php": "^8.2|^8.3",
"illuminate/cache": "^11.0",
- "mongodb/laravel-mongodb": "^4.1"
+ "mongodb/laravel-mongodb": "^5.0",
+ "ext-mongodb": "*"
},
"require-dev": {
"orchestra/testbench": "^9.0"
@@ -39,5 +39,8 @@
"ForFit\\Mongodb\\Cache\\ServiceProvider"
]
}
+ },
+ "scripts": {
+ "test": "vendor/bin/phpunit"
}
}
diff --git a/phpunit.xml b/phpunit.xml
index 5733f58..b374467 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -1,17 +1,8 @@
-
+
tests
@@ -19,5 +10,10 @@
+
+
+
+
+
diff --git a/src/Console/Commands/MongodbCacheDropIndex.php b/src/Console/Commands/MongodbCacheDropIndex.php
index 5db9359..b581c89 100644
--- a/src/Console/Commands/MongodbCacheDropIndex.php
+++ b/src/Console/Commands/MongodbCacheDropIndex.php
@@ -4,7 +4,7 @@
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
-use \MongoDB\Driver\ReadPreference;
+use MongoDB\Driver\ReadPreference;
/**
* Drop the indexes created by MongodbCacheIndex
@@ -12,34 +12,22 @@
class MongodbCacheDropIndex extends Command
{
- /**
- * The name and signature of the console command.
- *
- * @var string
- */
+ /** The name and signature of the console command. */
protected $signature = 'mongodb:cache:dropindex {index}';
- /**
- * The console command description.
- *
- * @var string
- */
+ /** The console command description. */
protected $description = 'Drops the passed index from the mongodb `cache` collection';
- /**
- * Execute the console command.
- *
- * @return mixed
- */
- public function handle()
+ /** Execute the console command. */
+ public function handle(): void
{
$cacheCollectionName = config('cache')['stores']['mongodb']['table'];
- DB::connection('mongodb')->getMongoDB()->command([
+ DB::connection('mongodb')->getDatabase()->command([
'dropIndexes' => $cacheCollectionName,
'index' => $this->argument('index'),
], [
- 'readPreference' => new ReadPreference(ReadPreference::RP_PRIMARY)
+ 'readPreference' => new ReadPreference(ReadPreference::PRIMARY)
]);
}
}
diff --git a/src/Console/Commands/MongodbCacheIndex.php b/src/Console/Commands/MongodbCacheIndex.php
index a2f7b43..7dfb698 100644
--- a/src/Console/Commands/MongodbCacheIndex.php
+++ b/src/Console/Commands/MongodbCacheIndex.php
@@ -4,7 +4,7 @@
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
-use \MongoDB\Driver\ReadPreference;
+use MongoDB\Driver\ReadPreference;
/**
* Create indexes for the cache collection
@@ -12,30 +12,18 @@
class MongodbCacheIndex extends Command
{
- /**
- * The name and signature of the console command.
- *
- * @var string
- */
+ /** The name and signature of the console command. */
protected $signature = 'mongodb:cache:index';
- /**
- * The console command description.
- *
- * @var string
- */
+ /** The console command description. */
protected $description = 'Create indexes on the mongodb `cache` collection';
- /**
- * Execute the console command.
- *
- * @return mixed
- */
- public function handle()
+ /** Execute the console command. */
+ public function handle(): void
{
$cacheCollectionName = config('cache')['stores']['mongodb']['table'];
- DB::connection('mongodb')->getMongoDB()->command([
+ DB::connection('mongodb')->getDatabase()->command([
'createIndexes' => $cacheCollectionName,
'indexes' => [
[
@@ -52,7 +40,7 @@ public function handle()
]
]
], [
- 'readPreference' => new ReadPreference(ReadPreference::RP_PRIMARY)
+ 'readPreference' => new ReadPreference(ReadPreference::PRIMARY)
]);
}
}
diff --git a/src/Console/Commands/MongodbCacheIndexTags.php b/src/Console/Commands/MongodbCacheIndexTags.php
index 1a2de24..96ebe28 100644
--- a/src/Console/Commands/MongodbCacheIndexTags.php
+++ b/src/Console/Commands/MongodbCacheIndexTags.php
@@ -4,7 +4,7 @@
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
-use \MongoDB\Driver\ReadPreference;
+use MongoDB\Driver\ReadPreference;
/**
* Create indexes for the cache collection
@@ -12,30 +12,18 @@
class MongodbCacheIndexTags extends Command
{
- /**
- * The name and signature of the console command.
- *
- * @var string
- */
+ /** The name and signature of the console command. */
protected $signature = 'mongodb:cache:index_tags';
- /**
- * The console command description.
- *
- * @var string
- */
+ /** The console command description. */
protected $description = 'Create indexes on the tags column of mongodb `cache` collection';
- /**
- * Execute the console command.
- *
- * @return mixed
- */
- public function handle()
+ /** Execute the console command.*/
+ public function handle(): void
{
$cacheCollectionName = config('cache')['stores']['mongodb']['table'];
- DB::connection('mongodb')->getMongoDB()->command([
+ DB::connection('mongodb')->getDatabase()->command([
'createIndexes' => $cacheCollectionName,
'indexes' => [
[
@@ -45,7 +33,7 @@ public function handle()
],
]
], [
- 'readPreference' => new ReadPreference(ReadPreference::RP_PRIMARY)
+ 'readPreference' => new ReadPreference(ReadPreference::PRIMARY)
]);
}
}
diff --git a/src/MongoTaggedCache.php b/src/MongoTaggedCache.php
index 7c1d048..bfa0644 100644
--- a/src/MongoTaggedCache.php
+++ b/src/MongoTaggedCache.php
@@ -2,16 +2,16 @@
namespace ForFit\Mongodb\Cache;
+use Illuminate\Cache\Events\KeyWritten;
use Illuminate\Cache\Repository;
use Illuminate\Contracts\Cache\Store;
-use Illuminate\Cache\Events\KeyWritten;
class MongoTaggedCache extends Repository
{
- protected $tags;
+ protected array $tags;
/**
- * @param \Illuminate\Contracts\Cache\Store $store
+ * @param Store $store
* @param array $tags
*/
public function __construct(Store $store, array $tags = [])
@@ -27,12 +27,12 @@ public function __construct(Store $store, array $tags = [])
* @param string $key
* @param mixed $value
* @param \DateTimeInterface|\DateInterval|float|int $ttl
- * @return void
+ * @return bool|null
*/
public function put($key, $value, $ttl = null)
{
if (is_array($key)) {
- return $this->putMany($key, $value);
+ $this->putMany($key, $value);
}
$seconds = $this->getSeconds(is_null($ttl) ? 315360000 : $ttl);
@@ -45,19 +45,17 @@ public function put($key, $value, $ttl = null)
}
return $result;
- } else {
- return $this->forget($key);
}
+
+ return $this->forget($key);
}
/**
* Saves array of key value pairs to the cache
*
- * @param array $values
* @param \DateTimeInterface|\DateInterval|float|int $ttl
- * @return void
*/
- public function putMany(array $values, $ttl = null)
+ public function putMany(array $values, $ttl = null): void
{
foreach ($values as $key => $value) {
$this->put($key, $value, $ttl);
@@ -66,11 +64,9 @@ public function putMany(array $values, $ttl = null)
/**
* Flushes the cache for the given tags
- *
- * @return void
*/
- public function flush()
+ public function flush(): void
{
- return $this->store->flushByTags($this->tags);
+ $this->store->flushByTags($this->tags);
}
}
diff --git a/src/Store.php b/src/Store.php
index 5d758f3..71ec693 100644
--- a/src/Store.php
+++ b/src/Store.php
@@ -3,13 +3,13 @@
namespace ForFit\Mongodb\Cache;
use Closure;
-use Illuminate\Support\InteractsWithTime;
use Illuminate\Cache\RetrievesMultipleKeys;
-use Illuminate\Database\ConnectionInterface;
use Illuminate\Contracts\Cache\Store as StoreInterface;
-use Jenssegers\Mongodb\Query\Builder;
+use Illuminate\Database\ConnectionInterface;
+use Illuminate\Support\InteractsWithTime;
use MongoDB\BSON\UTCDateTime;
use MongoDB\Driver\Exception\BulkWriteException;
+use MongoDB\Laravel\Query\Builder;
class Store implements StoreInterface
{
@@ -59,7 +59,7 @@ public function get($key)
{
$cacheData = $this->table()->where('key', $this->getKeyWithPrefix($key))->first();
- return $cacheData ? unserialize($cacheData['value']) : null;
+ return $cacheData ? unserialize($cacheData->value) : null;
}
/**
@@ -107,7 +107,7 @@ public function decrement($key, $value = 1)
/**
* @inheritDoc
*/
- public function forever($key, $value)
+ public function forever($key, $value): bool
{
return $this->put($key, $value, 315360000);
}
@@ -115,7 +115,7 @@ public function forever($key, $value)
/**
* @inheritDoc
*/
- public function forget($key)
+ public function forget($key): bool
{
$this->table()->where('key', '=', $this->getPrefix() . $key)->delete();
@@ -125,7 +125,7 @@ public function forget($key)
/**
* @inheritDoc
*/
- public function flush()
+ public function flush(): bool
{
$this->table()->delete();
@@ -135,29 +135,23 @@ public function flush()
/**
* @inheritDoc
*/
- public function getPrefix()
+ public function getPrefix(): string
{
return $this->prefix;
}
/**
* Sets the tags to be used
- *
- * @param array $tags
- * @return MongoTaggedCache
*/
- public function tags(array $tags)
+ public function tags(array $tags): MongoTaggedCache
{
return new MongoTaggedCache($this, $tags);
}
/**
* Deletes all records with the given tag
- *
- * @param array $tags
- * @return void
*/
- public function flushByTags(array $tags)
+ public function flushByTags(array $tags): void
{
foreach ($tags as $tag) {
$this->table()->where('tags', $tag)->delete();
@@ -166,19 +160,16 @@ public function flushByTags(array $tags)
/**
* Retrieve an item's expiration time from the cache by key.
- *
- * @param string $key
- * @return null|float|int
*/
- public function getExpiration($key)
+ public function getExpiration($key): ?float
{
$cacheData = $this->table()->where('key', $this->getKeyWithPrefix($key))->first();
- if (empty($cacheData['expiration'])) {
+ if (empty($cacheData->expiration)) {
return null;
}
- $expirationSeconds = $cacheData['expiration']->toDateTime()->getTimestamp();
+ $expirationSeconds = $cacheData->expiration->toDateTime()->getTimestamp();
return round($expirationSeconds - $this->currentTime());
}
@@ -199,7 +190,7 @@ protected function table()
* @param string $key
* @return string
*/
- protected function getKeyWithPrefix(string $key)
+ protected function getKeyWithPrefix(string $key): string
{
return $this->getPrefix() . $key;
}
@@ -210,7 +201,7 @@ protected function getKeyWithPrefix(string $key)
* @param string $key
* @param int $value
* @param Closure $callback
- * @return int|bool
+ * @return false|mixed
*/
protected function incrementOrDecrement($key, $value, Closure $callback)
{
diff --git a/tests/AdvancedCacheFeaturesTest.php b/tests/AdvancedCacheFeaturesTest.php
new file mode 100644
index 0000000..6a56b76
--- /dev/null
+++ b/tests/AdvancedCacheFeaturesTest.php
@@ -0,0 +1,168 @@
+store = new Store(
+ DB::connection('mongodb'),
+ $this->table()
+ );
+
+ // Freeze time for consistent testing
+ Carbon::setTestNow(now());
+ }
+
+ protected function tearDown(): void
+ {
+ parent::tearDown();
+ Carbon::setTestNow(); // Clear test now
+ }
+
+ #[Test]
+ public function it_can_increment_numeric_values(): void
+ {
+ // Arrange
+ $this->store->put('counter', 5, 60);
+
+ // Act
+ $result = $this->store->increment('counter', 3);
+
+ // Assert
+ $this->assertEquals(8, $result);
+ $this->assertEquals(8, $this->store->get('counter'));
+ }
+
+ #[Test]
+ public function it_can_decrement_numeric_values(): void
+ {
+ // Arrange
+ $this->store->put('counter', 10, 60);
+
+ // Act
+ $result = $this->store->decrement('counter', 3);
+
+ // Assert
+ $this->assertEquals(7, $result);
+ $this->assertEquals(7, $this->store->get('counter'));
+ }
+
+ #[Test]
+ public function it_returns_false_when_incrementing_non_existent_key(): void
+ {
+ // Act
+ $result = $this->store->increment('non-existent', 1);
+
+ // Assert
+ $this->assertFalse($result);
+ }
+
+ #[Test]
+ public function it_can_store_items_forever(): void
+ {
+ // Act
+ $result = $this->store->forever('permanent-key', 'permanent-value');
+
+ // Assert
+ $this->assertTrue($result);
+ $this->assertEquals('permanent-value', $this->store->get('permanent-key'));
+
+ // Verify long expiration time (10 years minimum)
+ $expiration = $this->store->getExpiration('permanent-key');
+ $this->assertNotNull($expiration);
+ $this->assertGreaterThanOrEqual(315360000, $expiration); // >= 10 years
+ }
+
+ #[Test]
+ public function it_can_store_and_retrieve_arrays(): void
+ {
+ // Arrange
+ $data = ['name' => 'Test', 'values' => [1, 2, 3]];
+
+ // Act
+ $this->store->put('array-data', $data, 60);
+
+ // Assert
+ $result = $this->store->get('array-data');
+ $this->assertEquals($data, $result);
+ $this->assertIsArray($result);
+ $this->assertEquals([1, 2, 3], $result['values']);
+ }
+
+ #[Test]
+ public function it_can_store_and_retrieve_objects(): void
+ {
+ // Arrange
+ $data = new \stdClass();
+ $data->name = 'Test Object';
+ $data->value = 123;
+
+ // Act
+ $this->store->put('object-data', $data, 60);
+
+ // Assert
+ $result = $this->store->get('object-data');
+ $this->assertEquals($data, $result);
+ $this->assertInstanceOf(\stdClass::class, $result);
+ $this->assertEquals('Test Object', $result->name);
+ $this->assertEquals(123, $result->value);
+ }
+
+ #[Test]
+ public function it_can_forget_specific_keys(): void
+ {
+ // Arrange
+ $this->store->put('forget-me', 'value', 60);
+ $this->store->put('keep-me', 'value', 60);
+
+ // Assert item exists before forgetting
+ $this->assertEquals('value', $this->store->get('forget-me'));
+
+ // Act
+ $result = $this->store->forget('forget-me');
+
+ // Assert
+ $this->assertTrue($result);
+ $this->assertNull($this->store->get('forget-me'));
+ $this->assertEquals('value', $this->store->get('keep-me'));
+ }
+
+ #[Test]
+ public function it_can_flush_entire_cache(): void
+ {
+ // Arrange
+ $this->store->put('key1', 'value1', 60);
+ $this->store->put('key2', 'value2', 60);
+ $this->store->tags(['tag1'])->put('key3', 'value3', 60);
+
+ // Act
+ $result = $this->store->flush();
+
+ // Assert
+ $this->assertTrue($result);
+ $this->assertNull($this->store->get('key1'));
+ $this->assertNull($this->store->get('key2'));
+ $this->assertNull($this->store->get('key3'));
+
+ // Verify MongoDB collection is empty
+ $count = DB::connection('mongodb')
+ ->table($this->table())
+ ->count();
+
+ $this->assertEquals(0, $count);
+ }
+}
diff --git a/tests/LaravelIntegrationTest.php b/tests/LaravelIntegrationTest.php
new file mode 100644
index 0000000..282eafa
--- /dev/null
+++ b/tests/LaravelIntegrationTest.php
@@ -0,0 +1,163 @@
+app['config']->set('cache.default', 'mongodb');
+ $this->app['config']->set('cache.prefix', 'laravel_cache:');
+
+ // Ensure MongoDB cache store is defined
+ $this->app['config']->set('cache.stores.mongodb', [
+ 'driver' => 'mongodb',
+ 'table' => $this->table(),
+ 'connection' => 'mongodb',
+ ]);
+
+ // Resolve the cache manager to register the driver
+ $this->app->make('cache');
+
+ // Clear any existing cache data
+ Cache::flush();
+
+ // Freeze time for consistent testing
+ Carbon::setTestNow(now());
+ }
+
+ protected function tearDown(): void
+ {
+ parent::tearDown();
+ Carbon::setTestNow(); // Clear test now
+ }
+
+ #[Test]
+ public function it_registers_mongodb_driver_with_laravel(): void
+ {
+ // Simply test if the MongoDB cache driver works
+ $uniqueKey = 'mongodb-driver-test-' . uniqid('', true);
+ $uniqueValue = 'test-value-' . uniqid('', true);
+
+ // Store a value using the MongoDB driver
+ Cache::driver('mongodb')->put($uniqueKey, $uniqueValue, 60);
+
+ // Retrieve it and check if it matches
+ $this->assertEquals($uniqueValue, Cache::driver('mongodb')->get($uniqueKey));
+ }
+
+ #[Test]
+ public function it_works_with_laravel_cache_facade(): void
+ {
+ // Act
+ Cache::put('facade-test', 'facade-value', 60);
+
+ // Assert
+ $this->assertEquals('facade-value', Cache::get('facade-test'));
+ }
+
+ #[Test]
+ public function it_supports_cache_helper_functions(): void
+ {
+ // Act - Using the global cache() helper
+ cache(['helper-test' => 'helper-value'], 60);
+
+ // Assert
+ $this->assertEquals('helper-value', cache('helper-test'));
+ }
+
+ #[Test]
+ public function it_supports_has_method(): void
+ {
+ // Arrange
+ Cache::put('exists-key', 'exists-value', 60);
+
+ // Act & Assert
+ $this->assertTrue(Cache::has('exists-key'));
+ $this->assertFalse(Cache::has('doesnt-exist-key'));
+ }
+
+ #[Test]
+ public function it_supports_add_method(): void
+ {
+ // Since we now call flush() in setUp, we need to ensure no conflicts
+ $uniqueKey = 'add-test-' . uniqid('', true);
+
+ // Act & Assert - Adding to non-existent key succeeds
+ $this->assertTrue(Cache::add($uniqueKey, 'add-value', 60));
+ $this->assertEquals('add-value', Cache::get($uniqueKey));
+
+ // Act & Assert - Adding to existing key fails
+ $this->assertFalse(Cache::add($uniqueKey, 'new-value', 60));
+ $this->assertEquals('add-value', Cache::get($uniqueKey)); // Value remains unchanged
+ }
+
+ #[Test]
+ public function it_supports_remember_method(): void
+ {
+ // Act - This should store the value
+ $value = rand(1000, 9999);
+ $result1 = Cache::remember('remember-test', 60, function () use ($value) {
+ return 'remembered-' . $value;
+ });
+
+ // Act - This should retrieve from cache without executing callback
+ $result2 = Cache::remember('remember-test', 60, function () {
+ return 'different-' . rand(1000, 9999);
+ });
+
+ // Assert values match and callback wasn't executed second time
+ $this->assertEquals($result1, $result2);
+ }
+
+ #[Test]
+ public function it_supports_forever_method(): void
+ {
+ // Act
+ Cache::forever('forever-key', 'forever-value');
+
+ // Assert
+ $this->assertEquals('forever-value', Cache::get('forever-key'));
+ }
+
+ #[Test]
+ public function it_supports_pull_method(): void
+ {
+ // Arrange
+ Cache::put('pull-key', 'pull-value', 60);
+
+ // Act - Pull retrieves and removes in one operation
+ $result = Cache::pull('pull-key');
+
+ // Assert
+ $this->assertEquals('pull-value', $result);
+ $this->assertFalse(Cache::has('pull-key'));
+ }
+
+ #[Test]
+ public function it_supports_counting_and_incrementing(): void
+ {
+ // Arrange
+ Cache::put('count-key', 3, 60);
+
+ // Act
+ $result1 = Cache::increment('count-key');
+ $result2 = Cache::increment('count-key', 2);
+ $result3 = Cache::decrement('count-key');
+
+ // Assert
+ $this->assertEquals(4, $result1);
+ $this->assertEquals(6, $result2);
+ $this->assertEquals(5, $result3);
+ $this->assertEquals(5, Cache::get('count-key'));
+ }
+}
diff --git a/tests/Models/Cache.php b/tests/Models/Cache.php
deleted file mode 100644
index 8599471..0000000
--- a/tests/Models/Cache.php
+++ /dev/null
@@ -1,16 +0,0 @@
- 'array'];
-}
diff --git a/tests/Overrides/Builder.php b/tests/Overrides/Builder.php
deleted file mode 100644
index edf548c..0000000
--- a/tests/Overrides/Builder.php
+++ /dev/null
@@ -1,33 +0,0 @@
-compileWheres(), $this->parseValues($values));
- }
-
- return parent::update($values);
- }
-
- public function where($column, $operator = null, $value = null, $boolean = 'and')
- {
- if ($column === 'tags') {
- return parent::where($column, 'like', "%$operator%");
- }
-
- return parent::where($column, $operator, $value, $boolean);
- }
-
- public function first($columns = ['*'])
- {
- $result = (array)parent::first($columns);
-
- return $this->parseValues($result);
- }
-}
diff --git a/tests/Overrides/BuilderHelpers.php b/tests/Overrides/BuilderHelpers.php
deleted file mode 100644
index 841c1a8..0000000
--- a/tests/Overrides/BuilderHelpers.php
+++ /dev/null
@@ -1,287 +0,0 @@
-map(function ($item) {
- if (is_array($item)) {
- $item = json_encode($item);
- } elseif ((bool)strtotime($item) !== false) {
- $item = Carbon::createFromTimeString($item);
- }
-
- return $item;
- })->toArray();
- }
-
- /**
- * Compile the where array.
- * @return array
- */
- protected function compileWheres(): array
- {
- // The wheres to compile.
- $wheres = $this->wheres ?: [];
-
- // We will add all compiled wheres to this array.
- $compiled = [];
-
- foreach ($wheres as $i => &$where) {
- // Make sure the operator is in lowercase.
- if (isset($where['operator'])) {
- $where['operator'] = strtolower($where['operator']);
-
- // Operator conversions
- $convert = [
- 'regexp' => 'regex',
- 'elemmatch' => 'elemMatch',
- 'geointersects' => 'geoIntersects',
- 'geowithin' => 'geoWithin',
- 'nearsphere' => 'nearSphere',
- 'maxdistance' => 'maxDistance',
- 'centersphere' => 'centerSphere',
- 'uniquedocs' => 'uniqueDocs',
- ];
-
- if (array_key_exists($where['operator'], $convert)) {
- $where['operator'] = $convert[$where['operator']];
- }
- }
-
- // Convert id's.
- if (isset($where['column']) && ($where['column'] == '_id' || Str::endsWith($where['column'], '._id'))) {
- // Multiple values.
- if (isset($where['values'])) {
- foreach ($where['values'] as &$value) {
- $value = $this->convertKey($value);
- }
- } // Single value.
- elseif (isset($where['value'])) {
- $where['value'] = $this->convertKey($where['value']);
- }
- }
-
- // Convert DateTime values to UTCDateTime.
- if (isset($where['value'])) {
- if (is_array($where['value'])) {
- array_walk_recursive($where['value'], function (&$item, $key) {
- if ($item instanceof \DateTime) {
- $item = new UTCDateTime($item->format('Uv'));
- }
- });
- } else {
- if ($where['value'] instanceof DateTime) {
- $where['value'] = new UTCDateTime($where['value']->format('Uv'));
- }
- }
- } elseif (isset($where['values'])) {
- array_walk_recursive($where['values'], function (&$item, $key) {
- if ($item instanceof DateTime) {
- $item = new UTCDateTime($item->format('Uv'));
- }
- });
- }
-
- // The next item in a "chain" of wheres devices the boolean of the
- // first item. So if we see that there are multiple wheres, we will
- // use the operator of the next where.
- if ($i == 0 && count($wheres) > 1 && $where['boolean'] == 'and') {
- $where['boolean'] = $wheres[$i + 1]['boolean'];
- }
-
- // We use different methods to compile different wheres.
- $method = "compileWhere{$where['type']}";
- $result = $this->{$method}($where);
-
- // Wrap the where with an $or operator.
- if ($where['boolean'] == 'or') {
- $result = ['$or' => [$result]];
- }
-
- // If there are multiple wheres, we will wrap it with $and. This is needed
- // to make nested wheres work.
- elseif (count($wheres) > 1) {
- $result = ['$and' => [$result]];
- }
-
- // Merge the compiled where with the others.
- $compiled = array_merge_recursive($compiled, $result);
- }
-
- return $compiled;
- }
-
- /**
- * @param array $where
- * @return array
- */
- protected function compileWhereAll(array $where): array
- {
- extract($where);
-
- return [$column => ['$all' => array_values($values)]];
- }
-
- /**
- * @param array $where
- * @return array
- */
- protected function compileWhereBasic(array $where): array
- {
- extract($where);
-
- // Replace like or not like with a Regex instance.
- if (in_array($operator, ['like', 'not like'])) {
- if ($operator === 'not like') {
- $operator = 'not';
- } else {
- $operator = '=';
- }
-
- // Convert to regular expression.
- $regex = preg_replace('#(^|[^\\\])%#', '$1.*', preg_quote($value));
-
- // Convert like to regular expression.
- if (!Str::startsWith($value, '%')) {
- $regex = '^' . $regex;
- }
- if (!Str::endsWith($value, '%')) {
- $regex .= '$';
- }
-
- $value = new Regex($regex, 'i');
- } // Manipulate regexp operations.
- elseif (in_array($operator, ['regexp', 'not regexp', 'regex', 'not regex'])) {
- // Automatically convert regular expression strings to Regex objects.
- if (!$value instanceof Regex) {
- $e = explode('/', $value);
- $flag = end($e);
- $regstr = substr($value, 1, -(strlen($flag) + 1));
- $value = new Regex($regstr, $flag);
- }
-
- // For inverse regexp operations, we can just use the $not operator
- // and pass it a Regex instence.
- if (Str::startsWith($operator, 'not')) {
- $operator = 'not';
- }
- }
-
- if (!isset($operator) || $operator == '=') {
- $query = [$column => $value];
- } elseif (array_key_exists($operator, $this->conversion)) {
- $query = [$column => [$this->conversion[$operator] => $value]];
- } else {
- $query = [$column => ['$' . $operator => $value]];
- }
-
- return $query;
- }
-
- /**
- * @param array $where
- * @return mixed
- */
- protected function compileWhereNested(array $where)
- {
- extract($where);
-
- return $query->compileWheres();
- }
-
- /**
- * @param array $where
- * @return array
- */
- protected function compileWhereIn(array $where): array
- {
- extract($where);
-
- return [$column => ['$in' => array_values($values)]];
- }
-
- /**
- * @param array $where
- * @return array
- */
- protected function compileWhereNotIn(array $where): array
- {
- extract($where);
-
- return [$column => ['$nin' => array_values($values)]];
- }
-
- /**
- * @param array $where
- * @return array
- */
- protected function compileWhereNull(array $where): array
- {
- $where['operator'] = '=';
- $where['value'] = null;
-
- return $this->compileWhereBasic($where);
- }
-
- /**
- * @param array $where
- * @return array
- */
- protected function compileWhereNotNull(array $where): array
- {
- $where['operator'] = '!=';
- $where['value'] = null;
-
- return $this->compileWhereBasic($where);
- }
-
- /**
- * @param array $where
- * @return array
- */
- protected function compileWhereBetween(array $where): array
- {
- extract($where);
-
- if ($not) {
- return [
- '$or' => [
- [
- $column => [
- '$lte' => $values[0],
- ],
- ],
- [
- $column => [
- '$gte' => $values[1],
- ],
- ],
- ],
- ];
- }
-
- return [
- $column => [
- '$gte' => $values[0],
- '$lte' => $values[1],
- ],
- ];
- }
-
- /**
- * @param array $where
- * @return mixed
- */
- protected function compileWhereRaw(array $where)
- {
- return $where['sql'];
- }
-}
diff --git a/tests/StoreTest.php b/tests/StoreTest.php
index 502f9d2..932f326 100644
--- a/tests/StoreTest.php
+++ b/tests/StoreTest.php
@@ -5,141 +5,141 @@
use ForFit\Mongodb\Cache\MongoTaggedCache;
use ForFit\Mongodb\Cache\Store;
use Illuminate\Support\Carbon;
-use Tests\Models\Cache;
+use Illuminate\Support\Facades\DB;
+use PHPUnit\Framework\Attributes\Test;
class StoreTest extends TestCase
{
- protected $store;
+ protected Store $store;
protected function setUp(): void
{
parent::setUp();
- $this->store = new Store($this->connection(), $this->table());
+ // Setup the store with a real connection
+ $this->store = new Store(
+ DB::connection('mongodb'),
+ $this->table()
+ );
- // Freeze time.
+ // Freeze time for consistent testing
Carbon::setTestNow(now());
}
- /** @test */
- public function it_stores_an_item_in_the_cache_for_given_time()
+ protected function tearDown(): void
+ {
+ parent::tearDown();
+ Carbon::setTestNow(); // Clear test now
+ }
+
+ #[Test]
+ public function it_stores_an_item_in_the_cache_for_given_time(): void
{
// Act
- $sut = $this->store->put('test-key', 'test-value', 3);
+ $result = $this->store->put('test-key', 'test-value', 3);
// Assert
- $this->assertTrue($sut);
- $this->assertDatabaseHas($this->table(), [
- 'key' => 'test-key',
- 'value' => serialize('test-value'),
- 'expiration' => (now()->timestamp + 3) * 1000,
- 'tags' => '[]'
- ]);
+ $this->assertTrue($result);
+
+ // Verify the item was stored
+ $cacheItem = DB::connection('mongodb')
+ ->table($this->table())
+ ->where('key', 'test-key')
+ ->first();
+
+ $this->assertNotNull($cacheItem);
+ $this->assertEquals(serialize('test-value'), $cacheItem->value);
}
- /** @test */
- public function it_updates_an_item_in_the_cache_for_given_time()
+ #[Test]
+ public function it_updates_an_item_in_the_cache_for_given_time(): void
{
- Cache::create([
- 'key' => 'test-key',
- 'value' => serialize('fake-value')
- ]);
-
- // Act
- $sut = $this->store->put('test-key', 'new-value', 3);
+ // Setup - add initial value
+ $this->store->put('test-key', 'initial-value', 3);
+
+ // Act - update the value
+ $result = $this->store->put('test-key', 'new-value', 3);
// Assert
- $this->assertTrue($sut);
- $this->assertDatabaseHas($this->table(), [
- 'key' => 'test-key',
- 'value' => serialize('new-value'),
- 'expiration' => (now()->timestamp + 3) * 1000,
- 'tags' => '[]'
- ]);
+ $this->assertTrue($result);
+
+ // Verify the item was updated
+ $cacheItem = DB::connection('mongodb')
+ ->table($this->table())
+ ->where('key', 'test-key')
+ ->first();
+
+ $this->assertNotNull($cacheItem);
+ $this->assertEquals(serialize('new-value'), $cacheItem->value);
}
- /** @test */
- public function it_retrieves_value_from_the_cache_by_given_key()
+ #[Test]
+ public function it_retrieves_value_from_the_cache_by_given_key(): void
{
- // Arrange
- Cache::create([
- 'key' => 'test-key',
- 'value' => serialize('test-value')
- ]);
+ // Setup - store a value
+ $this->store->put('test-key', 'test-value', 3);
// Act
- $sut = $this->store->get('test-key');
+ $result = $this->store->get('test-key');
// Assert
- $this->assertIsString($sut);
- $this->assertEquals('test-value', $sut);
+ $this->assertEquals('test-value', $result);
}
- /** @test */
- public function it_returns_null_if_key_does_not_exist()
+ #[Test]
+ public function it_returns_null_if_key_does_not_exist(): void
{
// Act
- $sut = $this->store->get('test-key');
+ $result = $this->store->get('non-existent-key');
// Assert
- $this->assertNull($sut);
+ $this->assertNull($result);
}
- /** @test */
- public function it_sets_the_tags_to_be_used()
+ #[Test]
+ public function it_sets_the_tags_to_be_used(): void
{
// Act
- $sut = $this->store->tags(['tag1', 'tag2']);
+ $result = $this->store->tags(['tag1', 'tag2']);
// Assert
- $this->assertInstanceOf(MongoTaggedCache::class, $sut);
- $this->assertPropertySame(['tag1', 'tag2'], 'tags', $sut);
+ $this->assertInstanceOf(MongoTaggedCache::class, $result);
+
+ // Use reflection to test the tags property
+ $reflection = new \ReflectionObject($result);
+ $property = $reflection->getProperty('tags');
+ $this->assertEquals(['tag1', 'tag2'], $property->getValue($result));
}
- /** @test */
- public function it_deletes_all_records_with_the_given_tag()
+ #[Test]
+ public function it_deletes_all_records_with_the_given_tag(): void
{
- // Arrange
- Cache::create([
- 'key' => 'test-key-1',
- 'value' => serialize('test-value-1'),
- 'tags' => ['tag1']
- ]);
-
- Cache::create([
- 'key' => 'test-key-2',
- 'value' => serialize('test-value-2'),
- 'tags' => ['tag2']
- ]);
+ // Setup
+ $this->store->tags(['tag1'])->put('key1', 'value1', 60);
+ $this->store->tags(['tag2'])->put('key2', 'value2', 60);
+ $this->store->tags(['tag1', 'tag2'])->put('key3', 'value3', 60);
// Act
$this->store->flushByTags(['tag1']);
-
+
// Assert
- $this->assertDatabaseMissing($this->table(), [
- 'key' => 'test-key-1',
- 'value' => serialize('test-value-1'),
- ]);
- $this->assertDatabaseHas($this->table(), [
- 'key' => 'test-key-2',
- 'value' => serialize('test-value-2'),
- ]);
+ $this->assertNull($this->store->get('key1'));
+ $this->assertEquals('value2', $this->store->get('key2'));
+ $this->assertNull($this->store->get('key3'));
}
- /** @test */
- public function it_retrieves_an_items_expiration_time_by_given_key()
+ #[Test]
+ public function it_retrieves_an_items_expiration_time_by_given_key(): void
{
- // Arrange
- Cache::create([
- 'key' => 'test-key',
- 'value' => serialize('test-value'),
- 'expiration' => now()->addDays(2)
- ]);
+ // Setup with expiration time - 2 days
+ $this->store->put('test-key', 'test-value', 172800);
// Act
- $sut = $this->store->getExpiration('test-key');
+ $result = $this->store->getExpiration('test-key');
- // Assert
- $this->assertEquals(172800, $sut); // 2 days in seconds.
+ // Assert - approximately 2 days in seconds (172800)
+ // Allow for small differences in timing during test execution
+ $this->assertGreaterThan(172700, $result);
+ $this->assertLessThan(172900, $result);
}
}
diff --git a/tests/TaggedCacheTest.php b/tests/TaggedCacheTest.php
new file mode 100644
index 0000000..f409f1a
--- /dev/null
+++ b/tests/TaggedCacheTest.php
@@ -0,0 +1,169 @@
+store = new Store(
+ DB::connection('mongodb'),
+ $this->table()
+ );
+
+ // Freeze time for consistent testing
+ Carbon::setTestNow(now());
+ }
+
+ protected function tearDown(): void
+ {
+ parent::tearDown();
+ Carbon::setTestNow(); // Clear test now
+ }
+
+ #[Test]
+ public function it_can_store_multiple_items_with_tags(): void
+ {
+ // Arrange
+ $taggedCache = $this->store->tags(['api', 'v1']);
+
+ // Act
+ $taggedCache->putMany([
+ 'key1' => 'value1',
+ 'key2' => 'value2',
+ 'key3' => 'value3'
+ ], 60);
+
+ // Assert - The values are retrievable from the tagged cache
+ $this->assertEquals('value1', $taggedCache->get('key1'));
+ $this->assertEquals('value2', $taggedCache->get('key2'));
+ $this->assertEquals('value3', $taggedCache->get('key3'));
+
+ // Assert - They're also retrievable from the base cache
+ $this->assertEquals('value1', $this->store->get('key1'));
+ $this->assertEquals('value2', $this->store->get('key2'));
+ $this->assertEquals('value3', $this->store->get('key3'));
+ }
+
+ #[Test]
+ public function it_can_flush_by_specific_tag(): void
+ {
+ // Arrange - Create items with different combinations of tags
+ $this->store->tags(['api'])->put('api-only', 'api-value', 60);
+ $this->store->tags(['v1'])->put('v1-only', 'v1-value', 60);
+ $this->store->tags(['api', 'v1'])->put('api-v1', 'both-value', 60);
+ $this->store->tags(['other'])->put('other-tag', 'other-value', 60);
+ $this->store->put('no-tag', 'no-tag-value', 60);
+
+ // Act - Flush only the 'api' tag
+ $this->store->flushByTags(['api']);
+
+ // Assert - 'api' tagged items should be gone
+ $this->assertNull($this->store->get('api-only'));
+ $this->assertNull($this->store->get('api-v1'));
+
+ // Assert - Other items should remain
+ $this->assertEquals('v1-value', $this->store->get('v1-only'));
+ $this->assertEquals('other-value', $this->store->get('other-tag'));
+ $this->assertEquals('no-tag-value', $this->store->get('no-tag'));
+ }
+
+ #[Test]
+ public function it_can_flush_all_tagged_items(): void
+ {
+ // Arrange
+ $taggedCache = $this->store->tags(['api', 'v1']);
+ $taggedCache->put('tagged1', 'value1', 60);
+ $taggedCache->put('tagged2', 'value2', 60);
+
+ // Add untagged item
+ $this->store->put('untagged', 'untagged-value', 60);
+
+ // Act - Flush the tagged cache
+ $taggedCache->flush();
+
+ // Assert - Tagged items should be gone
+ $this->assertNull($this->store->get('tagged1'));
+ $this->assertNull($this->store->get('tagged2'));
+
+ // Assert - Untagged item should remain
+ $this->assertEquals('untagged-value', $this->store->get('untagged'));
+ }
+
+ #[Test]
+ public function it_can_put_forever_with_tags(): void
+ {
+ // Arrange
+ $taggedCache = $this->store->tags(['permanent']);
+
+ // Act
+ $taggedCache->forever('permanent-tagged', 'permanent-value');
+
+ // Assert
+ $this->assertEquals('permanent-value', $taggedCache->get('permanent-tagged'));
+ }
+
+ #[Test]
+ public function it_respects_cache_ttl_with_tags(): void
+ {
+ // Arrange - store a value with a short TTL (1 second)
+ $taggedCache = $this->store->tags(['expiring']);
+ $taggedCache->put('expires-soon', 'expiring-value', 1);
+
+ // Assert - Value exists right after storing
+ $this->assertEquals('expiring-value', $taggedCache->get('expires-soon'));
+
+ // Wait for expiration
+ sleep(2);
+
+ // MongoDB TTL index runs approximately every 60 seconds
+ // So we can't reliably test automatic deletion
+ // Let's verify via a direct collection check if possible
+ $count = DB::connection('mongodb')
+ ->table($this->table())
+ ->where('key', 'expires-soon')
+ ->count();
+
+ // If it's 0, great - it was removed
+ // If not, we can still check the expiration time is in the past
+ if ($count > 0) {
+ $cacheItem = DB::connection('mongodb')
+ ->table($this->table())
+ ->where('key', 'expires-soon')
+ ->first();
+
+ $expireAt = $cacheItem->expiration->toDateTime()->getTimestamp();
+ $this->assertLessThanOrEqual(time(), $expireAt, 'Expiration time should be in the past');
+ } else {
+ $this->assertTrue(true, 'Item was automatically removed by MongoDB TTL index');
+ }
+ }
+
+ #[Test]
+ public function it_can_increment_and_decrement_with_tags(): void
+ {
+ // Arrange
+ $taggedCache = $this->store->tags(['counters']);
+ $taggedCache->put('counter', 5, 60);
+
+ // Act & Assert - Increment
+ $this->assertEquals(8, $taggedCache->increment('counter', 3));
+ $this->assertEquals(8, $taggedCache->get('counter'));
+
+ // Act & Assert - Decrement
+ $this->assertEquals(6, $taggedCache->decrement('counter', 2));
+ $this->assertEquals(6, $taggedCache->get('counter'));
+ }
+}
diff --git a/tests/TestCase.php b/tests/TestCase.php
index 2f4c318..8e6c96c 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -3,15 +3,15 @@
namespace Tests;
use ForFit\Mongodb\Cache\ServiceProvider as MongoDbCacheServiceProvider;
-use Illuminate\Database\ConnectionInterface;
-use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\DB;
+use MongoDB\Laravel\MongoDBServiceProvider;
use Orchestra\Testbench\TestCase as Orchestra;
-use Tests\Overrides\Builder;
+use PHPUnit\Framework\Attributes\RequiresPhpExtension;
+#[RequiresPhpExtension('mongodb')]
abstract class TestCase extends Orchestra
{
- private $table = 'cache_test';
- private $connectionInterface;
+ private string $table = 'cache_test';
/**
* @return void
@@ -20,28 +20,16 @@ protected function setUp(): void
{
parent::setUp();
- $this->setUpDatabase($this->app);
- $this->connectionInterface = $this->initializeConnection();
+ // Clear the cache collection before each test
+ $this->flushCache();
}
/**
- * @param \Illuminate\Foundation\Application $app
- *
- * @return array
- */
- protected function getPackageProviders($app): array
- {
- return [
- MongoDbCacheServiceProvider::class,
- ];
- }
-
- /**
- * Set up the environment.
+ * Define environment setup.
*
* @param \Illuminate\Foundation\Application $app
*/
- protected function getEnvironmentSetUp($app)
+ protected function defineEnvironment($app): void
{
$app['config']->set('cache.stores.mongodb', [
'driver' => 'mongodb',
@@ -51,18 +39,29 @@ protected function getEnvironmentSetUp($app)
$app['config']->set('database.default', 'mongodb');
$app['config']->set('database.connections.mongodb', [
- 'driver' => 'sqlite',
- 'database' => ':memory:',
- 'prefix' => '',
+ 'driver' => 'mongodb',
+ 'host' => env('MONGODB_HOST', '127.0.0.1'),
+ 'port' => env('MONGODB_PORT', 27017),
+ 'database' => env('MONGODB_DATABASE', 'laravel_mongodb_cache_test'),
+ 'username' => env('MONGODB_USERNAME', ''),
+ 'password' => env('MONGODB_PASSWORD', ''),
+ 'options' => [
+ 'database' => env('MONGODB_AUTHENTICATION_DATABASE', 'admin'),
+ ],
]);
}
/**
- * @return \Illuminate\Database\Connection|\Mockery\LegacyMockInterface|\Mockery\MockInterface|null
+ * @param \Illuminate\Foundation\Application $app
+ *
+ * @return array
*/
- protected function connection()
+ protected function getPackageProviders($app): array
{
- return $this->connectionInterface->getMock();
+ return [
+ MongoDbCacheServiceProvider::class,
+ MongoDBServiceProvider::class
+ ];
}
/**
@@ -74,50 +73,21 @@ protected function table(): string
}
/**
- * Set up the database.
- *
- * @param \Illuminate\Foundation\Application $app
+ * Flush the cache collection
*/
- protected function setUpDatabase($app)
+ protected function flushCache(): void
{
- $app['db']->connection()->getSchemaBuilder()->create($this->table, function (Blueprint $table) {
- $table->increments('_id');
- $table->dateTimeTz('expiration')->nullable();
- $table->string('key');
- $table->string('value');
- $table->json('tags')->nullable();
- });
+ DB::connection('mongodb')
+ ->table($this->table)
+ ->delete();
}
/**
- * @return \Mockery\Expectation|\Mockery\ExpectationInterface|\Mockery\HigherOrderMessage
+ * Get the cache collection instance from MongoDB
*/
- protected function initializeConnection()
+ protected function getCacheCollection()
{
- $builder = app(Builder::class, ['connection' => $this->getConnection()]);
- $builder->from = $this->table;
-
- return $this->spy(ConnectionInterface::class)
- ->shouldReceive('table')
- ->andReturn($builder);
- }
-
- /**
- * Assert the object has given property.
- *
- * @param $expected
- * @param $property
- * @param $object
- * @param string $message
- * @return void
- * @throws \ReflectionException
- */
- protected function assertPropertySame($expected, $property, $object, string $message = '')
- {
- $reflectedClass = new \ReflectionClass($object);
- $reflection = $reflectedClass->getProperty($property);
- $reflection->setAccessible(true);
-
- $this->assertSame($expected, $reflection->getValue($object), $message);
+ return DB::connection('mongodb')
+ ->table($this->table);
}
}