diff --git a/src/Resources/ModelResolver.php b/src/Resources/ModelResolver.php index 47b2439..4e6759f 100644 --- a/src/Resources/ModelResolver.php +++ b/src/Resources/ModelResolver.php @@ -11,13 +11,47 @@ */ class ModelResolver { + /** + * Per-process cache of resolved model classes, keyed by resource class. + * + * @var array + */ + private static array $cache = []; + /** * Resolve the model class for a given Buildora resource class. * + * Results are cached per process so that repeated resolutions of the + * same resource class (e.g. across a single request) don't redo the + * same reflection/config lookups and validation. + * * @param string $resourceClass The fully qualified class name of the resource. * @return string The fully qualified class name of the associated model. */ public static function resolve(string $resourceClass): string + { + return self::$cache[$resourceClass] ??= self::doResolve($resourceClass); + } + + /** + * Clear the per-process resolution cache. + * + * Mainly useful in tests to avoid state leaking between test cases. + * + * @return void + */ + public static function clearCache(): void + { + self::$cache = []; + } + + /** + * Perform the actual resolution of the model class for a resource. + * + * @param string $resourceClass The fully qualified class name of the resource. + * @return string The fully qualified class name of the associated model. + */ + private static function doResolve(string $resourceClass): string { if (method_exists($resourceClass, 'modelClass')) { $modelClass = $resourceClass::modelClass(); diff --git a/tests/Fixtures/ModelResolverCacheTestModel.php b/tests/Fixtures/ModelResolverCacheTestModel.php new file mode 100644 index 0000000..8837584 --- /dev/null +++ b/tests/Fixtures/ModelResolverCacheTestModel.php @@ -0,0 +1,16 @@ +assertSame(ModelResolverCacheTestModel::class, $modelClass); + } + + /** @test */ + public function it_caches_the_resolution_so_repeated_calls_do_not_redo_the_work(): void + { + ModelResolver::resolve(ModelResolverCacheTestResource::class); + ModelResolver::resolve(ModelResolverCacheTestResource::class); + $modelClass = ModelResolver::resolve(ModelResolverCacheTestResource::class); + + // modelClass() should only have been invoked once: subsequent calls + // must be served from the per-process cache. + $this->assertSame(1, ModelResolverCacheTestResource::$callCount); + $this->assertSame(ModelResolverCacheTestModel::class, $modelClass); + } + + /** @test */ + public function it_keeps_separate_cache_entries_per_resource_class(): void + { + ModelResolver::resolve(ModelResolverCacheTestResource::class); + ModelResolver::resolve(ModelResolverCacheTestResourceTwo::class); + ModelResolver::resolve(ModelResolverCacheTestResource::class); + ModelResolver::resolve(ModelResolverCacheTestResourceTwo::class); + + $this->assertSame(1, ModelResolverCacheTestResource::$callCount); + $this->assertSame(1, ModelResolverCacheTestResourceTwo::$callCount); + } + + /** @test */ + public function clear_cache_forces_the_next_call_to_resolve_again(): void + { + ModelResolver::resolve(ModelResolverCacheTestResource::class); + ModelResolver::clearCache(); + ModelResolver::resolve(ModelResolverCacheTestResource::class); + + $this->assertSame(2, ModelResolverCacheTestResource::$callCount); + } +}