From 3a4306161265b8fa7835b62925f700ab4002b456 Mon Sep 17 00:00:00 2001 From: Randall Wilk Date: Wed, 27 Sep 2023 15:10:51 -0500 Subject: [PATCH] Add support for custom value and context serializers --- config/settings.php | 36 ++++ docs/best-practices/performance-tips.md | 2 +- src/Contracts/ContextSerializer.php | 12 ++ src/Contracts/KeyGenerator.php | 14 ++ src/Exceptions/InvalidContextValue.php | 15 ++ src/Settings.php | 12 +- src/SettingsServiceProvider.php | 10 +- src/Support/Context.php | 19 +- src/Support/ContextSerializer.php | 13 -- .../ContextSerializers/ContextSerializer.php | 16 ++ .../DotNotationContextSerializer.php | 34 ++++ src/Support/KeyGenerator.php | 17 -- src/Support/KeyGenerators/Md5KeyGenerator.php | 26 +++ .../KeyGenerators/ReadableKeyGenerator.php | 44 +++++ src/Support/ValueSerializer.php | 4 +- tests/Feature/SettingsTest.php | 185 +++++++++--------- tests/Support/Models/User.php | 5 + .../ContextSerializerTest.php | 2 +- .../DotNotationContextSerializerTest.php | 33 ++++ tests/Unit/ContextTest.php | 19 ++ tests/Unit/KeyGeneratorTest.php | 24 --- .../KeyGenerators/Md5KeyGeneratorTest.php | 39 ++++ .../ReadableKeyGeneratorTest.php | 42 ++++ tests/Unit/ValueSerializerTest.php | 8 +- 24 files changed, 472 insertions(+), 159 deletions(-) create mode 100644 src/Contracts/ContextSerializer.php create mode 100644 src/Contracts/KeyGenerator.php create mode 100644 src/Exceptions/InvalidContextValue.php delete mode 100644 src/Support/ContextSerializer.php create mode 100644 src/Support/ContextSerializers/ContextSerializer.php create mode 100644 src/Support/ContextSerializers/DotNotationContextSerializer.php delete mode 100644 src/Support/KeyGenerator.php create mode 100644 src/Support/KeyGenerators/Md5KeyGenerator.php create mode 100644 src/Support/KeyGenerators/ReadableKeyGenerator.php rename tests/Unit/{ => ContextSerializers}/ContextSerializerTest.php (86%) create mode 100644 tests/Unit/ContextSerializers/DotNotationContextSerializerTest.php delete mode 100644 tests/Unit/KeyGeneratorTest.php create mode 100644 tests/Unit/KeyGenerators/Md5KeyGeneratorTest.php create mode 100644 tests/Unit/KeyGenerators/ReadableKeyGeneratorTest.php diff --git a/config/settings.php b/config/settings.php index 6553bf3..a109414 100644 --- a/config/settings.php +++ b/config/settings.php @@ -112,4 +112,40 @@ | */ 'team_foreign_key' => 'team_id', + + /* + |-------------------------------------------------------------------------- + | Context Serializer + |-------------------------------------------------------------------------- + | + | The context serializer is responsible for converting a Context object + | into a string, which gets appended to a setting key in the database. + | + | Any custom serializer you use must implement the + | \Rawilk\Settings\Contracts\ContextSerializer interface. + | + | Supported: + | - \Rawilk\Settings\Support\ContextSerializers\ContextSerializer (default) + | - \Rawilk\Settings\Support\ContextSerializers\DotNotationContextSerializer + | + */ + 'context_serializer' => \Rawilk\Settings\Support\ContextSerializers\ContextSerializer::class, + + /* + |-------------------------------------------------------------------------- + | Key Generator + |-------------------------------------------------------------------------- + | + | The key generator is responsible for generating a suitable key for a + | setting. + | + | Any custom key generator you use must implement the + | \Rawilk\Settings\Contracts\KeyGenerator interface. + | + | Supported: + | - \Rawilk\Settings\Support\KeyGenerators\ReadableKeyGenerator + | - \Rawilk\Settings\Support\KeyGenerators\Md5KeyGenerator (default) + | + */ + 'key_generator' => \Rawilk\Settings\Support\KeyGenerators\Md5KeyGenerator::class, ]; diff --git a/docs/best-practices/performance-tips.md b/docs/best-practices/performance-tips.md index da57c0c..52a5a66 100644 --- a/docs/best-practices/performance-tips.md +++ b/docs/best-practices/performance-tips.md @@ -8,7 +8,7 @@ You are free to turn caching off, but you might notice a performance hit on larg on apps that are retrieving many settings on each page load. As always, if you choose to bypass the provided methods for setting and removing settings, you will need to flush the cache manually for -each setting you manipulate manually. To determine the cache key for a setting key, you should use the `Rawilk\Settings\Support\KeyGenerator` to +each setting you manipulate manually. To determine the cache key for a setting key, you should use the `Rawilk\Settings\Support\KeyGenerators\KeyGenerator` to generate the md5 version of the setting's key: ```php diff --git a/src/Contracts/ContextSerializer.php b/src/Contracts/ContextSerializer.php new file mode 100644 index 0000000..bb632c8 --- /dev/null +++ b/src/Contracts/ContextSerializer.php @@ -0,0 +1,12 @@ +keyGenerator = new KeyGenerator(new ContextSerializer); + public function __construct( + protected Driver $driver, + protected KeyGenerator $keyGenerator, + ) { $this->valueSerializer = new ValueSerializer; } diff --git a/src/SettingsServiceProvider.php b/src/SettingsServiceProvider.php index 4e30aab..cc4d1f4 100644 --- a/src/SettingsServiceProvider.php +++ b/src/SettingsServiceProvider.php @@ -6,6 +6,8 @@ use Rawilk\Settings\Contracts\Setting as SettingContract; use Rawilk\Settings\Drivers\Factory; +use Rawilk\Settings\Support\ContextSerializers\ContextSerializer; +use Rawilk\Settings\Support\KeyGenerators\Md5KeyGenerator; use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; @@ -59,8 +61,14 @@ protected function registerSettings(): void ); $this->app->singleton(Settings::class, function ($app) { + $keyGenerator = $this->app->make($app['config']['settings.key_generator'] ?? Md5KeyGenerator::class); + $keyGenerator->setContextSerializer( + $this->app->make($app['config']['settings.context_serializer'] ?? ContextSerializer::class) + ); + $settings = new Settings( - $app['SettingsFactory']->driver() + $app['SettingsFactory']->driver(), + $keyGenerator, ); $settings->useCacheKeyPrefix($app['config']['settings.cache_key_prefix'] ?? ''); diff --git a/src/Support/Context.php b/src/Support/Context.php index cabc9b6..68e4ea8 100644 --- a/src/Support/Context.php +++ b/src/Support/Context.php @@ -5,9 +5,11 @@ namespace Rawilk\Settings\Support; use Countable; +use Illuminate\Contracts\Support\Arrayable; use OutOfBoundsException; +use Rawilk\Settings\Exceptions\InvalidContextValue; -class Context implements Countable +class Context implements Arrayable, Countable { protected array $arguments = []; @@ -43,6 +45,8 @@ public function remove(string $name): self public function set(string $name, $value): self { + $this->ensureValidValue($name, $value); + $this->arguments[$name] = $value; return $this; @@ -52,4 +56,17 @@ public function count(): int { return count($this->arguments); } + + public function toArray(): array + { + return $this->arguments; + } + + protected function ensureValidValue(string $key, mixed $value): void + { + throw_unless( + is_string($value) || is_numeric($value) || is_bool($value) || is_null($value), + InvalidContextValue::forKey($key), + ); + } } diff --git a/src/Support/ContextSerializer.php b/src/Support/ContextSerializer.php deleted file mode 100644 index dd423ca..0000000 --- a/src/Support/ContextSerializer.php +++ /dev/null @@ -1,13 +0,0 @@ -toArray()) + ->map(function ($value, string $key) { + // Use the model's morph class when possible. + $value = match ($key) { + 'model' => rescue(fn () => app($value)->getMorphClass(), $value), + default => $value, + }; + + if ($value === false) { + $value = 0; + } + + return "{$key}:{$value}"; + }) + ->implode('::'); + } +} diff --git a/src/Support/KeyGenerator.php b/src/Support/KeyGenerator.php deleted file mode 100644 index a030e23..0000000 --- a/src/Support/KeyGenerator.php +++ /dev/null @@ -1,17 +0,0 @@ -serializer->serialize($context)); - } -} diff --git a/src/Support/KeyGenerators/Md5KeyGenerator.php b/src/Support/KeyGenerators/Md5KeyGenerator.php new file mode 100644 index 0000000..e1196d4 --- /dev/null +++ b/src/Support/KeyGenerators/Md5KeyGenerator.php @@ -0,0 +1,26 @@ +serializer->serialize($context)); + } + + public function setContextSerializer(ContextSerializer $serializer): self + { + $this->serializer = $serializer; + + return $this; + } +} diff --git a/src/Support/KeyGenerators/ReadableKeyGenerator.php b/src/Support/KeyGenerators/ReadableKeyGenerator.php new file mode 100644 index 0000000..c8ac3fd --- /dev/null +++ b/src/Support/KeyGenerators/ReadableKeyGenerator.php @@ -0,0 +1,44 @@ +normalizeKey($key); + + if ($context) { + $key .= ':c:::' . $this->serializer->serialize($context); + } + + return $key; + } + + public function setContextSerializer(ContextSerializer $serializer): KeyGeneratorContract + { + $this->serializer = $serializer; + + return $this; + } + + protected function normalizeKey(string $key): string + { + // We want to preserve period characters in the key, however everything else is fair game + // to convert to a slug. + return Str::of($key) + ->replace('.', '-dot-') + ->slug() + ->replace('-dot-', '.') + ->__toString(); + } +} diff --git a/src/Support/ValueSerializer.php b/src/Support/ValueSerializer.php index ab9fbc5..2ba9839 100644 --- a/src/Support/ValueSerializer.php +++ b/src/Support/ValueSerializer.php @@ -11,8 +11,8 @@ public function serialize($value): string return serialize($value); } - public function unserialize(string $serialized) + public function unserialize(string $serialized): mixed { - return unserialize($serialized); + return unserialize($serialized, ['allowed_classes' => false]); } } diff --git a/tests/Feature/SettingsTest.php b/tests/Feature/SettingsTest.php index 9fe2eed..14e5c7d 100644 --- a/tests/Feature/SettingsTest.php +++ b/tests/Feature/SettingsTest.php @@ -3,7 +3,7 @@ declare(strict_types=1); use Illuminate\Support\Facades\DB; -use Rawilk\Settings\Facades\Settings; +use Rawilk\Settings\Facades\Settings as SettingsFacade; use Rawilk\Settings\Support\Context; beforeEach(function () { @@ -18,209 +18,209 @@ }); it('can determine if a setting has been persisted', function () { - expect(Settings::has('foo'))->toBeFalse(); + expect(SettingsFacade::has('foo'))->toBeFalse(); - Settings::set('foo', 'bar'); + SettingsFacade::set('foo', 'bar'); - expect(Settings::has('foo'))->toBeTrue(); + expect(SettingsFacade::has('foo'))->toBeTrue(); DB::table('settings')->truncate(); - expect(Settings::has('foo'))->toBeFalse(); + expect(SettingsFacade::has('foo'))->toBeFalse(); }); it('gets persisted setting values', function () { - Settings::set('foo', 'bar'); + SettingsFacade::set('foo', 'bar'); - expect(Settings::get('foo'))->toBe('bar'); + expect(SettingsFacade::get('foo'))->toBe('bar'); }); it('returns a default value if a setting is not persisted', function () { - expect(Settings::get('foo', 'default value'))->toBe('default value'); + expect(SettingsFacade::get('foo', 'default value'))->toBe('default value'); }); it('can retrieve values based on context', function () { - Settings::set('foo', 'bar'); + SettingsFacade::set('foo', 'bar'); $userContext = new Context(['user_id' => 1]); - Settings::context($userContext)->set('foo', 'user_1_value'); + SettingsFacade::context($userContext)->set('foo', 'user_1_value'); expect(DB::table('settings')->count())->toBe(2) - ->and(Settings::get('foo'))->toBe('bar') - ->and(Settings::context($userContext)->get('foo'))->toBe('user_1_value'); + ->and(SettingsFacade::get('foo'))->toBe('bar') + ->and(SettingsFacade::context($userContext)->get('foo'))->toBe('user_1_value'); }); it('can determine if a setting is persisted based on context', function () { - Settings::set('foo', 'bar'); + SettingsFacade::set('foo', 'bar'); $userContext = new Context(['user_id' => 1]); $user2Context = new Context(['user_id' => 2]); - expect(Settings::has('foo'))->toBeTrue() - ->and(Settings::context($userContext)->has('foo'))->toBeFalse(); + expect(SettingsFacade::has('foo'))->toBeTrue() + ->and(SettingsFacade::context($userContext)->has('foo'))->toBeFalse(); - Settings::context($userContext)->set('foo', 'user 1 value'); + SettingsFacade::context($userContext)->set('foo', 'user 1 value'); - expect(Settings::context($userContext)->has('foo'))->toBeTrue() - ->and(Settings::context($user2Context)->has('foo'))->toBeFalse(); + expect(SettingsFacade::context($userContext)->has('foo'))->toBeTrue() + ->and(SettingsFacade::context($user2Context)->has('foo'))->toBeFalse(); - Settings::context($user2Context)->set('foo', 'user 2 value'); + SettingsFacade::context($user2Context)->set('foo', 'user 2 value'); - expect(Settings::context($userContext)->has('foo'))->toBeTrue() - ->and(Settings::context($user2Context)->has('foo'))->toBeTrue() - ->and(Settings::has('foo'))->toBeTrue(); + expect(SettingsFacade::context($userContext)->has('foo'))->toBeTrue() + ->and(SettingsFacade::context($user2Context)->has('foo'))->toBeTrue() + ->and(SettingsFacade::has('foo'))->toBeTrue(); }); it('can remove persisted values based on context', function () { $userContext = new Context(['user_id' => 1]); $user2Context = new Context(['user_id' => 2]); - Settings::set('foo', 'bar'); - Settings::context($userContext)->set('foo', 'user 1 value'); - Settings::context($user2Context)->set('foo', 'user 2 value'); + SettingsFacade::set('foo', 'bar'); + SettingsFacade::context($userContext)->set('foo', 'user 1 value'); + SettingsFacade::context($user2Context)->set('foo', 'user 2 value'); - expect(Settings::has('foo'))->toBeTrue() - ->and(Settings::context($userContext)->has('foo'))->toBeTrue() - ->and(Settings::context($user2Context)->has('foo'))->toBeTrue(); + expect(SettingsFacade::has('foo'))->toBeTrue() + ->and(SettingsFacade::context($userContext)->has('foo'))->toBeTrue() + ->and(SettingsFacade::context($user2Context)->has('foo'))->toBeTrue(); - Settings::context($user2Context)->forget('foo'); + SettingsFacade::context($user2Context)->forget('foo'); - expect(Settings::has('foo'))->toBeTrue() - ->and(Settings::context($userContext)->has('foo'))->toBeTrue() - ->and(Settings::context($user2Context)->has('foo'))->toBeFalse(); + expect(SettingsFacade::has('foo'))->toBeTrue() + ->and(SettingsFacade::context($userContext)->has('foo'))->toBeTrue() + ->and(SettingsFacade::context($user2Context)->has('foo'))->toBeFalse(); }); it('persists values', function () { - Settings::set('foo', 'bar'); + SettingsFacade::set('foo', 'bar'); expect(DB::table('settings')->count())->toBe(1) - ->and(Settings::get('foo'))->toBe('bar'); + ->and(SettingsFacade::get('foo'))->toBe('bar'); - Settings::set('foo', 'updated value'); + SettingsFacade::set('foo', 'updated value'); expect(DB::table('settings')->count())->toBe(1) - ->and(Settings::get('foo'))->toBe('updated value'); + ->and(SettingsFacade::get('foo'))->toBe('updated value'); }); it('removes persisted values from storage', function () { - Settings::set('foo', 'bar'); - Settings::set('bar', 'foo'); + SettingsFacade::set('foo', 'bar'); + SettingsFacade::set('bar', 'foo'); expect(DB::table('settings')->count())->toBe(2) - ->and(Settings::has('foo'))->toBeTrue() - ->and(Settings::has('bar'))->toBeTrue(); + ->and(SettingsFacade::has('foo'))->toBeTrue() + ->and(SettingsFacade::has('bar'))->toBeTrue(); - Settings::forget('foo'); + SettingsFacade::forget('foo'); expect(DB::table('settings')->count())->toBe(1) - ->and(Settings::has('foo'))->toBeFalse() - ->and(Settings::has('bar'))->toBeTrue(); + ->and(SettingsFacade::has('foo'))->toBeFalse() + ->and(SettingsFacade::has('bar'))->toBeTrue(); }); it('can evaluate stored boolean settings', function () { - Settings::set('app.debug', '1'); - expect(Settings::isTrue('app.debug'))->toBeTrue(); + SettingsFacade::set('app.debug', '1'); + expect(SettingsFacade::isTrue('app.debug'))->toBeTrue(); - Settings::set('app.debug', '0'); - expect(Settings::isTrue('app.debug'))->toBeFalse() - ->and(Settings::isFalse('app.debug'))->toBeTrue(); + SettingsFacade::set('app.debug', '0'); + expect(SettingsFacade::isTrue('app.debug'))->toBeFalse() + ->and(SettingsFacade::isFalse('app.debug'))->toBeTrue(); - Settings::set('app.debug', true); - expect(Settings::isTrue('app.debug'))->toBeTrue() - ->and(Settings::isFalse('app.debug'))->toBeFalse(); + SettingsFacade::set('app.debug', true); + expect(SettingsFacade::isTrue('app.debug'))->toBeTrue() + ->and(SettingsFacade::isFalse('app.debug'))->toBeFalse(); }); it('can cache values on retrieval', function () { enableSettingsCache(); - Settings::set('foo', 'bar'); + SettingsFacade::set('foo', 'bar'); resetQueryCount(); - expect(Settings::get('foo'))->toBe('bar'); + expect(SettingsFacade::get('foo'))->toBe('bar'); assertQueryCount(1); resetQueryCount(); - expect(Settings::get('foo'))->toBe('bar'); + expect(SettingsFacade::get('foo'))->toBe('bar'); assertQueryCount(0); }); it('flushes the cache when updating a value', function () { enableSettingsCache(); - Settings::set('foo', 'bar'); + SettingsFacade::set('foo', 'bar'); resetQueryCount(); - expect(Settings::get('foo'))->toBe('bar'); + expect(SettingsFacade::get('foo'))->toBe('bar'); assertQueryCount(1); resetQueryCount(); - expect(Settings::get('foo'))->toBe('bar'); + expect(SettingsFacade::get('foo'))->toBe('bar'); assertQueryCount(0); - Settings::set('foo', 'updated value'); + SettingsFacade::set('foo', 'updated value'); resetQueryCount(); - expect(Settings::get('foo'))->toBe('updated value'); + expect(SettingsFacade::get('foo'))->toBe('updated value'); assertQueryCount(1); }); it('does not invalidate other cached settings when updating a value', function () { enableSettingsCache(); - Settings::set('foo', 'bar'); - Settings::set('bar', 'foo'); + SettingsFacade::set('foo', 'bar'); + SettingsFacade::set('bar', 'foo'); resetQueryCount(); - expect(Settings::get('foo'))->toBe('bar') - ->and(Settings::get('bar'))->toBe('foo'); + expect(SettingsFacade::get('foo'))->toBe('bar') + ->and(SettingsFacade::get('bar'))->toBe('foo'); assertQueryCount(2); resetQueryCount(); - expect(Settings::get('foo'))->toBe('bar') - ->and(Settings::get('bar'))->toBe('foo'); + expect(SettingsFacade::get('foo'))->toBe('bar') + ->and(SettingsFacade::get('bar'))->toBe('foo'); assertQueryCount(0); - Settings::set('foo', 'updated value'); + SettingsFacade::set('foo', 'updated value'); resetQueryCount(); - expect(Settings::get('foo'))->toBe('updated value') - ->and(Settings::get('bar'))->toBe('foo'); + expect(SettingsFacade::get('foo'))->toBe('updated value') + ->and(SettingsFacade::get('bar'))->toBe('foo'); assertQueryCount(1); }); test('the boolean checks use cached values if cache is enabled', function () { enableSettingsCache(); - Settings::set('true.value', true); - Settings::set('false.value', false); + SettingsFacade::set('true.value', true); + SettingsFacade::set('false.value', false); resetQueryCount(); - expect(Settings::isTrue('true.value'))->toBeTrue() - ->and(Settings::isFalse('false.value'))->toBeTrue(); + expect(SettingsFacade::isTrue('true.value'))->toBeTrue() + ->and(SettingsFacade::isFalse('false.value'))->toBeTrue(); assertQueryCount(2); resetQueryCount(); - expect(Settings::isTrue('true.value'))->toBeTrue() - ->and(Settings::isFalse('false.value'))->toBeTrue(); + expect(SettingsFacade::isTrue('true.value'))->toBeTrue() + ->and(SettingsFacade::isFalse('false.value'))->toBeTrue(); assertQueryCount(0); }); it('does not use the cache if the cache is disabled', function () { - Settings::disableCache(); + SettingsFacade::disableCache(); DB::enableQueryLog(); - Settings::set('foo', 'bar'); + SettingsFacade::set('foo', 'bar'); resetQueryCount(); - expect(Settings::get('foo'))->toBe('bar'); + expect(SettingsFacade::get('foo'))->toBe('bar'); assertQueryCount(1); resetQueryCount(); - expect(Settings::get('foo'))->toBe('bar'); + expect(SettingsFacade::get('foo'))->toBe('bar'); assertQueryCount(1); }); it('can encrypt values', function () { - Settings::enableEncryption(); + SettingsFacade::enableEncryption(); - Settings::set('foo', 'bar'); + SettingsFacade::set('foo', 'bar'); $storedSetting = DB::table('settings')->first(); $unEncrypted = unserialize(decrypt($storedSetting->value)); @@ -229,21 +229,21 @@ }); it('can decrypt values', function () { - Settings::enableEncryption(); + SettingsFacade::enableEncryption(); - Settings::set('foo', 'bar'); + SettingsFacade::set('foo', 'bar'); // The stored value will be encrypted and not retrieve serialized yet if encryption // is enabled. $storedSetting = DB::table('settings')->first(); expect(isSerialized($storedSetting->value))->toBeFalse() - ->and(Settings::get('foo'))->toBe('bar'); + ->and(SettingsFacade::get('foo'))->toBe('bar'); }); it('does not encrypt if encryption is disabled', function () { - Settings::disableEncryption(); + SettingsFacade::disableEncryption(); - Settings::set('foo', 'bar'); + SettingsFacade::set('foo', 'bar'); $storedSetting = DB::table('settings')->first(); @@ -252,13 +252,16 @@ }); it('does not try to decrypt if encryption is disabled', function () { - Settings::enableEncryption(); - Settings::set('foo', 'bar'); + SettingsFacade::enableEncryption(); + SettingsFacade::set('foo', 'bar'); - Settings::disableEncryption(); + SettingsFacade::disableEncryption(); - expect(Settings::get('foo'))->not()->toBe('bar') - ->and(Settings::get('foo'))->not()->toBe(serialize('bar')); + $value = SettingsFacade::get('foo'); + + expect($value) + ->not->toBe('bar') + ->not->toBe(serialize('bar')); }); // Helpers... @@ -270,7 +273,7 @@ function assertQueryCount(int $expected): void function enableSettingsCache(): void { - Settings::enableCache(); + SettingsFacade::enableCache(); DB::connection()->enableQueryLog(); } diff --git a/tests/Support/Models/User.php b/tests/Support/Models/User.php index 9ab61d9..38330ec 100644 --- a/tests/Support/Models/User.php +++ b/tests/Support/Models/User.php @@ -14,6 +14,11 @@ class User extends Model use HasFactory; use HasSettings; + public function getMorphClass(): string + { + return 'user'; + } + protected static function newFactory(): UserFactory { return new UserFactory; diff --git a/tests/Unit/ContextSerializerTest.php b/tests/Unit/ContextSerializers/ContextSerializerTest.php similarity index 86% rename from tests/Unit/ContextSerializerTest.php rename to tests/Unit/ContextSerializers/ContextSerializerTest.php index 1859660..a6d9255 100644 --- a/tests/Unit/ContextSerializerTest.php +++ b/tests/Unit/ContextSerializers/ContextSerializerTest.php @@ -3,7 +3,7 @@ declare(strict_types=1); use Rawilk\Settings\Support\Context; -use Rawilk\Settings\Support\ContextSerializer; +use Rawilk\Settings\Support\ContextSerializers\ContextSerializer; it('accepts a context argument', function () { $context = (new Context)->set('a', 'a'); diff --git a/tests/Unit/ContextSerializers/DotNotationContextSerializerTest.php b/tests/Unit/ContextSerializers/DotNotationContextSerializerTest.php new file mode 100644 index 0000000..27c1cbc --- /dev/null +++ b/tests/Unit/ContextSerializers/DotNotationContextSerializerTest.php @@ -0,0 +1,33 @@ +serializer = new DotNotationContextSerializer; +}); + +it('serializes a context object to dot notation', function () { + $context = new Context([ + 'model' => User::class, + 'id' => 1, + 'bool_value' => true, + ]); + + expect($this->serializer->serialize($context))->toBe('model:user::id:1::bool_value:1'); +}); + +it('serializes null values to an empty string', function () { + expect($this->serializer->serialize(null))->toBe(''); +}); + +it('handles false boolean values', function () { + $context = new Context([ + 'bool-value' => false, + ]); + + expect($this->serializer->serialize($context))->toBe('bool-value:0'); +}); diff --git a/tests/Unit/ContextTest.php b/tests/Unit/ContextTest.php index 92ea4b4..e61991c 100644 --- a/tests/Unit/ContextTest.php +++ b/tests/Unit/ContextTest.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use Rawilk\Settings\Exceptions\InvalidContextValue; use Rawilk\Settings\Support\Context; it('serializes values when created', function () { @@ -34,3 +35,21 @@ $context = new Context; $context->get('test'); })->throws(OutOfBoundsException::class); + +it('can be converted to an array', function () { + $context = new Context(['id' => 1, 'model' => 'User']); + + expect($context->toArray()) + ->toBeArray() + ->toMatchArray([ + 'id' => 1, + 'model' => 'User', + ]); +}); + +it('only accepts numeric, string, or boolean values', function () { + new Context([ + 'id' => 1, + 'invalid-key' => ['array'], + ]); +})->throws(InvalidContextValue::class, 'invalid-key'); diff --git a/tests/Unit/KeyGeneratorTest.php b/tests/Unit/KeyGeneratorTest.php deleted file mode 100644 index f758b88..0000000 --- a/tests/Unit/KeyGeneratorTest.php +++ /dev/null @@ -1,24 +0,0 @@ -shouldReceive('serialize') - ->with($context) - ->andReturn('serialized'); - - $generator = new KeyGenerator($serializer); - - expect($generator->generate('key', $context))->toBe(md5('keyserialized')); -}); diff --git a/tests/Unit/KeyGenerators/Md5KeyGeneratorTest.php b/tests/Unit/KeyGenerators/Md5KeyGeneratorTest.php new file mode 100644 index 0000000..65a5fce --- /dev/null +++ b/tests/Unit/KeyGenerators/Md5KeyGeneratorTest.php @@ -0,0 +1,39 @@ +keyGenerator = (new Md5KeyGenerator) + ->setContextSerializer(new ContextSerializer); +}); + +it('generates an md5 hash of a key', function () { + // N; is for a serialized null context object + expect($this->keyGenerator->generate('my-key'))->toBe(md5('my-keyN;')); +}); + +it('generates an md5 hash of a key and context object', function () { + $context = new Context([ + 'id' => 123, + ]); + + expect($this->keyGenerator->generate('my-key', $context)) + ->toBe(md5('my-key' . serialize($context))); +}); + +it('works with other context serializers', function () { + $this->keyGenerator->setContextSerializer(new DotNotationContextSerializer); + + $context = new Context([ + 'id' => 123, + 'bool-value' => false, + ]); + + expect($this->keyGenerator->generate('my-key', $context)) + ->toBe(md5('my-keyid:123::bool-value:0')); +}); diff --git a/tests/Unit/KeyGenerators/ReadableKeyGeneratorTest.php b/tests/Unit/KeyGenerators/ReadableKeyGeneratorTest.php new file mode 100644 index 0000000..87527b5 --- /dev/null +++ b/tests/Unit/KeyGenerators/ReadableKeyGeneratorTest.php @@ -0,0 +1,42 @@ +keyGenerator = (new ReadableKeyGenerator) + ->setContextSerializer(new DotNotationContextSerializer); +}); + +it('generates a key without context', function (string $key, string $expectedKey) { + expect($this->keyGenerator->generate($key))->toBe($expectedKey); +})->with([ + ['my-key', 'my-key'], + ['my key', 'my-key'], + ['MY key', 'my-key'], + ['my.key', 'my.key'], +]); + +it('generates a key with context', function () { + $context = new Context([ + 'id' => 123, + 'model' => User::class, + ]); + + expect($this->keyGenerator->generate('app.timezone', $context))->toBe('app.timezone:c:::id:123::model:user'); +}); + +it('works with other context serializers', function () { + $this->keyGenerator->setContextSerializer(new ContextSerializer); + + $context = new Context([ + 'id' => 123, + ]); + + expect($this->keyGenerator->generate('my-key', $context))->toBe('my-key:c:::' . serialize($context)); +}); diff --git a/tests/Unit/ValueSerializerTest.php b/tests/Unit/ValueSerializerTest.php index f55370b..44cb732 100644 --- a/tests/Unit/ValueSerializerTest.php +++ b/tests/Unit/ValueSerializerTest.php @@ -15,13 +15,19 @@ $serialized = serialize($value); - expect($serializer->unserialize($serialized))->toEqual($value); + if (is_object($value)) { + expect($serializer->unserialize($serialized))->toBeObject(); + } else { + expect($serializer->unserialize($serialized))->toEqual($value); + } })->with('values'); dataset('values', [ null, 1, 1.1, + true, + false, 'string', ['array' => 'array'], (object) ['a' => 'b'],