diff --git a/composer.json b/composer.json index e1f32a0..5a2095f 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,8 @@ "illuminate/database": "^10.0", "illuminate/support": "^10.0", "ramsey/uuid": "^4.7", - "hanneskod/classtools": "^0.1.0" + "hanneskod/classtools": "^0.1.0", + "symfony/finder": "^6.3" }, "require-dev": { "illuminate/events": "^10.0", @@ -37,7 +38,18 @@ } }, "scripts": { - "test": "phpunit --colors=always" + "test": "phpunit --colors=always", + "post-autoload-dump": [ + "@clear", + "@prepare" + ], + "clear": "@php vendor/bin/testbench package:purge-skeleton --ansi", + "prepare": "@php vendor/bin/testbench package:discover --ansi", + "build": "@php vendor/bin/testbench workbench:build --ansi", + "serve": [ + "@build", + "@php vendor/bin/testbench serve" + ] }, "minimum-stability": "dev", "prefer-stable": true, @@ -47,5 +59,12 @@ "Eloquence\\EloquenceServiceProvider" ] } + }, + "autoload-dev": { + "psr-4": { + "Workbench\\App\\": "workbench/app/", + "Workbench\\Database\\Factories\\": "workbench/database/factories/", + "Workbench\\Database\\Seeders\\": "workbench/database/seeders/" + } } } diff --git a/phpunit.xml b/phpunit.xml index a80d0e1..92e20c7 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -17,4 +17,7 @@ ./tests/Acceptance + + + diff --git a/src/EloquenceServiceProvider.php b/src/EloquenceServiceProvider.php index 76540e6..2dd3a0a 100644 --- a/src/EloquenceServiceProvider.php +++ b/src/EloquenceServiceProvider.php @@ -6,12 +6,25 @@ class EloquenceServiceProvider extends ServiceProvider { - /** - * Initialises the service provider, and here we attach our own blueprint - * resolver to the schema, so as to provide the enhanced functionality. - */ - public function boot() + public function boot(): void + { + $this->publishes([ + __DIR__.'/../config/eloquence.php' => config_path('eloquence.php'), + ], 'config'); + + $this->initialiseDbQueryLog(); + $this->initialiseCommands(); + } + + protected function initialiseDbQueryLog(): void { DBQueryLog::initialise(); } + + private function initialiseCommands(): void + { + $this->commands([ + Utilities\RebuildCaches::class, + ]); + } } diff --git a/src/Utilities/RebuildCaches.php b/src/Utilities/RebuildCaches.php new file mode 100644 index 0000000..3437304 --- /dev/null +++ b/src/Utilities/RebuildCaches.php @@ -0,0 +1,97 @@ + 'rebuildCountCache', + HasSums::class => 'rebuildSumCache', + ]; + + public function handle(): void + { + $path = $this->argument('path') ?? app_path(); + + $this->allModelsUsingCaches($path)->each(function(string $class) { + $traits = class_uses_recursive($class); + + foreach ($this->caches as $trait => $method) { + if (!in_array($trait, $traits)) continue; + + $class::$method(); + } + }); + } + + /** + * Returns only those models that are utilising eloquence cache mechanisms. + * + * @param string $path + * @return Collection + */ + private function allModelsUsingCaches(string $path): Collection + { + return collect(Finder::create()->files()->in($path)->name('*.php')) + ->filter(fn(SplFileInfo $file) => $file->getFilename()[0] === Str::upper($file->getFilename()[0])) + ->map(fn(SplFileInfo $file) => $this->fullyQualifiedClassName($file)) + ->filter(fn(string $class) => is_subclass_of($class, Model::class)) + ->filter(fn(string $class) => $this->usesCaches($class)); + } + + /** + * Determines the fully qualified class name of the provided file. + * + * @param SplFileInfo $file + * @return string + */ + private function fullyQualifiedClassName(SplFileInfo $file) + { + $tokens = \PhpToken::tokenize($file->getContents()); + $namespace = null; + $class = null; + + foreach ($tokens as $i => $token) { + if ($token->is(T_NAMESPACE)) { + $namespace = $tokens[$i+2]->text; + } + + if ($token->is(T_CLASS)) { + $class = $tokens[$i+2]->text; + } + + if ($namespace && $class) { + break; + } + } + + if (!$namespace || !$class) { + $this->error(sprintf('Could not find namespace or class in %s', $file->getRealPath())); + } + + return sprintf('%s\\%s', $namespace, $class); + } + + /** + * Returns true if the provided class uses any of the caches provided by Eloquence. + * + * @param string $class + * @return bool + */ + private function usesCaches(string $class): bool + { + return (bool) array_intersect(class_uses_recursive($class), array_keys($this->caches)); + } +} diff --git a/tests/Acceptance/AcceptanceTestCase.php b/tests/Acceptance/AcceptanceTestCase.php index 12aaf4a..038f533 100644 --- a/tests/Acceptance/AcceptanceTestCase.php +++ b/tests/Acceptance/AcceptanceTestCase.php @@ -1,6 +1,7 @@ init(); } + protected function getPackageProviders($app) + { + return [ + EloquenceServiceProvider::class, + ]; + } + protected function getEnvironmentSetUp($app) { $app['config']->set('database.default', 'test'); diff --git a/tests/Acceptance/RebuildCachesCommandTest.php b/tests/Acceptance/RebuildCachesCommandTest.php new file mode 100644 index 0000000..bdbdcad --- /dev/null +++ b/tests/Acceptance/RebuildCachesCommandTest.php @@ -0,0 +1,25 @@ +artisan('eloquence:rebuild-caches '.__DIR__.'/../../tests/Acceptance/Models'); + + $result->assertExitCode(0); + + $this->assertDatabaseHas('users', [ + 'post_count' => 5, + ]); + + $this->assertDatabaseHas('users', [ + 'post_count' => 2, + ]); + + $this->assertDatabaseHas('orders', [ + 'total_amount' => 30, + ]); + } +} \ No newline at end of file