From 4f8557ceef282842c75466d13f75347c20156d7d Mon Sep 17 00:00:00 2001 From: Martin Kluska Date: Thu, 22 Dec 2022 17:39:06 +0100 Subject: [PATCH] feat(Context): Add and use ContextServiceContract instead of a implementation class + improve phpstan support - Add ContextServiceContractAssert - ContextServiceContract is a singleton BREAKING CHANGE: AbstractContext uses ContextServiceContract instead of a class. --- src/Context/ContextServiceProvider.php | 3 + src/Context/Contexts/AbstractContext.php | 4 +- src/Context/Contexts/AbstractIsContext.php | 7 +- .../Contracts/ContextServiceContract.php | 46 ++++++ src/Context/Services/ContextCallService.php | 22 --- src/Context/Services/ContextEventsService.php | 3 +- src/Context/Services/ContextService.php | 27 +--- .../CacheMeServiceContractAssert.php | 4 +- .../CacheMeServiceContractGetExpectation.php | 5 + .../ContextServiceContractAssert.php | 130 +++++++++++++++++ ...ontextServiceContractDeleteExpectation.php | 20 +++ ...tServiceContractGetCacheKeyExpectation.php | 21 +++ .../ContextServiceContractGetExpectation.php | 23 +++ .../ContextServiceContractIsExpectation.php | 23 +++ .../ContextServiceContractSetExpectation.php | 22 +++ ...viceContractSetWithoutCacheExpectation.php | 22 +++ .../Context/ContextServiceProviderTest.php | 40 ++++++ .../Context/Services/ContextServiceTest.php | 92 ++++++++++++ tests/Feature/Context/Services/IsContext.php | 26 ++++ .../Services/TestDependencyContext.php | 35 +++++ .../Services/TestNoDependencyContext.php | 32 +++++ tests/Feature/Context/Services/TestValue.php | 20 +++ .../ContextServiceContractAssertTest.php | 134 ++++++++++++++++++ 23 files changed, 710 insertions(+), 51 deletions(-) create mode 100644 src/Context/Contracts/ContextServiceContract.php delete mode 100644 src/Context/Services/ContextCallService.php create mode 100644 src/Testing/Context/Contracts/ContextServiceContractAssert.php create mode 100644 src/Testing/Context/Contracts/ContextServiceContractDeleteExpectation.php create mode 100644 src/Testing/Context/Contracts/ContextServiceContractGetCacheKeyExpectation.php create mode 100644 src/Testing/Context/Contracts/ContextServiceContractGetExpectation.php create mode 100644 src/Testing/Context/Contracts/ContextServiceContractIsExpectation.php create mode 100644 src/Testing/Context/Contracts/ContextServiceContractSetExpectation.php create mode 100644 src/Testing/Context/Contracts/ContextServiceContractSetWithoutCacheExpectation.php create mode 100644 tests/Feature/Context/ContextServiceProviderTest.php create mode 100644 tests/Feature/Context/Services/ContextServiceTest.php create mode 100644 tests/Feature/Context/Services/IsContext.php create mode 100644 tests/Feature/Context/Services/TestDependencyContext.php create mode 100644 tests/Feature/Context/Services/TestNoDependencyContext.php create mode 100644 tests/Feature/Context/Services/TestValue.php create mode 100644 tests/Unit/Testing/Context/Contracts/ContextServiceContractAssertTest.php diff --git a/src/Context/ContextServiceProvider.php b/src/Context/ContextServiceProvider.php index 5b1c4073..8dc52a56 100644 --- a/src/Context/ContextServiceProvider.php +++ b/src/Context/ContextServiceProvider.php @@ -5,7 +5,9 @@ namespace LaraStrict\Context; use Illuminate\Support\ServiceProvider; +use LaraStrict\Context\Contracts\ContextServiceContract; use LaraStrict\Context\Services\ContextEventsService; +use LaraStrict\Context\Services\ContextService; class ContextServiceProvider extends ServiceProvider { @@ -16,5 +18,6 @@ public function register(): void // Make the service context singleton - if we are using heavy dependency injection it will slow down // resolving if not singleton $this->app->singleton(ContextEventsService::class, ContextEventsService::class); + $this->app->singleton(ContextServiceContract::class, ContextService::class); } } diff --git a/src/Context/Contexts/AbstractContext.php b/src/Context/Contexts/AbstractContext.php index 8be9b80c..d26860ff 100644 --- a/src/Context/Contexts/AbstractContext.php +++ b/src/Context/Contexts/AbstractContext.php @@ -4,9 +4,9 @@ namespace LaraStrict\Context\Contexts; +use LaraStrict\Context\Contracts\ContextServiceContract; use LaraStrict\Context\Contracts\ContextValueContract; use LaraStrict\Context\Services\ContextEventsService; -use LaraStrict\Context\Services\ContextService; /** * Context allows us to access data across multiple services / actions without loading data again using dependency @@ -22,7 +22,7 @@ public function getCacheTtl(): int return 3600; } - abstract public function get(ContextService $contextService): ContextValueContract; + abstract public function get(ContextServiceContract $contextService): ContextValueContract; abstract public function getCacheKey(): string; diff --git a/src/Context/Contexts/AbstractIsContext.php b/src/Context/Contexts/AbstractIsContext.php index 4c477ed4..3ef74239 100644 --- a/src/Context/Contexts/AbstractIsContext.php +++ b/src/Context/Contexts/AbstractIsContext.php @@ -5,7 +5,7 @@ namespace LaraStrict\Context\Contexts; use Closure; -use LaraStrict\Context\Services\ContextService; +use LaraStrict\Context\Contracts\ContextServiceContract; use LaraStrict\Context\Values\BoolContextValue; /** @@ -14,13 +14,14 @@ */ abstract class AbstractIsContext extends AbstractContext { - public function get(ContextService $contextService): BoolContextValue + public function get(ContextServiceContract $contextService): BoolContextValue { return $contextService->is($this, $this->is()); } /** - * @return Closure():bool + * @return Closure(mixed...):bool + * @phpstan-return Closure(mixed,mixed,mixed,mixed,mixed):bool */ abstract public function is(): Closure; } diff --git a/src/Context/Contracts/ContextServiceContract.php b/src/Context/Contracts/ContextServiceContract.php new file mode 100644 index 00000000..ced5b36b --- /dev/null +++ b/src/Context/Contracts/ContextServiceContract.php @@ -0,0 +1,46 @@ +container->call($createState); - } -} diff --git a/src/Context/Services/ContextEventsService.php b/src/Context/Services/ContextEventsService.php index c7f69ade..838554af 100644 --- a/src/Context/Services/ContextEventsService.php +++ b/src/Context/Services/ContextEventsService.php @@ -9,12 +9,13 @@ use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Database\Eloquent\Model; use LaraStrict\Context\Contexts\AbstractContext; +use LaraStrict\Context\Contracts\ContextServiceContract; class ContextEventsService { public function __construct( private readonly Dispatcher $eventsDispatcher, - private readonly ContextService $contextService, + private readonly ContextServiceContract $contextService, private readonly Container $container ) { } diff --git a/src/Context/Services/ContextService.php b/src/Context/Services/ContextService.php index e9ebdeac..8d75ff22 100644 --- a/src/Context/Services/ContextService.php +++ b/src/Context/Services/ContextService.php @@ -5,7 +5,6 @@ namespace LaraStrict\Context\Services; use Closure; -use Illuminate\Cache\Repository; use Illuminate\Contracts\Container\Container; use LaraStrict\Cache\Contracts\CacheMeServiceContract; use LaraStrict\Cache\Enums\CacheMeStrategy; @@ -13,20 +12,16 @@ use LaraStrict\Context\Concerns\UseCacheWithTags; use LaraStrict\Context\Contexts\AbstractContext; use LaraStrict\Context\Contexts\AbstractIsContext; +use LaraStrict\Context\Contracts\ContextServiceContract; use LaraStrict\Context\Contracts\ContextValueContract; use LaraStrict\Context\Values\BoolContextValue; use LaraStrict\Core\Services\ImplementsService; -/** - * Shareable context values between logic that needs same data. Stored in memory and if context supports its it will - * stores in cache repository (only usable with boot). - */ -class ContextService +class ContextService implements ContextServiceContract { protected const TAG = 'context'; public function __construct( - private readonly ContextCallService $callService, private readonly CacheMeServiceContract $cacheMeManager, private readonly ImplementsService $implementsService ) { @@ -67,36 +62,24 @@ public function setWithoutCache(AbstractContext $context, ContextValueContract $ ); } - /** - * @template T of ContextValueContract - * - * @param Closure(mixed,mixed,mixed): T $createState Create the state - * - * @return T - */ public function get(AbstractContext $context, Closure $createState): ContextValueContract { $fullCacheKey = $this->getCacheKey($context); return $this->cacheMeManager->get( key: $fullCacheKey, - getValue: fn () => $this->callService->createState($context, $createState), + getValue: $createState, tags: $this->getTags($context), minutes: $context->getCacheTtl(), strategy: $this->cacheStrategy($context) ); } - /** - * Returns bool state of the context. - * - * @param Closure():bool $is - */ public function is(AbstractIsContext $context, Closure $is): BoolContextValue { return $this->get( - $context, - static fn (Container $container) => new BoolContextValue((bool) $container->call($is)) + context: $context, + createState: static fn (Container $container) => new BoolContextValue((bool) $container->call($is)) ); } diff --git a/src/Testing/Cache/Contracts/CacheMeServiceContractAssert.php b/src/Testing/Cache/Contracts/CacheMeServiceContractAssert.php index ba892288..d4c38f0e 100644 --- a/src/Testing/Cache/Contracts/CacheMeServiceContractAssert.php +++ b/src/Testing/Cache/Contracts/CacheMeServiceContractAssert.php @@ -59,7 +59,9 @@ public function get( Assert::assertEquals($expectation->minutes, $minutes, $message); Assert::assertEquals($expectation->strategy, $strategy, $message); - return $getValue(); + $callGetValueHook = $expectation->callGetValueHook; + + return $callGetValueHook instanceof Closure === false ? $getValue() : $callGetValueHook($getValue); } /** diff --git a/src/Testing/Cache/Contracts/CacheMeServiceContractGetExpectation.php b/src/Testing/Cache/Contracts/CacheMeServiceContractGetExpectation.php index d3f2b5a8..b20a32eb 100644 --- a/src/Testing/Cache/Contracts/CacheMeServiceContractGetExpectation.php +++ b/src/Testing/Cache/Contracts/CacheMeServiceContractGetExpectation.php @@ -4,16 +4,21 @@ namespace LaraStrict\Testing\Cache\Contracts; +use Closure; use LaraStrict\Cache\Constants\CacheExpirations; use LaraStrict\Cache\Enums\CacheMeStrategy; final class CacheMeServiceContractGetExpectation { + /** + * @param Closure(Closure):mixed|null $callGetValueHook + */ public function __construct( public readonly string $key, public readonly array $tags = [], public readonly int $minutes = CacheExpirations::HalfDay, public readonly CacheMeStrategy $strategy = CacheMeStrategy::MemoryAndRepository, + public readonly ?Closure $callGetValueHook = null ) { } } diff --git a/src/Testing/Context/Contracts/ContextServiceContractAssert.php b/src/Testing/Context/Contracts/ContextServiceContractAssert.php new file mode 100644 index 00000000..cab4207b --- /dev/null +++ b/src/Testing/Context/Contracts/ContextServiceContractAssert.php @@ -0,0 +1,130 @@ + $delete + * @param array $set + * @param array $setWithoutCache + * @param array $get + * @param array $is + * @param array $getCacheKey + */ + public function __construct( + array $delete = [], + array $set = [], + array $setWithoutCache = [], + array $get = [], + array $is = [], + array $getCacheKey = [], + ) { + $this->setExpectations(ContextServiceContractDeleteExpectation::class, array_values(array_filter($delete))); + $this->setExpectations(ContextServiceContractSetExpectation::class, array_values(array_filter($set))); + $this->setExpectations( + ContextServiceContractSetWithoutCacheExpectation::class, + array_values(array_filter($setWithoutCache)) + ); + $this->setExpectations(ContextServiceContractGetExpectation::class, array_values(array_filter($get))); + $this->setExpectations(ContextServiceContractIsExpectation::class, array_values(array_filter($is))); + $this->setExpectations( + ContextServiceContractGetCacheKeyExpectation::class, + array_values(array_filter($getCacheKey)) + ); + } + + public function delete(AbstractContext $context): void + { + $expectation = $this->getExpectation(ContextServiceContractDeleteExpectation::class); + $message = $this->getDebugMessage(); + + Assert::assertEquals($expectation->context, $context, $message); + + if (is_callable($expectation->hook)) { + call_user_func($expectation->hook, $context, $expectation); + } + } + + public function set(AbstractContext $context, ContextValueContract $value): void + { + $expectation = $this->getExpectation(ContextServiceContractSetExpectation::class); + $message = $this->getDebugMessage(); + + Assert::assertEquals($expectation->context, $context, $message); + Assert::assertEquals($expectation->value, $value, $message); + + if (is_callable($expectation->hook)) { + call_user_func($expectation->hook, $context, $value, $expectation); + } + } + + public function setWithoutCache(AbstractContext $context, ContextValueContract $value): void + { + $expectation = $this->getExpectation(ContextServiceContractSetWithoutCacheExpectation::class); + $message = $this->getDebugMessage(); + + Assert::assertEquals($expectation->context, $context, $message); + Assert::assertEquals($expectation->value, $value, $message); + + if (is_callable($expectation->hook)) { + call_user_func($expectation->hook, $context, $value, $expectation); + } + } + + public function get(AbstractContext $context, Closure $createState): ContextValueContract + { + $expectation = $this->getExpectation(ContextServiceContractGetExpectation::class); + $message = $this->getDebugMessage(); + + Assert::assertEquals($expectation->context, $context, $message); + Assert::assertEquals($expectation->createState, $createState, $message); + + if (is_callable($expectation->hook)) { + call_user_func($expectation->hook, $context, $createState, $expectation); + } + + /** @phpstan-ignore-next-line */ + return $expectation->return; + } + + public function is(AbstractIsContext $context, Closure $is): BoolContextValue + { + $expectation = $this->getExpectation(ContextServiceContractIsExpectation::class); + $message = $this->getDebugMessage(); + + Assert::assertEquals($expectation->context, $context, $message); + Assert::assertEquals($expectation->is, $is, $message); + + if (is_callable($expectation->hook)) { + call_user_func($expectation->hook, $context, $is, $expectation); + } + + return $expectation->return; + } + + public function getCacheKey(AbstractContext $context): string + { + $expectation = $this->getExpectation(ContextServiceContractGetCacheKeyExpectation::class); + $message = $this->getDebugMessage(); + + Assert::assertEquals($expectation->context, $context, $message); + + if (is_callable($expectation->hook)) { + call_user_func($expectation->hook, $context, $expectation); + } + + return $expectation->return; + } +} diff --git a/src/Testing/Context/Contracts/ContextServiceContractDeleteExpectation.php b/src/Testing/Context/Contracts/ContextServiceContractDeleteExpectation.php new file mode 100644 index 00000000..e88445e9 --- /dev/null +++ b/src/Testing/Context/Contracts/ContextServiceContractDeleteExpectation.php @@ -0,0 +1,20 @@ +assertBindings( + application: $this->app(), + expectedMap: [ + ContextEventsService::class => ContextEventsService::class, + ContextServiceContract::class => ContextService::class, + ], + registerServiceProvider: ContextServiceProvider::class + ); + } + + public function testSingletons(): void + { + $this->assertSingletons( + application: $this->app(), + expectedMap: [ContextEventsService::class, ContextServiceContract::class], + registerServiceProvider: ContextServiceProvider::class + ); + } +} diff --git a/tests/Feature/Context/Services/ContextServiceTest.php b/tests/Feature/Context/Services/ContextServiceTest.php new file mode 100644 index 00000000..5687cd13 --- /dev/null +++ b/tests/Feature/Context/Services/ContextServiceTest.php @@ -0,0 +1,92 @@ + $self->assert( + context: new TestDependencyContext(value: 'test'), + assert: static fn (TestValue $value) => $self->assertEquals('test', $value->value), + callHook: static fn (Closure $getValue) => $getValue('test'), + expectedCacheKey: 'Tests\LaraStrict\Feature\Context\Services\TestDependencyContext-test', + ), + ], + [ + static fn (self $self) => $self->assert( + context: new TestNoDependencyContext(value: 'test'), + assert: static fn (TestValue $value) => $self->assertEquals('test', $value->value), + callHook: static fn (Closure $getValue) => $getValue(), + expectedCacheKey: 'Tests\LaraStrict\Feature\Context\Services\TestNoDependencyContext-test', + ), + ], + [ + static fn (self $self) => $self->assert( + context: new IsContext(id: 1), + assert: static fn (BoolContextValue $value) => $self->assertEquals(true, $value->isValid()), + callHook: static fn (Closure $getValue) => $getValue( + new TestingContainer( + call: static fn (Closure $getValue): bool => $getValue(true) + ) + ), + expectedCacheKey: 'Tests\LaraStrict\Feature\Context\Services\IsContext-1', + ), + ], + [ + static fn (self $self) => $self->assert( + context: new IsContext(id: 1), + assert: static fn (BoolContextValue $value) => $self->assertEquals(false, $value->isValid()), + callHook: static fn (Closure $getValue) => $getValue( + new TestingContainer( + call: static fn (Closure $getValue): bool => $getValue(false) + ) + ), + expectedCacheKey: 'Tests\LaraStrict\Feature\Context\Services\IsContext-1', + ), + ], + ]; + } + + public function assert( + AbstractContext $context, + Closure $assert, + Closure $callHook, + string $expectedCacheKey, + ): void { + $service = new ContextService( + cacheMeManager: new CacheMeServiceContractAssert(get: [ + new CacheMeServiceContractGetExpectation( + key: $expectedCacheKey, + tags: ['context'], + minutes: 3600, + strategy: CacheMeStrategy::Memory, + callGetValueHook: $callHook + ), + ]), + implementsService: new ImplementsService() + ); + + $value = $context->get($service); + + $assert($value); + } +} diff --git a/tests/Feature/Context/Services/IsContext.php b/tests/Feature/Context/Services/IsContext.php new file mode 100644 index 00000000..db96dde3 --- /dev/null +++ b/tests/Feature/Context/Services/IsContext.php @@ -0,0 +1,26 @@ + $value; + } + + public function getCacheKey(): string + { + return (string) $this->id; + } +} diff --git a/tests/Feature/Context/Services/TestDependencyContext.php b/tests/Feature/Context/Services/TestDependencyContext.php new file mode 100644 index 00000000..75a6df22 --- /dev/null +++ b/tests/Feature/Context/Services/TestDependencyContext.php @@ -0,0 +1,35 @@ +get( + context: $this, + createState: fn (string $dependency): TestValue => new TestValue($this->value) + ); + + Assert::assertEquals($this->value, $value->value); + Assert::assertInstanceOf(TestValue::class, $value); + + return $value; + } + + public function getCacheKey(): string + { + return $this->value; + } +} diff --git a/tests/Feature/Context/Services/TestNoDependencyContext.php b/tests/Feature/Context/Services/TestNoDependencyContext.php new file mode 100644 index 00000000..c7ed7972 --- /dev/null +++ b/tests/Feature/Context/Services/TestNoDependencyContext.php @@ -0,0 +1,32 @@ +get(context: $this, createState: fn (): TestValue => new TestValue($this->value)); + + Assert::assertEquals($this->value, $value->value); + Assert::assertInstanceOf(TestValue::class, $value); + + return $value; + } + + public function getCacheKey(): string + { + return $this->value; + } +} diff --git a/tests/Feature/Context/Services/TestValue.php b/tests/Feature/Context/Services/TestValue.php new file mode 100644 index 00000000..a6b34b7c --- /dev/null +++ b/tests/Feature/Context/Services/TestValue.php @@ -0,0 +1,20 @@ + new ContextServiceContractAssert(delete: [ + new ContextServiceContractDeleteExpectation(context: $context), + ]), + call: static fn (ContextServiceContractAssert $assert) => $assert->delete(context: $context), + ), + new AssertExpectationEntity( + methodName: 'set', + createAssert: static fn () => new ContextServiceContractAssert(set: [ + new ContextServiceContractSetExpectation(context: $context, value: $value), + ]), + call: static fn (ContextServiceContractAssert $assert) => $assert->set( + context: $context, + value: $value + ), + ), + new AssertExpectationEntity( + methodName: 'setWithoutCache', + createAssert: static fn () => new ContextServiceContractAssert(setWithoutCache: [ + new ContextServiceContractSetWithoutCacheExpectation(context: $context, value: $value), + ]), + call: static fn (ContextServiceContractAssert $assert) => $assert->setWithoutCache( + context: $context, + value: $value + ), + ), + new AssertExpectationEntity( + methodName: 'get', + createAssert: fn () => new ContextServiceContractAssert(get: [ + new ContextServiceContractGetExpectation( + return: $value, + context: $context, + createState: static function () { + }, + hook: function ( + AbstractContext $context, + Closure $createState, + ContextServiceContractGetExpectation $expectation + ) use ($value): void { + $this->assertSame($value, $createState('test')); + } + ), + ]), + call: fn (ContextServiceContractAssert $assert) => $assert->get( + context: $context, + createState: function (string $string) use ($value): TestValue { + $this->assertEquals(expected: 'test', actual: $string); + return $value; + } + ), + checkResult: true, + expectedResult: $value + ), + new AssertExpectationEntity( + methodName: 'is', + createAssert: fn () => new ContextServiceContractAssert(is: [ + new ContextServiceContractIsExpectation( + return: $boolValue, + context: $isContext, + is: static function () { + }, + hook: function ( + AbstractContext $context, + Closure $is, + ContextServiceContractIsExpectation $expectation + ): void { + $this->assertTrue(condition: $is('test')); + } + ), + ]), + call: fn (ContextServiceContractAssert $assert) => $assert->is( + context: $isContext, + is: function (string $string): bool { + $this->assertEquals(expected: 'test', actual: $string); + return true; + } + ), + checkResult: true, + expectedResult: $boolValue + ), + new AssertExpectationEntity( + methodName: 'getCacheKey', + createAssert: static fn () => new ContextServiceContractAssert(getCacheKey: [ + new ContextServiceContractGetCacheKeyExpectation(return: 'key', context: $context), + ]), + call: static fn (ContextServiceContractAssert $assert) => $assert->getCacheKey(context: $context), + checkResult: true, + expectedResult: 'key' + ), + ]; + } + + protected function createEmptyAssert(): AbstractExpectationCallsMap + { + return new ContextServiceContractAssert(); + } +}