├── .styleci.yml ├── LICENSE ├── README.md ├── composer.json ├── config └── laraload.php └── src ├── Conditions └── CountRequests.php ├── Events └── PreloadCalledEvent.php ├── Facades └── Laraload.php ├── Http └── Middleware │ └── LaraloadMiddleware.php ├── Laraload.php └── LaraloadServiceProvider.php /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel 2 | 3 | disabled: 4 | - single_class_element_per_statement 5 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## This package has been superseeded by [Laragear/Preload](https://github.com/Laragear/Preload). 2 | 3 | Please migrate to the new package. 4 | 5 | --- 6 | 7 | # Laraload 8 | 9 | Effortlessly create a PHP Preload Script for your Laravel project. 10 | 11 | ## Requirements 12 | 13 | * Laravel 6.x, 7.x or 8.x (Lumen _may_ work too) 14 | * PHP 7.4.3, PHP 8.0 or later 15 | * `ext-opcache` 16 | 17 | > The Opcache extension is not enforced by the package. Just be sure to enable it in your project's PHP main process. 18 | 19 | ## Installation 20 | 21 | Call composer and you're done. 22 | 23 | ```bash 24 | composer require darkghosthunter/laraload 25 | ``` 26 | 27 | ## What is Preloading? What does this? 28 | 29 | Preloading is a new feature for PHP. It "compiles" a list of files into memory, thus making the application code _fast_ without warming up. For that to work, it needs to read a PHP script that "uploads" these files into memory, at process startup. 30 | 31 | This allows the first Requests to avoid cold starts, where all the scripts must be loaded by PHP at that moment. Since this step is moved to the process startup, first Requests become _faster_ as all needed scripts are already in memory. 32 | 33 | This package wraps the Preloader package that generates a preload file. Once it's generated, you can point the generated list into your `php.ini`: 34 | 35 | ```ini 36 | opcache.preload = 'www/app/storage/preload.php'; 37 | ``` 38 | 39 | After that, the next time PHP starts, this list of files will be preloaded. 40 | 41 | ## Usage 42 | 43 | By default, this package constantly recreates your preload script each 500 requests in `storage/preload.php`. That's it. But you want the details, don't you? 44 | 45 | 1. A global terminable middleware checks for non-error response. 46 | 2. Then it calls a custom *Condition* class. 47 | 3. If the *Condition* evaluates to `true`, the Preload Script is generated. 48 | 4. A `PreloadCalledEvent` is fired with the generation status. 49 | 50 | ## Configuration 51 | 52 | Some people may not be happy with the "default" behaviour. Luckily, you can configure your own way to generate the script. 53 | 54 | First publish the configuration file: 55 | 56 | ```bash 57 | php artisan vendor:publish --provider="DarkGhostHunter\Laraload\LaraloadServiceProvider" 58 | ``` 59 | 60 | Let's check the config array: 61 | 62 | ```php 63 | env('LARALOAD_ENABLE'), 67 | 'condition' => \DarkGhostHunter\Laraload\Conditions\CountRequests::class, 68 | 'output' => storage_path('preload.php'), 69 | 'memory' => 32, 70 | 'use_require' => false, 71 | 'autoload' => base_path('vendor/autoload.php'), 72 | 'ignore-not-found' => true, 73 | ]; 74 | ``` 75 | 76 | #### Enable 77 | 78 | ```php 79 | env('LARALOAD_ENABLE'), 83 | ]; 84 | ``` 85 | 86 | By default, Laraload will be automatically enabled on production environments. You can forcefully enable or disable it using an environment variable set to `true` or `false`, respectively. 87 | 88 | ```dotenv 89 | LARALOAD_ENABLE=true 90 | ``` 91 | 92 | #### Condition 93 | 94 | ```php 95 | 'App\MyCustomCondition@handle', 99 | ]; 100 | ``` 101 | 102 | This package comes with a _simple_ condition class that returns `true` every 500 requests, which triggers the script generation. 103 | 104 | You can define your own condition class to generate the Preload script. This will be called after the request is handled to the browser, and it will be resolved by the Service Container. 105 | 106 | The condition is called the same way as a Controller action: as an invokable class or using _Class@action_ notation. 107 | 108 | #### Output 109 | 110 | ```php 111 | '/var/www/preloads/my_preload.php', 115 | ]; 116 | ``` 117 | 118 | By default, the script is saved in your storage path, but you can change the filename and path to save it as long PHP has permissions to write on it. Double-check your file permissions. 119 | 120 | #### Memory Limit 121 | 122 | ```php 123 | 64, 127 | ]; 128 | ``` 129 | 130 | For most applications, 32MB is fine as a preload limit, but you may fine-tune it for your project specifically. 131 | 132 | #### Method 133 | 134 | ```php 135 | true, 139 | 'autoload' => base_path('vendor/autoload.php'), 140 | ]; 141 | ``` 142 | 143 | Opcache allows to preload files using `require_once` or `opcache_compile_file()`. 144 | 145 | Laraload uses `opcache_compile_file()` for better manageability on the files preloaded. Some unresolved links may output warnings at startup, but nothing critical. 146 | 147 | Using `require_once` will "execute" all files, resolving all the links (imports, parent classes, traits, interfaces, etc.) before compiling it, and may output heavy errors on files that shouldn't be executed. Depending on your application, you may want to use one over the other. 148 | 149 | If you plan use `require_once`, ensure you have set the correct path to the Composer Autoloader, since it will be used to resolve classes, among other files. 150 | 151 | ### Ignore not found files 152 | 153 | ```php 154 | true, 158 | ]; 159 | ``` 160 | 161 | Version 2.1.0 and onward ignores non-existent files by default. This may work for files created by Laravel at runtime and actively cached by Opcache, but that on deployment are absent, like [real-time facades](https://laravel.com/docs/facades#real-time-facades). 162 | 163 | You can disable this for any reason, which will throw an Exception if any file is missing, but is recommended leaving it alone unless you know what you're doing. 164 | 165 | ### Include & Exclude directories 166 | 167 | For better manageability, you can now append or exclude files from directories using the [Symfony Finder](https://symfony.com/doc/current/components/finder.html), which is included in this package, to retrieve a list of files inside of them with better filtering options. 168 | 169 | To do so, add an `array` of directories, or register a callback receiving a Symfony Finder instance to further filter which files you want to append or exclude. You can do this in your App Service Provider by using the `Laravel` facade (or injecting Laraload). 170 | 171 | ```php 172 | 173 | use Symfony\Component\Finder\Finder; 174 | use Illuminate\Support\ServiceProvider; 175 | use DarkGhostHunter\Laraload\Facades\Laraload; 176 | 177 | class AppServiceProvider extends ServiceProvider 178 | { 179 | // ... 180 | 181 | public function boot() 182 | { 183 | Laraload::append(function (Finder $find) { 184 | $find->in('www/app/vendor/name/package/src')->name('*.php'); 185 | }); 186 | 187 | Laraload::exclude(function (Finder $find) { 188 | $find->in('www/app/resources/views')->name('*.blade.php'); 189 | }); 190 | } 191 | } 192 | ``` 193 | 194 | ### FAQ 195 | 196 | * **Can I disable Laraload?** 197 | 198 | [Yes.](#enable) 199 | 200 | * **Do I need to restart my PHP Server to pick up the changes?** 201 | 202 | Absolutely. Generating the script is not enough, PHP won't pick up the changes if the script path is empty or the PHP process itself is not restarted **completely**. You can schedule a server restart with CRON or something. 203 | 204 | * **The package returns errors when I used it!** 205 | 206 | Check you're using the latest PHP stable version (critical), and Opcache is enabled. Also, check your storage directory is writable. 207 | 208 | As a safe-bet, you can use the safe preloader script in `darkghosthunter/preloader/helpers/safe-preloader.php` and debug the error. 209 | 210 | If you're sure this is an error by the package, [open an issue](https://github.com/DarkGhostHunter/Laraload/issues/new) with full details and stack trace. If it's a problem on the Preloader itself, [issue it there](https://github.com/DarkGhostHunter/Preloader/issues). 211 | 212 | * **Why I can't use something like `php artisan laraload:generate` instead? Like a [Listener](https://laravel.com/docs/events) or [Scheduler](https://laravel.com/docs/scheduling)?** 213 | 214 | Opcache is not enabled when using PHP CLI, and if it is, it will gather wrong statistics. You must let the live application generate the list automatically _on demand_. 215 | 216 | * **Does this excludes the package itself from the list?** 217 | 218 | It does not: since the underlying Preloader package may be not heavily requested, it doesn't matter if its excluded or not. The files in Laraload are also not excluded from the list, since these are needed to trigger the Preloader itself without hindering performance. 219 | 220 | * **I activated Laraload but my application still doesn't feel _fast_. What's wrong?** 221 | 222 | Laraload creates a preloading script, but **doesn't load the script into Opcache**. Once the script is generated, you must include it in your `php.ini` - currently there is no other way to do it. This will take effect only at PHP process startup. 223 | 224 | If you still _feel_ your app is slow, remember to benchmark your app, cache your config and views, check your database queries and API calls, and queue expensive logic, among other things. You can also use [Laravel Octane](https://github.com/laravel/octane) on [RoadRunner](https://roadrunner.dev/). 225 | 226 | * **How the list is created?** 227 | 228 | Basically: the most hit files in descending order. Each file consumes memory, so the list is _soft-cut_ when the files reach a given memory limit (32MB by default). 229 | 230 | * **You said "_soft-cut_", why is that?** 231 | 232 | Each file is loaded using `opcache_compile_file()`. If the last file is a class with links outside the list, PHP will issue some warnings, which is normal and intended, but it won't compile the linked files if these were not added before. 233 | 234 | * **Can I just put all the files in my project?** 235 | 236 | You shouldn't. Including all the files of your application may have diminishing returns compared to, for example, only the most requested. You can always benchmark your app yourself to prove this is wrong for your exclusive case. 237 | 238 | * **Can I use a Closure for my condition?** 239 | 240 | No, you must use your default condition class or your own class, or use `Class@method` notation. 241 | 242 | * **Can I deactivate the middleware? Or check only XXX status?** 243 | 244 | Nope. If you are looking for total control, [use directly the Preloader package](https://github.com/DarkGhostHunter/Preloader/). 245 | 246 | * **Does the middleware works on unit testing?** 247 | 248 | Nope. The middleware is not registered if the application is running under Unit Testing environment. 249 | 250 | * **How can I know when a Preload script is successfully generated?** 251 | 252 | When the Preload script is called, you will receive a `PreloadCalledEvent` instance with the compilation status (`true` on success, `false` on failure). You can [add a Listener](https://laravel.com/docs/events#registering-events-and-listeners) to dispatch an email or a Slack notification. 253 | 254 | If there is a bigger problem, your application logger will catch the exception. 255 | 256 | * **Why now I need to use a callback to append/exclude files, instead of a simple array of files?** 257 | 258 | This new version uses Preloader 2, which offers greater flexibility to handle files inside a directory. This approach is incompatible with just issuing directly an array of files, but is more convenient in the long term. Considering that appending and excluding files mostly requires pin-point precision, it was decided to leave it as method calls for this kind of flexibility. 259 | 260 | * **How can I change the number of hits, cache or cache key for the default condition?** 261 | 262 | While I encourage you to create your own condition, you can easily change them by adding a [container event](https://laravel.com/docs/8.x/container#container-events) to your `AppServiceProvider.php`, under the `register()` method. 263 | 264 | ```php 265 | $this->app->when(\DarkGhostHunter\Laraload\Conditions\CountRequests::class) 266 | ->needs('$hits') 267 | ->give(1500); 268 | ``` 269 | 270 | ## License 271 | 272 | This package is licenced by the [MIT License](LICENSE). 273 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "darkghosthunter/laraload", 3 | "description": "Effortlessly make a Preload script for your Laravel application.", 4 | "keywords": [ 5 | "darkghosthunter", 6 | "laraload" 7 | ], 8 | "homepage": "https://github.com/darkghosthunter/laraload", 9 | "license": "MIT", 10 | "type": "library", 11 | "authors": [ 12 | { 13 | "name": "Italo Israel Baeza Cabrera", 14 | "email": "darkghosthunter@gmail.com", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^7.4.3||^8.0.2", 20 | "illuminate/support": "^6.0||^7.0||^8.0", 21 | "illuminate/http": "^6.0||^7.0||^8.0", 22 | "illuminate/events": "^6.0||^7.0||^8.0", 23 | "darkghosthunter/preloader": "^2.2.0", 24 | "symfony/finder": "^4.3||^5.0" 25 | }, 26 | "require-dev": { 27 | "orchestra/testbench": "^4.1||^5.0||^6.0", 28 | "phpunit/phpunit": "^9.3", 29 | "mockery/mockery": "^1.4", 30 | "orchestra/canvas": "^4.0||^5.0||^6.0" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "DarkGhostHunter\\Laraload\\": "src" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "Tests\\": "tests" 40 | } 41 | }, 42 | "scripts": { 43 | "test": "vendor/bin/phpunit --coverage-clover build/logs/clover.xml", 44 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage" 45 | }, 46 | "config": { 47 | "sort-packages": true 48 | }, 49 | "extra": { 50 | "laravel": { 51 | "providers": [ 52 | "DarkGhostHunter\\Laraload\\LaraloadServiceProvider" 53 | ], 54 | "aliases": { 55 | "Laraload": "DarkGhostHunter\\Laraload\\Facades\\Laraload" 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /config/laraload.php: -------------------------------------------------------------------------------- 1 | env('LARALOAD_ENABLE'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Condition logic 23 | |-------------------------------------------------------------------------- 24 | | 25 | | The custom condition logic you want to execute to generate (or not) the 26 | | Preload script. You can use any class using it's name, or Class@method 27 | | notation. This will be executed using the Service Container's "call". 28 | | 29 | */ 30 | 31 | 'condition' => \DarkGhostHunter\Laraload\Conditions\CountRequests::class, 32 | 33 | /* 34 | |-------------------------------------------------------------------------- 35 | | Output 36 | |-------------------------------------------------------------------------- 37 | | 38 | | Once the Preload script is generated, it will written to the storage 39 | | path of your application, since it should have permission to write. 40 | | You can change the script output for anything as long is writable. 41 | | 42 | */ 43 | 44 | 'output' => storage_path('preload.php'), 45 | 46 | /* 47 | |-------------------------------------------------------------------------- 48 | | Memory Limit 49 | |-------------------------------------------------------------------------- 50 | | 51 | | The Preloader script can be configured to handle a limited number of 52 | | files based on their memory consumption. The default is a safe bet 53 | | for most apps, but you can change it for your app specifically. 54 | | 55 | */ 56 | 57 | 'memory' => 32, 58 | 59 | /* 60 | |-------------------------------------------------------------------------- 61 | | Upload method 62 | |-------------------------------------------------------------------------- 63 | | 64 | | Opcache supports preloading files by using `require_once` (which executes 65 | | and resolves each file link), and `opcache_compile_file` (which not). If 66 | | you want to use require ensure the Composer Autoloader path is correct. 67 | | 68 | */ 69 | 70 | 'use_require' => false, 71 | 'autoload' => base_path('vendor/autoload.php'), 72 | 73 | /* 74 | |-------------------------------------------------------------------------- 75 | | Ignore Not Found 76 | |-------------------------------------------------------------------------- 77 | | 78 | | Sometimes Opcache will include in the list files that are generated by 79 | | Laravel at runtime which don't exist when deploying the application. 80 | | To avoid errors on preloads, we can tell Preloader to ignore them. 81 | | 82 | */ 83 | 84 | 'ignore-not-found' => true, 85 | 86 | ]; 87 | -------------------------------------------------------------------------------- /src/Conditions/CountRequests.php: -------------------------------------------------------------------------------- 1 | cache = $cache; 38 | $this->hits = $hits; 39 | $this->cacheKey = $cacheKey; 40 | } 41 | 42 | /** 43 | * Recreates the Preload script each given number of requests. 44 | * 45 | * @return bool 46 | */ 47 | public function __invoke(): bool 48 | { 49 | // Increment the count by one. If it doesn't exists, we will start with 1. 50 | $count = $this->cache->increment($this->cacheKey); 51 | 52 | // Each number of hits return true 53 | if ($count && $count % $this->hits === 0) { 54 | $this->cache->set($this->cacheKey, 0); 55 | return true; 56 | } 57 | 58 | return false; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Events/PreloadCalledEvent.php: -------------------------------------------------------------------------------- 1 | success = $success; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Facades/Laraload.php: -------------------------------------------------------------------------------- 1 | container = Container::getInstance(); 32 | $this->config = $config; 33 | } 34 | 35 | /** 36 | * Handle an incoming request. 37 | * 38 | * @param \Illuminate\Http\Request $request 39 | * @param \Closure $next 40 | * 41 | * @return mixed 42 | */ 43 | public function handle($request, Closure $next) 44 | { 45 | return $next($request); 46 | } 47 | 48 | /** 49 | * Perform any final actions for the request lifecycle. 50 | * 51 | * @param \Symfony\Component\HttpFoundation\Request $request 52 | * @param \Symfony\Component\HttpFoundation\Response $response 53 | * 54 | * @return void 55 | * @throws \Illuminate\Contracts\Container\BindingResolutionException 56 | */ 57 | public function terminate($request, $response) 58 | { 59 | if ($this->responseNotError($response) && $this->conditionIsTrue()) { 60 | $this->container->make(Laraload::class)->generate(); 61 | } 62 | } 63 | 64 | /** 65 | * Returns if the Response is anything but an error or an invalid response. 66 | * 67 | * @param \Psr\Http\Message\ResponseInterface|\Symfony\Component\HttpFoundation\Response $response 68 | * 69 | * @return bool 70 | */ 71 | protected function responseNotError($response): bool 72 | { 73 | return $response->getStatusCode() < 400; 74 | } 75 | 76 | /** 77 | * Checks if the given condition logic is true or false. 78 | * 79 | * @return bool 80 | */ 81 | protected function conditionIsTrue(): bool 82 | { 83 | return (bool)$this->container->call($this->config->get('laraload.condition'), [], '__invoke'); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Laraload.php: -------------------------------------------------------------------------------- 1 | config = $config->get('laraload'); 56 | $this->preloader = $preloader; 57 | $this->dispatcher = $dispatcher; 58 | } 59 | 60 | /** 61 | * Registers a callback to use to append files to the Preloader. 62 | * 63 | * @param array|string|callable $append 64 | * 65 | * @return void 66 | */ 67 | public function append($append): void 68 | { 69 | $this->append = $append; 70 | } 71 | 72 | /** 73 | * Registers a callback to use to exclude files from the Preloader. 74 | * 75 | * @param array|string|callable $exclude 76 | * 77 | * @return void 78 | */ 79 | public function exclude($exclude): void 80 | { 81 | $this->exclude = $exclude; 82 | } 83 | 84 | /** 85 | * Generates the Preloader Script. 86 | * 87 | * @return bool 88 | */ 89 | public function generate(): bool 90 | { 91 | $preloader = $this->preloader 92 | ->ignoreNotFound($this->config['ignore-not-found']) 93 | ->memoryLimit($this->config['memory']) 94 | ->exclude($this->exclude) 95 | ->append($this->append); 96 | 97 | if ($this->config['use_require']) { 98 | $preloader->useRequire($this->config['autoload']); 99 | } 100 | 101 | $this->dispatcher->dispatch(new Events\PreloadCalledEvent( 102 | $result = $preloader->writeTo($this->config['output']) 103 | )); 104 | 105 | return $result; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/LaraloadServiceProvider.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom(__DIR__ . '/../config/laraload.php', 'laraload'); 21 | 22 | $this->app->singleton(Preloader::class, fn() => Preloader::make()); 23 | $this->app->singleton(Laraload::class); 24 | } 25 | 26 | /** 27 | * Bootstrap the application services. 28 | * 29 | * @param \Illuminate\Config\Repository $config 30 | * @param \Illuminate\Contracts\Http\Kernel $kernel 31 | * 32 | * @return void 33 | */ 34 | public function boot(Repository $config, Kernel $kernel) 35 | { 36 | // We will only register the middleware if not Running Unit Tests 37 | if ($this->shouldRun($config)) { 38 | $kernel->pushMiddleware(LaraloadMiddleware::class); 39 | } 40 | 41 | if ($this->app->runningInConsole()) { 42 | $this->publishes([__DIR__ . '/../config/laraload.php' => config_path('laraload.php')], 'config'); 43 | } 44 | } 45 | 46 | /** 47 | * Checks if Laraload should run. 48 | * 49 | * @param \Illuminate\Config\Repository $config 50 | * 51 | * @return bool 52 | * 53 | * @codeCoverageIgnore 54 | */ 55 | protected function shouldRun(Repository $config): bool 56 | { 57 | // If it's null run only on production, otherwise the developer decides. 58 | return $config->get('laraload.enable') ?? $this->app->environment('production'); 59 | } 60 | } 61 | --------------------------------------------------------------------------------