├── .styleci.yml ├── helpers └── safe_preloader.php ├── src ├── ConditionsScript.php ├── preload.php.stub ├── Opcache.php ├── ManagesFiles.php ├── GeneratesScript.php ├── PreloaderCompiler.php ├── PreloaderLister.php └── Preloader.php ├── LICENSE ├── phpunit.xml ├── composer.json └── README.md /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel 2 | 3 | disabled: 4 | - single_class_element_per_statement 5 | -------------------------------------------------------------------------------- /helpers/safe_preloader.php: -------------------------------------------------------------------------------- 1 | condition = $condition; 23 | 24 | return $this; 25 | } 26 | 27 | /** 28 | * Run the Preloader script one in a given number of random chances. 29 | * 30 | * @param int $chances 31 | * @return $this 32 | */ 33 | public function whenOneIn(int $chances) : self 34 | { 35 | return $this->when(function () use ($chances) { 36 | return random_int(1, $chances) === (int)ceil($chances/2); 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Italo Israel Baeza Cabrera 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | tests 15 | 16 | 17 | 18 | 19 | src/ 20 | 21 | src/preload.php.stub 22 | src/Opcache.php 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "darkghosthunter/preloader", 3 | "type": "library", 4 | "description": "Preloader helper to create a PHP-ready preload script from Opcache.", 5 | "keywords": [ 6 | "darkghosthunter", 7 | "preloader", 8 | "php-7.4", 9 | "opcache" 10 | ], 11 | "license": "MIT", 12 | "minimum-stability": "dev", 13 | "prefer-stable": true, 14 | "homepage": "https://github.com/darkghosthunter/preloader", 15 | "authors": [ 16 | { 17 | "name": "Italo Israel Baeza Cabrera", 18 | "email": "darkghosthunter@gmail.com", 19 | "role": "Developer" 20 | } 21 | ], 22 | "require": { 23 | "php": "^7.4||^8.0", 24 | "ext-json": "*", 25 | "symfony/finder": "^4.3||^5.0.5" 26 | }, 27 | "require-dev": { 28 | "phpunit/phpunit": "^9.3" 29 | }, 30 | "autoload": { 31 | "psr-4": { 32 | "DarkGhostHunter\\Preloader\\" : "src/" 33 | } 34 | }, 35 | "autoload-dev": { 36 | "psr-4": { 37 | "Tests\\" : "tests/" 38 | } 39 | }, 40 | "scripts": { 41 | "test": "vendor/bin/phpunit --coverage-clover build/logs/clover.xml" 42 | }, 43 | "config": { 44 | "sort-packages": true 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/preload.php.stub: -------------------------------------------------------------------------------- 1 | getMessage() . \PHP_EOL; 50 | echo 'File: ' . $file . \PHP_EOL; 51 | 52 | throw $e; 53 | } 54 | } 55 | 56 | -------------------------------------------------------------------------------- /src/Opcache.php: -------------------------------------------------------------------------------- 1 | status ??= opcache_get_status(true)) { 31 | return $this->status; 32 | } 33 | 34 | throw new RuntimeException( 35 | 'Opcache is disabled. Further reference: https://www.php.net/manual/en/opcache.configuration' 36 | ); 37 | } 38 | 39 | /** 40 | * Returns if Opcache is enabled 41 | * 42 | * @return bool 43 | */ 44 | public function isEnabled() : bool 45 | { 46 | return $this->getStatus()['opcache_enabled']; 47 | } 48 | 49 | /** 50 | * Returns the scripts used by Opcache 51 | * 52 | * @return array 53 | */ 54 | public function getScripts() : array 55 | { 56 | return $this->getStatus()['scripts']; 57 | } 58 | 59 | /** 60 | * Returns the number of scripts cached 61 | * 62 | * @return int 63 | */ 64 | public function getNumberCachedScripts() 65 | { 66 | return $this->getStatus()['opcache_statistics']['num_cached_scripts']; 67 | } 68 | 69 | /** 70 | * Returns the number of hits in Opcache 71 | * 72 | * @return mixed 73 | */ 74 | public function getHits() 75 | { 76 | return $this->getStatus()['opcache_statistics']['hits']; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/ManagesFiles.php: -------------------------------------------------------------------------------- 1 | appended = $this->findFiles($directories); 40 | 41 | return $this; 42 | } 43 | 44 | /** 45 | * Exclude a list of directories from the preload list generation. 46 | * 47 | * @param string|array|\Closure|\Symfony\Component\Finder\Finder $directories 48 | * @return $this 49 | */ 50 | public function exclude($directories) : self 51 | { 52 | $this->excluded = $this->findFiles($directories); 53 | 54 | return $this; 55 | } 56 | 57 | /** 58 | * Excludes the package files from the preload list. 59 | * 60 | * @return $this 61 | */ 62 | public function selfExclude() : self 63 | { 64 | $this->selfExclude = true; 65 | 66 | return $this; 67 | } 68 | 69 | /** 70 | * Instantiates the Symfony Finder to look for files. 71 | * 72 | * @param string|array|\Closure|\Symfony\Component\Finder\Finder $files 73 | * @return \Symfony\Component\Finder\Finder 74 | */ 75 | protected function findFiles($files) : Finder 76 | { 77 | if (is_callable($files)) { 78 | $files($finder = new Finder()); 79 | } 80 | else { 81 | $finder = (new Finder())->in($files); 82 | } 83 | 84 | return $finder->files(); 85 | } 86 | 87 | /** 88 | * Return an array of the files from the Finder. 89 | * 90 | * @param \Symfony\Component\Finder\Finder $finder 91 | * @return array 92 | */ 93 | protected function getFilesFromFinder(Finder $finder) : array 94 | { 95 | $paths = []; 96 | 97 | try { 98 | foreach ($finder as $file) { 99 | $paths[] = $file->getRealPath(); 100 | } 101 | 102 | return $paths; 103 | } 104 | // If the developer used an empty array for the Finder instance, we will just 105 | // catch the exception thrown by having no directories to find and return an 106 | // empty array. Otherwise the Preloader will fail when retrieving the list. 107 | catch (LogicException $exception) { 108 | return $paths; 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/GeneratesScript.php: -------------------------------------------------------------------------------- 1 | memory = (float)$limit; 46 | 47 | return $this; 48 | } 49 | 50 | /** 51 | * Use `require_once $file` to preload each file on the list. 52 | * 53 | * @param string $autoloader 54 | * @return $this 55 | */ 56 | public function useRequire(string $autoloader) : self 57 | { 58 | $this->useRequire = true; 59 | $this->autoloader = $autoloader; 60 | 61 | return $this; 62 | } 63 | 64 | /** 65 | * If it should NOT throw an exception when a file to preload doesn't exists. 66 | * 67 | * @param bool $ignore 68 | * @return $this 69 | */ 70 | public function ignoreNotFound($ignore = true) 71 | { 72 | $this->ignoreNotFound = $ignore; 73 | 74 | return $this; 75 | } 76 | 77 | /** 78 | * Returns a digestible Opcache configuration 79 | * 80 | * @return array 81 | */ 82 | protected function getOpcacheConfig() 83 | { 84 | $opcache = $this->opcache->getStatus(); 85 | 86 | return [ 87 | '@opcache_memory_used' => 88 | number_format($opcache['memory_usage']['used_memory'] / 1024 ** 2, 1, '.', ''), 89 | '@opcache_memory_free' => 90 | number_format($opcache['memory_usage']['free_memory'] / 1024 ** 2, 1, '.', ''), 91 | '@opcache_memory_wasted' => 92 | number_format($opcache['memory_usage']['wasted_memory'] / 1024 ** 2, 1, '.', ''), 93 | '@opcache_files' => 94 | $opcache['opcache_statistics']['num_cached_scripts'], 95 | '@opcache_hit_rate' => 96 | number_format($opcache['opcache_statistics']['opcache_hit_rate'], 2, '.', ''), 97 | '@opcache_misses' => 98 | $opcache['opcache_statistics']['misses'], 99 | ]; 100 | } 101 | 102 | /** 103 | * Returns a digestible Preloader configuration 104 | * 105 | * @return array 106 | */ 107 | protected function getPreloaderConfig() 108 | { 109 | return [ 110 | '@preloader_memory_limit' => $this->memory ? $this->memory . ' MB' : '(disabled)', 111 | '@preloader_overwrite' => $this->overwrite ? 'true' : 'false', 112 | '@preloader_appended' => count($this->lister->appended), 113 | '@preloader_excluded' => count($this->lister->excluded), 114 | ]; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/PreloaderCompiler.php: -------------------------------------------------------------------------------- 1 | preloaderConfig, $this->opcacheConfig, [ 73 | '@output' => $this->scriptRealPath(), 74 | '@generated_at' => date('Y-m-d H:i:s e'), 75 | '@autoload' => isset($this->autoloader) 76 | ? 'require_once \'' . realpath($this->autoloader) . '\';': null, 77 | '@list' => $this->parseList(), 78 | '@failure' => $this->ignoreNotFound ? 'continue;' : 'throw new \Exception("{$file} does not exist or is unreadable.");', 79 | '@mechanism' => $this->useRequire 80 | ? 'require_once $file' : 'opcache_compile_file($file)', 81 | ]); 82 | 83 | return str_replace(array_keys($replacing), $replacing, $this->contents); 84 | } 85 | 86 | /** 87 | * Returns the output file real path. 88 | * 89 | * @return string 90 | */ 91 | protected function scriptRealPath() 92 | { 93 | // @codeCoverageIgnoreStart 94 | // We need to get the real path of the preloader file. To do that, we 95 | // will check if the file already exists, and if not, create a dummy 96 | // one. If that "touch" fails, we will return whatever path we got. 97 | if (file_exists($this->writeTo)) { 98 | return realpath($this->writeTo); 99 | } 100 | 101 | return $this->touchAndGetRealPath(); 102 | // @codeCoverageIgnoreEnd 103 | } 104 | 105 | /** 106 | * Creates a dummy file and returns the real path of it, if possible. 107 | * 108 | * @return false|string 109 | */ 110 | protected function touchAndGetRealPath() 111 | { 112 | // @codeCoverageIgnoreStart 113 | $path = $this->writeTo; 114 | 115 | if (touch($this->writeTo)) { 116 | $path = $this->writeTo; 117 | unlink($this->writeTo); 118 | } 119 | 120 | return $path ?: $this->writeTo; 121 | // @codeCoverageIgnoreEnd 122 | } 123 | 124 | /** 125 | * Parses the list into an injectable string 126 | * 127 | * @return string 128 | */ 129 | protected function parseList() 130 | { 131 | return PHP_EOL . ' ' . "'" . implode("'," . PHP_EOL . " '", $this->list) . "'" . PHP_EOL; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/PreloaderLister.php: -------------------------------------------------------------------------------- 1 | excludePreloadVariable($this->list); 53 | 54 | // Exclude the files set by the developer 55 | $scripts = $this->exclude($scripts); 56 | 57 | // Sort the scripts by hit ratio 58 | $scripts = $this->sortScripts($scripts); 59 | 60 | // Cull the list by memory usage 61 | $scripts = $this->cutByMemoryLimit($scripts); 62 | 63 | // Add files to the preload. 64 | $scripts = array_merge($scripts, $this->appended); 65 | 66 | // Remove duplicates and return 67 | return array_unique($scripts); 68 | } 69 | 70 | /** 71 | * Excludes de `$PRELOAD$` file key from the list. 72 | * 73 | * @param array $list 74 | * @return array 75 | */ 76 | protected function excludePreloadVariable(array $list) 77 | { 78 | unset($list['$PRELOAD$']); 79 | 80 | return $list; 81 | } 82 | 83 | /** 84 | * Retrieve a sorted scripts list used by Opcache. 85 | * 86 | * @param array $scripts 87 | * @return array 88 | */ 89 | protected function sortScripts(array $scripts) 90 | { 91 | // There is no problem here with the Preloader. 92 | array_multisort( 93 | array_column($scripts, 'hits'), SORT_DESC, 94 | array_column($scripts, 'last_used_timestamp'), SORT_DESC, 95 | $scripts 96 | ); 97 | 98 | return $scripts; 99 | } 100 | 101 | /** 102 | * Cuts the files by their cumulative memory consumption. 103 | * 104 | * @param $files 105 | * @return array 106 | */ 107 | protected function cutByMemoryLimit($files) 108 | { 109 | // Exit early if the memory limit is zero (disabled). 110 | if (! $limit = $this->memory * 1024**2) { 111 | return array_keys($files); 112 | } 113 | 114 | $cumulative = 0; 115 | 116 | $resulting = []; 117 | 118 | // We will cycle through each file and check how much memory it consumes. If adding 119 | // the file exceeds the memory limit set, we will stop adding files to the compiled 120 | // list of preloaded files. Otherwise, we'll keep cycling and return all the files. 121 | foreach ($files as $key => $file) { 122 | $cumulative += $file['memory_consumption']; 123 | 124 | if ($cumulative > $limit) { 125 | return $resulting; 126 | } 127 | 128 | $resulting[] = $key; 129 | } 130 | 131 | return $resulting; 132 | } 133 | 134 | /** 135 | * Exclude files from the list 136 | * 137 | * @param array $scripts 138 | * @return array 139 | */ 140 | protected function exclude(array $scripts) 141 | { 142 | return array_diff_key($scripts, array_flip(array_merge($this->excluded, $this->excludedPackageFiles()))); 143 | } 144 | 145 | /** 146 | * Get the list of files excluded for this package 147 | * 148 | * @return array 149 | */ 150 | protected function excludedPackageFiles() 151 | { 152 | if ($this->selfExclude) { 153 | return array_map(fn ($entry) => realpath($entry), glob(realpath(__DIR__) . DIRECTORY_SEPARATOR . '*.php')); 154 | } 155 | 156 | return []; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/Preloader.php: -------------------------------------------------------------------------------- 1 | compiler = $compiler; 66 | $this->lister = $lister; 67 | $this->opcache = $opcache; 68 | } 69 | 70 | /** 71 | * Returns the raw list of files to include in the script 72 | * 73 | * @return array 74 | */ 75 | public function getList() : array 76 | { 77 | return $this->prepareLister()->build(); 78 | } 79 | 80 | /** 81 | * Prepares the Preload Lister. 82 | * 83 | * @return \DarkGhostHunter\Preloader\PreloaderLister 84 | */ 85 | protected function prepareLister() 86 | { 87 | $this->lister->list = $this->opcache->getScripts(); 88 | $this->lister->memory = $this->memory; 89 | $this->lister->selfExclude = $this->selfExclude; 90 | $this->lister->appended = isset($this->appended) ? $this->getFilesFromFinder($this->appended) : []; 91 | $this->lister->excluded = isset($this->excluded) ? $this->getFilesFromFinder($this->excluded) : []; 92 | 93 | return $this->lister; 94 | } 95 | 96 | /** 97 | * Writes the Preload script to the given path. 98 | * 99 | * @param string $path 100 | * @param bool $overwrite 101 | * @return bool 102 | */ 103 | public function writeTo(string $path, bool $overwrite = true) : bool 104 | { 105 | return $this->canGenerate($path, $overwrite) && $this->performWrite($path); 106 | } 107 | 108 | /** 109 | * Return if this Preloader can NOT generate the script. 110 | * 111 | * @param string $path 112 | * @param bool $overwrite 113 | * @return bool 114 | */ 115 | protected function canGenerate(string $path, bool $overwrite) : bool 116 | { 117 | // When using require, check if the autoloader exists. 118 | if ($this->useRequire && ! file_exists($this->autoloader)) { 119 | throw new LogicException('Cannot proceed without a Composer Autoload.'); 120 | } 121 | 122 | // Bail out if Opcache is not enabled. 123 | if (! $this->opcache->isEnabled()) { 124 | throw new LogicException('Opcache is disabled. No preload script can be generated.'); 125 | } 126 | 127 | // Also bail out if Opcache doesn't have any cached script. 128 | if (! $this->opcache->getNumberCachedScripts()) { 129 | throw new LogicException('Opcache reports 0 cached scripts. No preload can be generated.'); 130 | } 131 | 132 | // We can't generate a script if we can't overwrite an existing file. 133 | if (! ($this->overwrite = $overwrite) && file_exists($path)) { 134 | throw new LogicException('Preloader script already exists in the given path.'); 135 | } 136 | 137 | // If there is a condition, call it. 138 | if ($this->condition) { 139 | return (bool) call_user_func($this->condition); 140 | } 141 | 142 | return true; 143 | } 144 | 145 | /** 146 | * Writes the preloader script file into the specified path. 147 | * 148 | * @param string $path 149 | * @return false|int 150 | * 151 | * @codeCoverageIgnore 152 | */ 153 | protected function performWrite(string $path) 154 | { 155 | if (file_put_contents($path, $this->prepareCompiler($path)->compile(), LOCK_EX)) { 156 | return true; 157 | } 158 | 159 | throw new RuntimeException("Preloader couldn't write the script to [$path]."); 160 | } 161 | 162 | /** 163 | * Prepares the Compiler to make a list. 164 | * 165 | * @param string $path 166 | * @return \DarkGhostHunter\Preloader\PreloaderCompiler 167 | */ 168 | protected function prepareCompiler(string $path) 169 | { 170 | $this->compiler->list = $this->getList(); 171 | $this->compiler->autoloader = $this->autoloader; 172 | $this->compiler->contents = file_get_contents(static::STUB_LOCATION); 173 | $this->compiler->opcacheConfig = $this->getOpcacheConfig(); 174 | $this->compiler->preloaderConfig = $this->getPreloaderConfig(); 175 | $this->compiler->useRequire = $this->useRequire; 176 | $this->compiler->ignoreNotFound = $this->ignoreNotFound; 177 | $this->compiler->autoloader = $this->autoloader; 178 | $this->compiler->writeTo = $path; 179 | 180 | return $this->compiler; 181 | } 182 | 183 | /** 184 | * Creates a new Preloader instance 185 | * 186 | * @return static 187 | */ 188 | public static function make() : self 189 | { 190 | return new static(new PreloaderCompiler, new PreloaderLister, new Opcache); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![ 2 | Braden Collum - Unsplash (UL) #9HI8UJMSdZA](https://images.unsplash.com/photo-1461896836934-ffe607ba8211?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1280&h=400&q=80) 3 | 4 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/darkghosthunter/preloader.svg?style=flat-square)](https://packagist.org/packages/darkghosthunter/preloader) [![License](https://poser.pugx.org/darkghosthunter/preloader/license)](https://packagist.org/packages/darkghosthunter/preloader) 5 | ![](https://img.shields.io/packagist/php-v/darkghosthunter/preloader.svg) 6 | ![](https://github.com/DarkGhostHunter/Preloader/workflows/PHP%20Composer/badge.svg) 7 | [![Coverage Status](https://coveralls.io/repos/github/DarkGhostHunter/Preloader/badge.svg?branch=master)](https://coveralls.io/github/DarkGhostHunter/Preloader?branch=master) 8 | 9 | # Opcache Preloader 10 | 11 | Get the best options to keep your application fast as ever, with just one line. 12 | 13 | This package generates a [PHP 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. 14 | 15 | > If you're looking for preloading your Laravel project, check [Laragear Preload](https://github.com/Laragear/Preload). 16 | 17 | ## Table of Contents 18 | 19 | - [Requirements](#requirements) 20 | - [Installation](#installation) 21 | - [Usage](#usage) 22 | - [How it works](#how-it-works) 23 | - [Configuration](#configuration) 24 | * [Conditions](#conditions) 25 | + [`when()`](#when) 26 | + [`whenOneIn()`](#whenOneIn) 27 | * [Listing](#listing) 28 | + [`append()`](#append) 29 | + [`exclude()`](#exclude) 30 | + [`selfExclude()`](#selfexclude) 31 | * [Generation](#generation) 32 | + [`memoryLimit()`](#memorylimit) 33 | + [`useRequire()`](#userequire) 34 | + [`ignoreNotFound()`](#ignorenotfound) 35 | * [Compilation](#compilation) 36 | + [`writeTo()`](#writeto) 37 | + [`getList()`](#getlist) 38 | - [Safe Preloader](#safe-preloader) 39 | - [Example](#example) 40 | - [Security](#security) 41 | - [License](#license) 42 | 43 | ## Requirements 44 | 45 | * PHP 7.4.3, PHP 8.0 or later. 46 | * [Opcache & Preloading enabled](https://www.php.net/manual/en/book.opcache.php) (`ext-opcache`). 47 | * Composer Autoloader (optional). 48 | 49 | ## Installation 50 | 51 | Require this using Composer into your project 52 | 53 | composer require darkghosthunter/preloader 54 | 55 | > This package doesn't require `ext-opcache` to install. Just be sure to have it [enabled in your application server](https://www.php.net/manual/en/book.opcache.php). 56 | 57 | ## Usage 58 | 59 | Anywhere in your application, where Opcache is **enabled** and running, call `Preloader` with where to output the compiled script: 60 | 61 | ```php 62 | writeTo(__DIR__.'/preloader.php'); 67 | ``` 68 | 69 | This will automatically gather Opcache statistics, and write an optimized `preload.php` file. In this case, the file will be created in the same directory the Preloader was called. 70 | 71 | www 72 | └── app 73 | ├── PreloaderCall.php 74 | └── preload.php 75 | 76 | Once generated, tell PHP to use this file as a preloader at start up in your `php.ini`. 77 | 78 | ```ini 79 | opcache.preload=/www/app/preload.php 80 | ``` 81 | 82 | Once the script is generated, **you're encouraged to restart your PHP process** (or server, in some cases) to pick up the generated preload script. Only generating the script [is not enough](https://www.php.net/manual/en/opcache.preloading.php). 83 | 84 | > If you use Preloader when Opcache is disabled or without hits, you will get an Exception. 85 | 86 | ## How it works 87 | 88 | This package asks Opcache only for the most requested files and compiles a list from it. You can [check this article in Medium about that preload](https://medium.com/p/9ede756f292c/). 89 | 90 | Since the best statistics are those you get **after** your application has been running for a while, you can use your own mechanisms to compile the list only after certain conditions are met. 91 | 92 | ![](https://miro.medium.com/max/1365/1*Zp-rR9-dPNn55L8GjSUpJg.png) 93 | 94 | Don't worry, you can configure what and how compile the list. 95 | 96 | ## Configuration 97 | 98 | You can configure the Preloader to run when a condition is met, limit the file list, among what other things. 99 | 100 | ### Conditions 101 | 102 | #### `when()` 103 | 104 | This method executes the given callable and checks if the preloader should compile the list or not based on what the callable returning value evaluates. 105 | 106 | ```php 107 | when(fn () => $app->cache()->get('should_run')); 112 | ``` 113 | 114 | This is handy if you can combine the condition with your own application logic, like a given number of requests, or an external signal. 115 | 116 | #### `whenOneIn()` 117 | 118 | This is method is just a helper to allows you to quickly generate a Preloader script in one of a given number of random chances. 119 | 120 | ```php 121 | whenOneIn(50); 126 | ``` 127 | 128 | For example, the above makes the Preloader generate a compiled list one in fifty chances. 129 | 130 | ### Listing 131 | 132 | #### `append()` 133 | 134 | You can add a list of directories to the compiled list. The files inside them will be appended to the compiled list, and won't account for memory restrictions. 135 | 136 | ```php 137 | append([ 142 | __DIR__ . '/files/*/more_files', 143 | __DIR__ . '/classes/' 144 | ]); 145 | ``` 146 | 147 | > If the files you're adding are already in the compiled list, these will be removed from the included files to avoid effective duplicates. 148 | 149 | This packages includes [Symfony Finder](https://symfony.com/doc/current/components/finder.html), so as an alternative you can pass a closure that receives the Finder instance along with the files you want to append. 150 | 151 | ```php 152 | append(function (Finder $find) { 158 | $find->files()->in('/package')->name(['*.php', '*.twig']); 159 | }); 160 | ``` 161 | 162 | > The `exclude()` method take precedence over `append()`. If you exclude a file that is later appended, you won't exclude it at all. 163 | 164 | #### `exclude()` 165 | 166 | This method excludes files from inside directories from Opcache list, which later end up in the Preload list. Excluding files may free up the memory of the compiled list, leaving space for others to be included. 167 | 168 | You can pass an array of paths, which is good if you already have a list ready to exclude. 169 | 170 | ```php 171 | exclude([ 176 | __DIR__ . '/files/*/more_files', 177 | __DIR__ . '/vendor' 178 | ]); 179 | ``` 180 | 181 | This packages includes [Symfony Finder](https://symfony.com/doc/current/components/finder.html), so as an alternative you can pass a Closure receiving the Finder instance along with the files you want to exclude. 182 | 183 | ```php 184 | exclude(function (Finder $find) { 190 | $find->files()->in('/package')->name(['*.php', '*.twig']); 191 | }); 192 | ``` 193 | 194 | > The `exclude()` method take precedence over `append()`. If you exclude a file that is later appended, you won't exclude it at all. 195 | 196 | #### `selfExclude()` 197 | 198 | Automatically excludes the Package files from the Preload list. 199 | 200 | By default, the package is not excluded, since it may be part of the most requested files. It's recommended to exclude the package only if you have total confidence it won't be called once Opcache Preloading is enabled. 201 | 202 | ```php 203 | selfExclude(); 208 | ``` 209 | 210 | ### Generation 211 | 212 | #### `memoryLimit()` 213 | 214 | By default, Preloader defaults a memory limit of 32MB, which is enough for *most* applications. The Preloader will generate a list of files until that memory limit is reached. 215 | 216 | You can set your own memory limit in **MB**. 217 | 218 | ```php 219 | memoryLimit(32); 224 | ``` 225 | 226 | This takes into account the `memory_consumption` key of each script cached in Opcache, not the real file size. 227 | 228 | > This limit doesn't have any relation with Opcache memory limit. 229 | 230 | To disable the limit, use `memoryLimit(0)`. This will list **ALL** available files from Opcache. 231 | 232 | #### `useRequire()` 233 | 234 | By default, the Preloader will upload the files to Opcache using `opcache_compile_file()`. This avoids executing any file in your project, but no links (traits, interfaces, extended classes, ...) will be resolved from the files compiled. You may have some warnings of unresolved links when preloading (nothing too dangerous). 235 | 236 | You can change this using `useRequire()`, which changes to `require_once`, along the path the Composer Autoloader (usually at `vendor/autoload.php`) to resolve the links. 237 | 238 | ```php 239 | useRequire(__DIR__ . '/../vendor/autoload.php'); 244 | ``` 245 | 246 | > You may get some warnings when compiling a file with unresolved links. These are not critical, since these files usually are the least requested in your application. 247 | 248 | #### `ignoreNotFound()` 249 | 250 | Some applications may create files at runtime that are genuinely cached by Opcache, but doesn't exist when the application is firstly deployed. 251 | 252 | To avoid this problem, you can use the `ignoreNotFound()`, which will compile a script that ignore files not found or are unreadable. 253 | 254 | ```php 255 | ignoreNotFound(); 260 | ``` 261 | 262 | > If the file is readable but its preloading returns an error, it will throw an exception nonetheless. 263 | 264 | ### Compilation 265 | 266 | #### `writeTo()` 267 | 268 | 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. 269 | 270 | ```php 271 | writeTo(__DIR__ . '/preloader.php'); 276 | ``` 277 | 278 | You can issue `false` as second parameter to not overwrite any existing file in the write path. If a file is found, no preload logic will be run. 279 | 280 | #### `getList()` 281 | 282 | You can retrieve the raw list of files that should be included as an array using `getList()`. 283 | 284 | ```php 285 | getList(); 290 | ``` 291 | 292 | This may become handy if you have your own script, or you just want to tinker around it. 293 | 294 | ## Safe Preloader 295 | 296 | This packages comes with a handy Safe Preloader, located in `helpers/safe_preloader.php`. 297 | 298 | What it does is very simple: it registers a shutdown function for PHP that is executed after the preload script finishes, and registers any error the script may have returned so you can debug it. 299 | 300 | To use it, copy the file into an accessible path for PHP, and along with the real preloader script, reference it in your `php.ini`: 301 | 302 | ```ini 303 | opcache.preload=/www/app/safe_preloader.php 304 | ``` 305 | 306 | ```php 307 | run(); 342 | 343 | $response->sendToBrowser(); 344 | 345 | Preloader::make() 346 | ->whenOneIn(100) 347 | ->memoryLimit(64) 348 | ->writeTo(PHP_LOCALSTATEDIR . '/preload.php'); // put it in /var.; 349 | ``` 350 | 351 | ## Security 352 | 353 | If you discover any security related issues, please email darkghosthunter@gmail.com instead of using the issue tracker. 354 | 355 | ## License 356 | 357 | The MIT License (MIT). Please see [License File](LICENSE) for more information. 358 | --------------------------------------------------------------------------------