diff --git a/README.md b/README.md index 8db561b..14a62f8 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Braden Collum - Unsplash (UL) #9HI8UJMSdZA](https://images.unsplash.com/photo-14 Get the best options to keep your application fast as ever, with just one line. -This package generates a PHP 7.4 preloading script from your Opcache statistics automatically. No need to hack your way in. +This package generates a [PHP 7.4 preloading](https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.preload) script from your Opcache statistics automatically. No need to hack your way in. ## Installation @@ -18,7 +18,7 @@ Require this using Composer into your project composer require darkghosthunter/preloader -> This package doesn't enforces `ext-opcache` at install. Just be sure to have it [enabled in your application server](https://www.php.net/manual/en/book.opcache.php). +> This package doesn't requires `ext-opcache` to install. Just be sure to have it [enabled in your application server](https://www.php.net/manual/en/book.opcache.php). ## Usage @@ -47,54 +47,69 @@ Then, tell PHP to use this file as a preloader at startup in your `php.ini`. opcache.preload=/www/app/preload.php ``` -Restart your PHP process using Opcache and that's all, you're good. +Restart your PHP process that's using Opcache, and that's all, you're good. > If you use Preloader when Opcache is disabled or without hits, you will get an Exception. ## How it works -This package will ask Opcache for statistics about what files are the most requested. +This package will ask Opcache for statistics about what files are the most requested. You can [check this article in Medium about that preload](https://medium.com/p/9ede756f292c/). Since the best statistics are those you get after your application has been running for a while, you can use your own mechanisms (or the ones provided by the class) to compile the list only after certain conditions are met. +![](https://miro.medium.com/max/1365/1*Zp-rR9-dPNn55L8GjSUpJg.png) + Don't worry, you can configure what and how compile the list. ## Configuration Yuo can configure the Preloader to run when a condition is met, limit the file list, and where to output the compiled preload list. -After the Opcache hits reach a certain number, the Preloader will generate the script, not before. - ### `when()` (optional) If you don't feel like using Opcache hits, you can just use `when()`. The Preloader will proceed when the variable being passed evaluates to `true`, which will be in your hands. - Preloader::make()->when(false); // You will never run, ha ha ha! +```php +Preloader::make()->when(false); // You will never run, ha ha ha! +``` You can also use a Closure (Arrow function or any other callable) that returns `true`. - Preloader::make()->when(fn () => $app->cache()->get('should_run')); - +```php +Preloader::make()->when(fn () => $app->cache()->get('should_run')); +``` #### `whenHits()` (optional) This is the best way to gather good statistics for a good preloading list if you don't know the real load of your application. - Preloader::make()->whenHits(200000): // After a given number of hits. +```php +Preloader::make()->whenHits(200000); // After a given number of hits. +``` + +The list will be generated when the number of hits set are **above** the reported by Opcache. + +> Watch out! If you're using `overwrite()`, the script will be regenerated every time after the number of hits are reached! #### `whenOneIn()` (optional) -This is another helper for conditioning the generation. The list will be generated one in a given number of chances (the higher is it, the more rare till be). +The list will be generated one in a given number of chances (the higher is it, the less often will run). - Prealoder::make()->whenOneIn(2000); // 1 in 2,000 chances. +```php +Preloader::make()->whenOneIn(2000); // 1 in 2,000 chances. +``` This may come in handy using it with `overwrite()` to constantly recreate the list. - Prealoder::make()->overwrite()->whenOneIn(2000); +```php +Preloader::make()->overwrite()->whenOneIn(2000); +``` ### `memory()` (optional, default) - Preloader::make()->memory(32); +```php +Preloader::make()->memory(32); +``` Set your memory limit in **MB**. The default of 32MB is enough for *most* applications. The Preloader will generate a list of files until that memory limit is reached. @@ -104,39 +119,66 @@ This takes into account the `memory_consumption` key of each script cached in Op ### `exclude()` (optional) -You can exclude from the list given by Opcache using `exclude()`, which accepts a single file or an array of files. +You can exclude files from the list given by Opcache using `exclude()`, which accepts a single file or an array of files. These are passed to the `glob()` method. These file paths **must be absolute**. - Preloader::make()->exclude(['foo.php', 'bar.php']); +```php +Preloader::make()->exclude([ + '/app/foo.php', + '/app/bar.php', + '/app/quz/*.php' +]); +``` + +These excluded files will be excluded from the list generation, and won't count for memory limits. -These excluded files will be excluded from the list generation, also excluding them from memory limits. +Preloader library files are automatically excluded. You can disable this using `includePreloader()`: -> Preloader library files are automatically excluded. +```php +Preloader::make()->includePreloader() + ->exclude([ + '/app/foo.php', + '/app/bar.php', + '/app/quz/*.php' + ]); +``` ### `append()` (optional) - Preloader::make()->append(['foo.php', 'bar.php']); +Of course you can add files using absolute paths manually to the preload script to be generated. Just issue them with `append()`. These are passed to the `glob()` method. These file paths **must be absolute**. -Of course you can add files using absolute paths manually to the preload script to be generated. Just issue them with `append()`. +```php +Preloader::make()->append([ + '/app/foo.php', + '/app/bar.php', + '/app/quz/*.php' +]); +``` -Prepending files will put them **after** the list generation, so they won't count list or memory limits. +Prepending files will put them **after** the list generation, so they won't count for the list memory limit. - Preloader::make()->top(0.5)->memory(64)->append('foo.bar'); +```php +Preloader::make()->memory(64)->append('foo.bar'); +``` > Any duplicated file appended will be ignored since the list will remove them automatically before compiling the script. ### `output()` (required) -We need to know where to output the script. It's recommended to do it in the same application folder, since most PHP processes will have access to write in it. If not, you're free to point out where. +We need to know where to output the script. It's recommended to do it in the same application folder, since most PHP processes will have access to write inside the same directory. If not, you're free to point out where. - Preloader::make()->output(__DIR__ . '/../../my-preloader.php'); +```php +Preloader::make()->output(__DIR__ . '/../../my-preloader.php'); +``` ### `overwrite()` (optional) -Sometimes you may have run your preloader script already. To avoid replacing the list with another one, Preloader by default doesn't do nothing when it detects the file already exists. +Sometimes you may have run your preloader script already. To avoid replacing the list with another one, Preloader by default doesn't do nothing when it detects the script file already exists. To change this behaviour, you can use the `overwrite()` method to instruct Preloader to always rewrite the file. - Preloader::make()->overwrite()->generate(); +```php +Preloader::make()->overwrite()->generate(); +``` > Watch out using this along conditions like `whenHits()` and `when()`. If the condition are true, the Preloader will overwrite the preload script... over and over and over again! @@ -144,9 +186,11 @@ To change this behaviour, you can use the `overwrite()` method to instruct Prelo Once your Preloader configuration is ready, you can generate the list using `generate()`. - Preloader::make()->generate(); +```php +Preloader::make()->generate(); +``` -This will automatically create a PHP-ready script to preload your application. It will return `true` on success, and `false` when the when the conditions are not met or an existing preload file exists. +This will automatically create a PHP-ready script to preload your application. It will return `true` on success, and `false` when the when the conditions are not met or an existing preload file exists that shouldn't be overwritten. ## Give me an example @@ -176,10 +220,6 @@ $weekAfterDeploy = $app->deploymentTimestamp() + (7*24*60*60); ->generate(); ``` -## Contributing - -Please see [CONTRIBUTING](CONTRIBUTING.md) for details. - ## Security If you discover any security related issues, please email darkghosthunter@gmail.com instead of using the issue tracker. diff --git a/src/Conditions.php b/src/Conditions.php index 98ac5be..33d4792 100644 --- a/src/Conditions.php +++ b/src/Conditions.php @@ -2,8 +2,6 @@ namespace DarkGhostHunter\Preloader; -use DateTime; - trait Conditions { /** @@ -14,14 +12,14 @@ trait Conditions protected bool $shouldRun = true; /** - * Run the Preloader script after Opcache hits reach certain number + * Run the Preloader script when Opcache hits reach certain number * * @param int $hits * @return $this */ public function whenHits(int $hits = 200000) : self { - return $this->when(fn () => $hits > $this->opcache->getHits()); + return $this->when($hits > $this->opcache->getHits()); } /** @@ -32,7 +30,7 @@ public function whenHits(int $hits = 200000) : self */ public function whenOneIn(int $chances = 100) : self { - return $this->when(fn () => random_int(1, $chances) === (int)floor($chances/2)); + return $this->when(random_int(1, $chances) === (int)floor($chances/2)); } /** diff --git a/src/GeneratesScript.php b/src/GeneratesScript.php index 054ddb3..4394d2b 100644 --- a/src/GeneratesScript.php +++ b/src/GeneratesScript.php @@ -15,7 +15,7 @@ protected function shouldWrite() } /** - * Returns a digestible opcache configuration + * Returns a digestible Opcache configuration * * @return array */ diff --git a/src/LimitsList.php b/src/LimitsList.php index f13dcf7..98853d2 100644 --- a/src/LimitsList.php +++ b/src/LimitsList.php @@ -2,8 +2,6 @@ namespace DarkGhostHunter\Preloader; -use RuntimeException; - trait LimitsList { /** diff --git a/src/ManagesFiles.php b/src/ManagesFiles.php index d9cc34b..b1157a2 100644 --- a/src/ManagesFiles.php +++ b/src/ManagesFiles.php @@ -4,6 +4,19 @@ trait ManagesFiles { + /** + * Include the Preloader files in the file list. + * + * @param bool $include + * @return $this + */ + public function includePreloader(bool $include = true) + { + $this->lister->includePreloader = $include; + + return $this; + } + /** * Append a list of files to the preload list. These won't count for file and memory limits. * @@ -12,7 +25,7 @@ trait ManagesFiles */ public function append($files) : self { - $this->lister->append = (array)$files; + $this->lister->append = $this->listFiles((array)$files); return $this; } @@ -25,8 +38,28 @@ public function append($files) : self */ public function exclude($files) : self { - $this->lister->exclude = (array)$files; + $this->lister->exclude = $this->listFiles((array)$files); return $this; } + + /** + * take every file string and pass it to the glob function. + * + * @param array $files + * @return array + */ + protected function listFiles(array $files) : array + { + $paths = []; + + // We will cycle trough each "file" and save the resulting array given by glob. + // If the glob returns false, we will trust the developer goodwill and add it + // anyway, since the file may not exists until the app generates something. + foreach ($files as $file) { + $paths[] = glob($file) ?: [$file]; + } + + return array_merge(...$paths); + } } diff --git a/src/Preloader.php b/src/Preloader.php index 6f737a6..74049c6 100644 --- a/src/Preloader.php +++ b/src/Preloader.php @@ -17,7 +17,7 @@ class Preloader * * @const */ - protected const STUB = __DIR__ . '/preload.php.stub'; + protected const STUB_LOCATION = __DIR__ . '/preload.php.stub'; /** * Determines if the preload file should be rewritten. @@ -119,7 +119,7 @@ public function generate() return false; } - $this->compiler->contents = file_get_contents(static::STUB); + $this->compiler->contents = file_get_contents(static::STUB_LOCATION); $this->compiler->opcacheConfig = $this->getOpcacheConfig(); $this->compiler->preloaderConfig = $this->getPreloaderConfig(); $this->compiler->list = $this->lister->build(); @@ -146,7 +146,7 @@ protected function canGenerate() } if (! $this->compiler->autoload) { - throw new LogicException('Cannot proceed without an Composer Autoload.'); + throw new LogicException('Cannot proceed without a Composer Autoload.'); } if (! file_exists($this->compiler->autoload)) { diff --git a/src/PreloaderLister.php b/src/PreloaderLister.php index d6d24b2..5162646 100644 --- a/src/PreloaderLister.php +++ b/src/PreloaderLister.php @@ -25,6 +25,13 @@ class PreloaderLister */ public array $exclude = []; + /** + * If the Preloader package should also be included. + * + * @var bool + */ + public bool $includePreloader = false; + /** * Opcache class access * @@ -136,7 +143,7 @@ protected function exclude(array $scripts) */ protected function excludedPackageFiles() { - return [ + return $this->includePreloader ? [] : [ realpath(__DIR__ . '/Conditions.php'), realpath(__DIR__ . '/GeneratesScript.php'), realpath(__DIR__ . '/LimitsList.php'), diff --git a/src/preload.php.stub b/src/preload.php.stub index 570a3a7..d8a4bf1 100644 --- a/src/preload.php.stub +++ b/src/preload.php.stub @@ -9,7 +9,8 @@ * * Add (or update) this line in `php.ini`: * - * opcache.preload=@output + * opcache.preload=@output + * * * --- Config --- * Generated at: @generated_at @@ -25,6 +26,10 @@ * - Overwrite: @preloader_overwrite * - Files excluded: @preloader_excluded * - Files appended: @preloader_appended + * + * + * For more information: + * @see https://github.com/darkghosthunter/preloader */ require_once '@autoload'; diff --git a/tests/PreloaderTest.php b/tests/PreloaderTest.php index 83fe594..c79c6cc 100644 --- a/tests/PreloaderTest.php +++ b/tests/PreloaderTest.php @@ -40,6 +40,23 @@ class PreloaderTest extends TestCase ] ]; + protected function clearWorkdir() + { + if (is_file($file = $this->workdir . '/preload.php')) { + unlink($file); + } + + if (is_dir($dir = $this->workdir . '/examples')) { + $files = glob( $dir . '/*', GLOB_MARK ); + + foreach ($files as $file) { + unlink($file); + } + + rmdir($dir); + } + } + protected function setUp() : void { parent::setUp(); @@ -49,6 +66,14 @@ protected function setUp() : void if (is_file($file = $this->workdir . '/preload.php')) { unlink($file); } + + if (is_dir($dir = $this->workdir . '/examples')) { + foreach (glob( $dir . '/*') as $file) { + unlink($file); + } + + rmdir($dir); + } } public function testCreatesInstance() @@ -302,6 +327,13 @@ public function testWhenHits() ->output($this->workdir . '/preload.php') ->generate() ); + + $this->assertFalse( + $preloader->whenHits(1002) + ->autoload($this->workdir . '/autoload.php') + ->output($this->workdir . '/preload.php') + ->generate() + ); } public function testWhenOneIn() @@ -459,6 +491,123 @@ public function testAppendsFilesWithoutAffectingLimits() $this->assertStringContainsString('Files appended: 2', $contents); } + public function testExcludesUsingGlobFormat() + { + $opcache = $this->createMock(Opcache::class); + + $opcache->method('isEnabled') + ->willReturn(true); + $opcache->method('getNumberCachedScripts') + ->willReturn(count($this->list)); + $opcache->method('getHits') + ->willReturn(1001); + $opcache->method('getStatus') + ->willReturn([ + 'memory_usage' => [ + 'used_memory' => rand(1000, 999999), + 'free_memory' => rand(1000, 999999), + 'wasted_memory' => rand(1000, 999999), + ], + 'opcache_statistics' => [ + 'num_cached_scripts' => rand(1000, 999999), + 'opcache_hit_rate' => rand(1, 99)/100, + 'misses' => rand(1000, 999999), + ], + ]); + $opcache->method('getScripts') + ->willReturn(array_merge($this->list, [ + $this->workdir . '/examples/foo.php' => [ + 'hits' => 10, + 'memory_consumption' => 0, + 'last_used_timestamp' => 1400000000 + ], + $this->workdir . '/examples/bar.php'=> [ + 'hits' => 10, + 'memory_consumption' . + '' => 0, + 'last_used_timestamp' => 1400000000 + ], + $this->workdir . '/examples/qux.php'=> [ + 'hits' => 10, + 'memory_consumption' => 0, + 'last_used_timestamp' => 1400000000 + ], + ])); + + $preloader = new Preloader(new PreloaderCompiler, new PreloaderLister($opcache), $opcache); + + mkdir($this->workdir . '/examples'); + touch($this->workdir . '/examples/foo.php'); + touch($this->workdir . '/examples/bar.php'); + touch($this->workdir . '/examples/qux.php'); + + $this->assertTrue( + $preloader + ->autoload($autoload = $this->workdir . '/autoload.php') + ->memory(10) + ->exclude($this->workdir . '/examples/*.php') + ->output($this->workdir . '/preload.php') + ->generate() + ); + + $contents = file_get_contents($this->workdir . '/preload.php'); + + $this->assertStringNotContainsString($this->workdir . '/examples/foo.php', $contents); + $this->assertStringNotContainsString($this->workdir . '/examples/bar.php', $contents); + $this->assertStringNotContainsString($this->workdir . '/examples/qux.php', $contents); + } + + public function testAppendsUsingGlobFormat() + { + $opcache = $this->createMock(Opcache::class); + + $opcache->method('isEnabled') + ->willReturn(true); + $opcache->method('getNumberCachedScripts') + ->willReturn(count($this->list)); + $opcache->method('getHits') + ->willReturn(1001); + $opcache->method('getStatus') + ->willReturn([ + 'memory_usage' => [ + 'used_memory' => rand(1000, 999999), + 'free_memory' => rand(1000, 999999), + 'wasted_memory' => rand(1000, 999999), + ], + 'opcache_statistics' => [ + 'num_cached_scripts' => rand(1000, 999999), + 'opcache_hit_rate' => rand(1, 99)/100, + 'misses' => rand(1000, 999999), + ], + ]); + $opcache->method('getScripts') + ->willReturn($this->list); + + $preloader = new Preloader(new PreloaderCompiler, new PreloaderLister($opcache), $opcache); + + mkdir($this->workdir . '/examples'); + touch($this->workdir . '/examples/foo.php'); + touch($this->workdir . '/examples/bar.php'); + touch($this->workdir . '/examples/qux.php'); + touch($this->workdir . '/examples/quz.md'); + + $this->assertTrue( + $preloader + ->autoload($autoload = $this->workdir . '/autoload.php') + ->memory(10) + ->append($this->workdir . '/examples/*.php') + ->output($this->workdir . '/preload.php') + ->generate() + ); + + $contents = file_get_contents($this->workdir . '/preload.php'); + + $this->assertStringContainsString($this->workdir . '/examples/foo.php', $contents); + $this->assertStringContainsString($this->workdir . '/examples/bar.php', $contents); + $this->assertStringContainsString($this->workdir . '/examples/qux.php', $contents); + $this->assertStringNotContainsString($this->workdir . '/examples/quz.md', $contents); + } + public function testExcludesFilesAffectingLimits() { $opcache = $this->createMock(Opcache::class); @@ -571,6 +720,81 @@ public function testExcludesPackageFiles() $this->assertStringContainsString($files, $contents); } + public function testIncludesPackageFiles() + { + $opcache = $this->createMock(Opcache::class); + + $package = array_flip([ + realpath(__DIR__ . '/../src/Conditions.php'), + realpath(__DIR__ . '/../src/GeneratesScript.php'), + realpath(__DIR__ . '/../src/LimitsList.php'), + realpath(__DIR__ . '/../src/ManagesFiles.php'), + realpath(__DIR__ . '/../src/Opcache.php'), + realpath(__DIR__ . '/../src/Preloader.php'), + realpath(__DIR__ . '/../src/PreloaderCompiler.php'), + realpath(__DIR__ . '/../src/PreloaderLister.php'), + ]); + + $opcache->method('isEnabled') + ->willReturn(true); + $opcache->method('getNumberCachedScripts') + ->willReturn(count($this->list) + count($package)); + $opcache->method('getHits') + ->willReturn(1001); + $opcache->method('getStatus') + ->willReturn([ + 'memory_usage' => [ + 'used_memory' => rand(1000, 999999), + 'free_memory' => rand(1000, 999999), + 'wasted_memory' => rand(1000, 999999), + ], + 'opcache_statistics' => [ + 'num_cached_scripts' => rand(1000, 999999), + 'opcache_hit_rate' => rand(1, 99)/100, + 'misses' => rand(1000, 999999), + ], + ]); + + $opcache->method('getScripts') + ->willReturn(array_merge($this->list, array_map(function () { + return [ + 'hits' => 1, + 'memory_consumption' => 1, + 'last_used_timestamp' => 1400000000 + ]; + }, $package))); + + $preloader = new Preloader(new PreloaderCompiler, new PreloaderLister($opcache), $opcache); + + $preloader + ->autoload($autoload = $this->workdir . '/autoload.php') + ->exclude([ + 'quz', + ]) + ->includePreloader() + ->output($this->workdir . '/preload.php') + ->generate(); + + $contents = file_get_contents($this->workdir . '/preload.php'); + + $files = '$files = [' . \PHP_EOL . + " 'bar'," . \PHP_EOL . + " 'foo'," . \PHP_EOL . + " 'qux'," . \PHP_EOL . + " 'baz'," . \PHP_EOL . + " '" . realpath(__DIR__ . '/../src/Conditions.php') . "',". \PHP_EOL . + " '" . realpath(__DIR__ . '/../src/GeneratesScript.php') . "',". \PHP_EOL . + " '" . realpath(__DIR__ . '/../src/LimitsList.php') . "',". \PHP_EOL . + " '" . realpath(__DIR__ . '/../src/ManagesFiles.php') . "',". \PHP_EOL . + " '" . realpath(__DIR__ . '/../src/Opcache.php') . "',". \PHP_EOL . + " '" . realpath(__DIR__ . '/../src/Preloader.php') . "',". \PHP_EOL . + " '" . realpath(__DIR__ . '/../src/PreloaderCompiler.php') . "',". \PHP_EOL . + " '" . realpath(__DIR__ . '/../src/PreloaderLister.php') . "'". \PHP_EOL . + '];'; + + $this->assertStringContainsString($files, $contents); + } + public function testExcludesAndAppends() { $opcache = $this->createMock(Opcache::class); @@ -689,7 +913,7 @@ public function testFailsWhenNoAutoloader() public function testFailsWhenNoAutoload() { $this->expectException(\LogicException::class); - $this->expectErrorMessage('Cannot proceed without an Composer Autoload.'); + $this->expectErrorMessage('Cannot proceed without a Composer Autoload.'); Preloader::make() ->output($this->workdir . '/preload.php') @@ -747,8 +971,6 @@ protected function tearDown() : void { parent::tearDown(); - if (is_file($file = $this->workdir . '/preload.php')) { - unlink($file); - } + $this->clearWorkdir(); } }