├── .coveralls.yml ├── .github ├── ISSUE_TEMPLATE │ ├── ISSUE_TEMPLATE.md │ └── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── ci.yml │ └── merge.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── assets └── preloader_config.php ├── composer.json ├── phpcs.xml ├── phpmd.xml ├── phpstan.neon ├── phpunit.xml.dist ├── src ├── Command │ └── PreloaderCommand.php ├── Exception │ ├── PreloadWriteException.php │ └── ResourceNotFoundException.php ├── Filesystem.php ├── Plugin.php ├── PreloadResource.php ├── Preloader.php └── PreloaderService.php └── tests ├── Dockerfile ├── TestCase ├── Command │ └── PreloaderCommandTest.php ├── PluginTest.php └── PreloadResourceTest.php ├── bootstrap.php ├── php.ini ├── plugins.php └── test_app ├── config ├── preloader_config.php ├── preloader_config_test.php └── routes.php ├── plugins ├── MyPluginOneZz │ └── src │ │ └── Plugin.php └── MyPluginTwoZz │ └── src │ └── Plugin.php ├── src └── Application.php └── vendor ├── vendorone └── packageone │ └── src │ └── VendorOnePackageOneTestClassZz.php └── vendortwo └── packagetwo └── src └── VendorTwoPackageTwoTestClassZz.php /.coveralls.yml: -------------------------------------------------------------------------------- 1 | coverage_clover: clover.xml 2 | json_path: coveralls-upload.json -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | This is a (multiple allowed): 2 | 3 | * [x] bug 4 | * [ ] enhancement 5 | * [ ] feature-discussion (RFC) 6 | 7 | * CakePHP Application Skeleton Version: EXACT RELEASE VERSION OR COMMIT HASH, HERE. 8 | * Platform and Target: YOUR WEB-SERVER, DATABASE AND OTHER RELEVANT INFO AND HOW THE REQUEST IS BEING MADE, HERE. 9 | 10 | ### What you did 11 | EXPLAIN WHAT YOU DID, PREFERABLY WITH CODE EXAMPLES, HERE. 12 | 13 | ### What happened 14 | EXPLAIN WHAT IS ACTUALLY HAPPENING, HERE. 15 | 16 | ### What you expected to happen 17 | EXPLAIN WHAT IS TO BE EXPECTED, HERE. 18 | 19 | Before you open an issue, please check if a similar issue already exists or has been closed before. 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | test_and_analyze: 8 | name: PHP ${{ matrix.php-versions }} Test 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | operating-system: [ ubuntu-22.04 ] 13 | php-versions: ['8.1', '8.4'] 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | 18 | - name: Setup PHP 19 | uses: shivammathur/setup-php@v2 20 | with: 21 | php-version: ${{ matrix.php-versions }} 22 | extensions: mbstring, intl, xdebug 23 | 24 | - name: PHP Version 25 | run: php -v 26 | 27 | - name: Install dependencies 28 | if: steps.composer-cache.outputs.cache-hit != 'true' 29 | run: | 30 | composer validate 31 | composer install --prefer-dist --no-progress --no-suggest 32 | 33 | - name: Test Suite + Static Analysis 34 | run: composer check 35 | 36 | coverage: 37 | name: Code Coverage 38 | runs-on: ubuntu-latest 39 | strategy: 40 | matrix: 41 | operating-system: [ ubuntu-22.04 ] 42 | php-versions: ['8.1'] 43 | steps: 44 | - name: Checkout 45 | uses: actions/checkout@v2 46 | 47 | - name: Setup PHP 48 | uses: shivammathur/setup-php@v2 49 | with: 50 | php-version: ${{ matrix.php-versions }} 51 | extensions: mbstring, intl, xdebug 52 | 53 | - name: PHP Version 54 | run: php -v 55 | 56 | - name: Install dependencies 57 | if: steps.composer-cache.outputs.cache-hit != 'true' 58 | run: | 59 | composer validate 60 | composer install --prefer-dist --no-progress --no-suggest 61 | 62 | - name: Code Coverage 63 | env: 64 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} 65 | run: | 66 | composer global require php-coveralls/php-coveralls 67 | export CODECOVERAGE=1 && vendor/bin/phpunit --coverage-clover=clover.xml 68 | php-coveralls --coverage_clover=clover.xml -v 69 | 70 | cakephp_version_compatibility: 71 | name: CakePHP ${{ matrix.cakephp-versions }} Test 72 | runs-on: ubuntu-latest 73 | strategy: 74 | matrix: 75 | version: ['~5.0', '^5.1'] 76 | steps: 77 | - name: Checkout 78 | uses: actions/checkout@v2 79 | 80 | - name: Setup PHP 81 | uses: shivammathur/setup-php@v2 82 | with: 83 | php-version: '8.1' 84 | extensions: mbstring, intl 85 | 86 | - name: PHP Version 87 | run: php -v 88 | 89 | - name: CakePHP ${{matrix.version}} Compatability 90 | run: | 91 | composer self-update 92 | rm -rf composer.lock 93 | composer require cakephp/cakephp:${{matrix.version}} --no-update 94 | composer install --prefer-dist --no-progress 95 | composer test -------------------------------------------------------------------------------- /.github/workflows/merge.yml: -------------------------------------------------------------------------------- 1 | name: Merge 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | jobs: 8 | run: 9 | name: Report coverage 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | operating-system: [ ubuntu-22.04 ] 14 | php-versions: ['8.1'] 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v2 18 | 19 | - name: Setup PHP 20 | uses: shivammathur/setup-php@v2 21 | with: 22 | php-version: ${{ matrix.php-versions }} 23 | extensions: mbstring, intl, xdebug 24 | 25 | - name: PHP Version 26 | run: php -v 27 | 28 | - name: Install dependencies 29 | if: steps.composer-cache.outputs.cache-hit != 'true' 30 | run: | 31 | composer validate 32 | composer install --prefer-dist --no-progress --no-suggest 33 | 34 | - name: Test 35 | run: vendor/bin/phpunit 36 | 37 | - name: Report Coverage 38 | env: 39 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | run: | 41 | composer check 42 | composer global require php-coveralls/php-coveralls 43 | export CODECOVERAGE=1 && vendor/bin/phpunit --coverage-clover=clover.xml 44 | php-coveralls --coverage_clover=clover.xml -v 45 | integration_test: 46 | name: Integration Test (Docker) 47 | runs-on: ubuntu-latest 48 | steps: 49 | - name: Checkout 50 | uses: actions/checkout@v2 51 | - name: Get branch name 52 | id: branch-name 53 | uses: tj-actions/branch-names@v5.1 54 | - name: Docker Build 55 | run: docker build -t cakepreloader:test tests/ --no-cache --build-arg BRANCH=dev-${{ steps.branch-name.outputs.current_branch }} 56 | - name: Docker Run 57 | run: docker run -d cakepreloader:test 58 | - name: Test Container 59 | run: | 60 | if docker ps | grep "cakepreloader:test"; then 61 | echo "container is running" 62 | else 63 | echo "container is not running" 64 | exit 1 65 | fi -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # CakePHP specific files # 2 | ########################## 3 | /config/app_local.php 4 | /config/.env 5 | /logs/* 6 | /tmp/* 7 | /vendor/* 8 | 9 | # OS generated files # 10 | ###################### 11 | .DS_Store 12 | .DS_Store? 13 | ._* 14 | .Spotlight-V100 15 | .Trashes 16 | Icon? 17 | ehthumbs.db 18 | Thumbs.db 19 | .directory 20 | 21 | # Tool specific files # 22 | ####################### 23 | # PHPUnit 24 | .phpunit.result.cache 25 | # vim 26 | *~ 27 | *.swp 28 | *.swo 29 | # sublime text & textmate 30 | *.sublime-* 31 | *.stTheme.cache 32 | *.tmlanguage.cache 33 | *.tmPreferences.cache 34 | # Eclipse 35 | .settings/* 36 | # JetBrains, aka PHPStorm, IntelliJ IDEA 37 | .idea/* 38 | # NetBeans 39 | nbproject/* 40 | # Visual Studio Code 41 | .vscode 42 | # Sass preprocessor 43 | .sass-cache/ 44 | 45 | # Project specific files # 46 | ########################## 47 | tests/test_app/preload.php 48 | tests/test_app/a-unique-name.php 49 | /composer.lock 50 | /coverage-reports/ 51 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Chris Nizzardini 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CakePHP Preloader 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/cnizzardini/cakephp-preloader.svg?style=flat-square)](https://packagist.org/packages/cnizzardini/cakephp-preloader) 4 | [![Build](https://github.com/cnizzardini/cakephp-preloader/actions/workflows/merge.yml/badge.svg)](https://github.com/cnizzardini/cakephp-preloader/actions/workflows/merge.yml) 5 | [![Coverage Status](https://coveralls.io/repos/github/cnizzardini/cakephp-preloader/badge.svg?branch=main)](https://coveralls.io/github/cnizzardini/cakephp-preloader?branch=main) 6 | [![License: MIT](https://img.shields.io/badge/license-mit-blue)](LICENSE.md) 7 | [![CakePHP](https://img.shields.io/badge/cakephp-^5.0-red?logo=cakephp)](https://book.cakephp.org/4/en/index.html) 8 | [![Minimum PHP Version](https://img.shields.io/badge/php-%3E%3D%208.1-8892BF.svg?logo=php)](https://php.net/) 9 | 10 | An OPCache preloader for CakePHP. 11 | 12 | Reference: https://www.php.net/manual/en/opcache.preloading.php 13 | 14 | This package is meant to provide an easy way for CakePHP application developers to generate preload 15 | files. Goals: 16 | 17 | - Generate an OPCache preloader with a simple command. 18 | - Allow optionally loading additional resources such as CakePHP plugins, userland app, and 19 | composer packages. 20 | - Provide a simplistic API for writing a custom preloader. 21 | 22 | For an alternative approach, checkout [DarkGhostHunter/Preloader](https://github.com/DarkGhostHunter/Preloader). 23 | 24 | For an OPCache UI, checkout [amnuts/opcache-gui](https://github.com/amnuts/opcache-gui). 25 | 26 | The current release is for CakePHP 5 and PHP 8.1, see previous releases for older versions of CakePHP and PHP. 27 | 28 | | Version | Branch | Cake Version | PHP Version | 29 | |---------|----------------------------------------------------------------|--------------|-------------| 30 | | 1.* | [main](https://github.com/cnizzardini/cakephp-preloader) | ^5.0 | ^8.1 | 31 | | 0.* | [v0](https://github.com/cnizzardini/cakephp-preloader/tree/v0) | ^4.2 | ^7.4 | 32 | 33 | ## Installation 34 | 35 | You can install this plugin into your CakePHP application using [composer](https://getcomposer.org). 36 | 37 | The recommended way to install composer packages is: 38 | 39 | ```console 40 | composer require cnizzardini/cakephp-preloader 41 | ``` 42 | 43 | Next, load the plugin: 44 | 45 | ```shell 46 | bin/cake plugin load CakePreloader --only-cli 47 | ``` 48 | 49 | Or via manual steps in the CakePHP [plugin documentation](https://book.cakephp.org/5/en/plugins.html#loading-a-plugin). 50 | 51 | ## Usage 52 | 53 | The easiest way to use CakePreloader is via the console command. This command can easily be included as 54 | part of your applications build process. 55 | 56 | ```console 57 | /srv/app $ bin/cake preloader --help 58 | Generate a preload file 59 | 60 | Usage: 61 | cake preloader [options] 62 | 63 | Options: 64 | 65 | --app Add your applications src directory into the preloader 66 | --help, -h Display this help. 67 | --name The preload file path. (default: ROOT . DS . 'preload.php') 68 | --packages A comma separated list of packages (e.g. vendor-name/package-name) to add to the preloader 69 | --plugins A comma separated list of your plugins to load or `*` to load all plugins/* 70 | --cli Should the preloader file exit when run via the php-cli? (default: true) 71 | --quiet, -q Enable quiet output. 72 | --verbose, -v Enable verbose output. 73 | ``` 74 | 75 | You may also load configurations from a `config/preloader_config.php` file. Please note, **command line arguments take 76 | precedence**. See [assets/preloader_config.php](assets/preloader_config.php) for a sample configuration file. If you 77 | prefer handling configurations another way read the CakePHP documentation on 78 | [loading configuration files](https://book.cakephp.org/5/en/development/configuration.html#loading-configuration-files). 79 | 80 | ### Examples: 81 | 82 | Default loads in CakePHP core files excluding TestSuite, Console, Command, and Shell namespaces. The preload file is 83 | written to `ROOT . DS . 'preload.php'`: 84 | 85 | ```console 86 | bin/cake preloader 87 | ``` 88 | 89 | Include a list of composer packages: 90 | 91 | ```console 92 | bin/cake preloader --packages=cakephp/authentication,cakephp/chronos 93 | ``` 94 | 95 | Include your `APP` code: 96 | 97 | ```console 98 | bin/cake preloader --app 99 | ``` 100 | 101 | Include all your projects plugins: 102 | 103 | ```console 104 | bin/cake preloader --plugins=* 105 | ``` 106 | 107 | Include a list of your projects plugins: 108 | 109 | ```console 110 | bin/cake preloader --plugins=MyPlugin,MyOtherPlugin 111 | ``` 112 | 113 | ### Before Write Event 114 | 115 | You can extend functionality by listening for the `CakePreloader.beforeWrite` event. This is dispatched just before 116 | your preloader file is written. 117 | 118 | ```php 119 | (\Cake\Event\EventManager::instance())->on('CakePreloader.beforeWrite', function(Event $event){ 120 | /** @var Preloader $preloader */ 121 | $preloader = $event->getSubject(); 122 | $resources = $preloader->getPreloadResources(); 123 | // modify resources or whatever... 124 | $preloader->setPreloadResources($resources); 125 | }); 126 | ``` 127 | 128 | For more on events, read the CakePHP [Events System](https://book.cakephp.org/5/en/core-libraries/events.html#registering-listeners) documentation. 129 | 130 | ### Preloader Class 131 | 132 | You can customize your OPCache Preloader using the same class used by the console command. Preloader uses a port of 133 | CakePHP 4.x's Filesystem class under the hood. 134 | 135 | ```php 136 | use CakePreloader\Preloader; 137 | 138 | $preloader = new Preloader(); 139 | $preloader->loadPath('/required/path/to/files', function (\SplFileInfo $file) { 140 | // optional call back method, return true to add the file to the preloader 141 | return true; 142 | }); 143 | 144 | // default path is ROOT . DS . 'preload.php' 145 | $preloader->write('/optional/path/to/preloader-file.php'); 146 | ``` 147 | 148 | ## Performance: 149 | 150 | Obviously, these types of benchmarks should be taken with a bit of a gain of salt. I benchmarked this using apache 151 | bench with this project here: https://github.com/mixerapi/demo which is a dockerized REST API 152 | (LEMP stack on alpine + php-fpm 8.0). CakePHP `DEBUG` was set to false. 153 | 154 | ```ini 155 | extension=intl.so 156 | extension=pdo_mysql.so 157 | extension=sodium 158 | extension=zip.so 159 | zend_extension=opcache.so 160 | 161 | [php] 162 | session.auto_start = Off 163 | short_open_tag = Off 164 | opcache.preload_user=root 165 | opcache.preload=/srv/app/preload.php 166 | opcache.interned_strings_buffer = 16 167 | opcache.max_accelerated_files = 20000 168 | opcache.memory_consumption = 256 169 | opcache.enable_cli = 0 170 | opcache.enable = 1 171 | opcache.revalidate_freq = 360 172 | opcache.fast_shutdown = 1 173 | realpath_cache_size = 4096K 174 | realpath_cache_ttl = 600 175 | ``` 176 | 177 | Note: `opcache.preload_user=root` and `opcache.preload=/srv/app/preload.php` were disabled for the no preload run. 178 | 179 | | Type | JSON View (no db) | JSON View (db select) | 180 | |---------------------------|------------------------|-------------------------| 181 | | OPCache Only | 892.69 [#/sec] (mean) | 805.29 [#/sec] (mean) | 182 | | OPCache Preload (default) | 1149.08 [#/sec] (mean) | 976.30 [#/sec] (mean) | 183 | 184 | 185 | This is 28% more requests per second for JSON responses and 21% more requests per second with JSON + simple SQL select 186 | when OPCache Preload is enabled. 187 | 188 | ## Tests / Analysis 189 | 190 | Test Suite: 191 | 192 | ```console 193 | composer test 194 | ``` 195 | 196 | Test Suite + Static Analysis: 197 | 198 | ```console 199 | composer check 200 | ``` 201 | -------------------------------------------------------------------------------- /assets/preloader_config.php: -------------------------------------------------------------------------------- 1 | [ 10 | /* 11 | |-------------------------------------------------------------------------- 12 | | name 13 | |-------------------------------------------------------------------------- 14 | | 15 | | The preload file path. (default: ROOT . DS . 'preload.php') 16 | | 17 | */ 18 | 'name' => ROOT . DS . 'preload.php', 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | app 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Should your APP files be included in the preloader (default: false)? 26 | | 27 | */ 28 | 'app' => false, 29 | 30 | /* 31 | |-------------------------------------------------------------------------- 32 | | packages 33 | |-------------------------------------------------------------------------- 34 | | 35 | | An array of composer packages to include in your preloader. 36 | | 37 | | @example ['cakephp/authentication','cakephp/chronos'] 38 | */ 39 | 'packages' => [], 40 | 41 | /* 42 | |-------------------------------------------------------------------------- 43 | | plugins 44 | |-------------------------------------------------------------------------- 45 | | 46 | | An array of your applications plugins to include in your preloader. To include 47 | | all your plugins set to `true`. 48 | | 49 | | @example ['MyPlugin','MyOtherPlugin'] 50 | */ 51 | 'plugins' => [], 52 | ] 53 | ]; 54 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cnizzardini/cakephp-preloader", 3 | "description": "OPCache Preloader plugin for CakePHP", 4 | "type": "cakephp-plugin", 5 | "license": "MIT", 6 | "keywords": ["cakephp","preload","preloader","opcache preloader","cakephp preloader", "cakephp opcache preloader", "cli"], 7 | "require": { 8 | "php": "^8.1", 9 | "cakephp/cakephp": "^5.0" 10 | }, 11 | "require-dev": { 12 | "phpunit/phpunit": "^10", 13 | "cakephp/cakephp-codesniffer": "^5.0", 14 | "phpmd/phpmd": "^2.10", 15 | "phpstan/phpstan": "^1.0" 16 | }, 17 | "autoload": { 18 | "psr-4": { 19 | "CakePreloader\\": "src/" 20 | } 21 | }, 22 | "autoload-dev": { 23 | "psr-4": { 24 | "CakePreloader\\Test\\": "tests/", 25 | "CakePreloader\\Test\\App\\": "tests/test_app/src/" 26 | } 27 | }, 28 | "scripts": { 29 | "check": [ 30 | "@test", 31 | "@phpcs", 32 | "@phpstan", 33 | "@phpmd" 34 | ], 35 | "phpcs": "phpcs --colors -p src/", 36 | "phpcbf": "phpcbf --colors -p src/", 37 | "phpstan": "phpstan analyse", 38 | "test": "phpunit --colors=always", 39 | "phpmd": "phpmd src/ ansi phpmd.xml", 40 | "coverage": "phpunit --coverage-html coverage-reports/" 41 | }, 42 | "support": { 43 | "issues": "https://github.com/cnizzardini/cakephp-preloader/issues", 44 | "source": "https://github.com/cnizzardini/cakephp-preloader" 45 | }, 46 | "authors": [ 47 | { 48 | "name": "Chris Nizzardini", 49 | "role": "Developer" 50 | } 51 | ], 52 | "config": { 53 | "allow-plugins": { 54 | "dealerdirect/phpcodesniffer-composer-installer": true 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /phpmd.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CakePreloader Ruleset 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 70 | 71 | 72 | 73 | 74 | 75 | 78 | 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 7 3 | checkMissingIterableValueType: false 4 | treatPhpDocTypesAsCertain: false 5 | paths: 6 | - src 7 | excludes_analyse: 8 | - src/Console/Installer.php 9 | bootstrapFiles: 10 | - tests/bootstrap.php 11 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | tests/TestCase/ 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | src/ 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Command/PreloaderCommand.php: -------------------------------------------------------------------------------- 1 | preloaderService = $preloaderService; 31 | } 32 | 33 | /** 34 | * Generates a preload file 35 | * 36 | * @param \Cake\Console\Arguments $args The command arguments. 37 | * @param \Cake\Console\ConsoleIo $io The console io 38 | * @return int|null The exit code or null for success 39 | */ 40 | public function execute(Arguments $args, ConsoleIo $io): ?int 41 | { 42 | $io->hr(); 43 | $io->out('Generating preloader...'); 44 | $io->hr(); 45 | 46 | $io->info('Preloader Config: ' . (Configure::check('PreloaderConfig') ? 'present' : 'not found')); 47 | 48 | try { 49 | $path = $this->preloaderService->generate($args, $io); 50 | $io->hr(); 51 | $io->success('Preload written to ' . $path); 52 | $io->out('You must restart your PHP service for the changes to take effect.'); 53 | $io->hr(); 54 | 55 | return static::CODE_SUCCESS; 56 | } catch (PreloadWriteException $e) { 57 | $io->err($e->getMessage()); 58 | 59 | return static::CODE_ERROR; 60 | } 61 | } 62 | 63 | /** 64 | * Get the option parser. 65 | * 66 | * @param \Cake\Console\ConsoleOptionParser $parser The option parser to update 67 | * @return \Cake\Console\ConsoleOptionParser 68 | */ 69 | public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser 70 | { 71 | $parser 72 | ->setDescription('Generate a preload file') 73 | ->addOption('name', [ 74 | 'help' => 'The preload file path (default: ROOT . DS . preload.php)', 75 | ]) 76 | ->addOption('app', [ 77 | 'help' => 'Add your applications src directory into the preloader', 78 | 'boolean' => true, 79 | ]) 80 | ->addOption('plugins', [ 81 | 'help' => 'A comma separated list of your plugins to load or `*` to load all plugins/*', 82 | ]) 83 | ->addOption('packages', [ 84 | 'help' => 'A comma separated list of packages (e.g. vendor-name/package-name) to add to the preloader', 85 | ]) 86 | ->addOption('cli', [ 87 | 'help' => 'Should the preloader file load when run via php-cli?', 88 | 'boolean' => true, 89 | 'default' => false, 90 | ]); 91 | 92 | if (defined('TEST')) { 93 | $parser->addOption('phpunit', [ 94 | 'help' => '(FOR TESTING ONLY)', 95 | 'boolean' => true, 96 | 'default' => false, 97 | ]); 98 | } 99 | 100 | return $parser; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Exception/PreloadWriteException.php: -------------------------------------------------------------------------------- 1 | filterIterator($directory, $filter); 64 | } 65 | 66 | /** 67 | * Find files/ directories recursively in given directory path. 68 | * 69 | * @param string $path Directory path. 70 | * @param mixed $filter If string will be used as regex for filtering using 71 | * `RegexIterator`, if callable will be as callback for `CallbackFilterIterator`. 72 | * Hidden directories (starting with dot e.g. .git) are always skipped. 73 | * @param int|null $flags Flags for FilesystemIterator::__construct(); 74 | * @return \Iterator 75 | */ 76 | public function findRecursive(string $path, mixed $filter = null, ?int $flags = null): Iterator 77 | { 78 | $flags = $flags ?? FilesystemIterator::KEY_AS_PATHNAME 79 | | FilesystemIterator::CURRENT_AS_FILEINFO 80 | | FilesystemIterator::SKIP_DOTS; 81 | $directory = new RecursiveDirectoryIterator($path, $flags); 82 | 83 | $dirFilter = new RecursiveCallbackFilterIterator( 84 | $directory, 85 | function (SplFileInfo $current) { 86 | if ($current->getFilename()[0] === '.' && $current->isDir()) { 87 | return false; 88 | } 89 | 90 | return true; 91 | }, 92 | ); 93 | 94 | $flatten = new RecursiveIteratorIterator( 95 | $dirFilter, 96 | RecursiveIteratorIterator::CHILD_FIRST, 97 | ); 98 | 99 | if ($filter === null) { 100 | return $flatten; 101 | } 102 | 103 | return $this->filterIterator($flatten, $filter); 104 | } 105 | 106 | /** 107 | * Wrap iterator in additional filtering iterator. 108 | * 109 | * @param \Iterator $iterator Iterator 110 | * @param mixed $filter Regex string or callback. 111 | * @return \Iterator 112 | */ 113 | protected function filterIterator(Iterator $iterator, mixed $filter): Iterator 114 | { 115 | if (is_string($filter)) { 116 | return new RegexIterator($iterator, $filter); 117 | } 118 | 119 | return new CallbackFilterIterator($iterator, $filter); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Plugin.php: -------------------------------------------------------------------------------- 1 | $app PluginApplicationInterface 35 | * @return void 36 | */ 37 | public function bootstrap(PluginApplicationInterface $app): void 38 | { 39 | if (file_exists(CONFIG . 'preloader_config.php')) { 40 | Configure::load('preloader_config', 'default'); 41 | } 42 | } 43 | 44 | /** 45 | * @param \Cake\Console\CommandCollection $commands CommandCollection 46 | * @return \Cake\Console\CommandCollection 47 | */ 48 | public function console(CommandCollection $commands): CommandCollection 49 | { 50 | $commands->add('preloader', PreloaderCommand::class); 51 | 52 | return $commands; 53 | } 54 | 55 | /** 56 | * @inheritDoc 57 | */ 58 | public function services(ContainerInterface $container): void 59 | { 60 | if (PHP_SAPI === 'cli') { 61 | $container 62 | ->add(PreloaderService::class); 63 | 64 | $container 65 | ->add(PreloaderCommand::class) 66 | ->addArgument(PreloaderService::class); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/PreloadResource.php: -------------------------------------------------------------------------------- 1 | type = $type; 55 | $this->file = $file; 56 | } 57 | 58 | /** 59 | * @return string 60 | */ 61 | public function getType(): string 62 | { 63 | return $this->type; 64 | } 65 | 66 | /** 67 | * @return string 68 | */ 69 | public function getFile(): string 70 | { 71 | return $this->file; 72 | } 73 | 74 | /** 75 | * Sets the file path 76 | * 77 | * @param string $file The new file path 78 | * @return self 79 | */ 80 | public function setPath(string $file): self 81 | { 82 | $this->file = $file; 83 | 84 | return $this; 85 | } 86 | 87 | /** 88 | * Returns the resource to be preloaded as either a require_once or opcache_compile_file string 89 | * 90 | * @return string 91 | * @throws \CakePreloader\Exception\ResourceNotFoundException 92 | */ 93 | public function getResource(): string 94 | { 95 | if (!file_exists($this->file)) { 96 | throw new ResourceNotFoundException( 97 | 'File `' . $this->file . '` does not exist', 98 | ); 99 | } 100 | 101 | return $this->type . "('" . $this->file . "'); \n"; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Preloader.php: -------------------------------------------------------------------------------- 1 | 30 | */ 31 | private array $preloadResources = []; 32 | 33 | /** 34 | * Returns an array of PreloadResource after sorting alphabetically 35 | * 36 | * @return array<\CakePreloader\PreloadResource> 37 | */ 38 | public function getPreloadResources(): array 39 | { 40 | uasort($this->preloadResources, function (PreloadResource $a, PreloadResource $b) { 41 | return strcasecmp($a->getFile(), $b->getFile()); 42 | }); 43 | 44 | return $this->preloadResources; 45 | } 46 | 47 | /** 48 | * Sets preloadResources 49 | * 50 | * @param array<\CakePreloader\PreloadResource> $preloadResources the array of PreloadResource instances 51 | * @return $this 52 | */ 53 | public function setPreloadResources(array $preloadResources) 54 | { 55 | $this->preloadResources = $preloadResources; 56 | 57 | return $this; 58 | } 59 | 60 | /** 61 | * Loads files in the file system $path recursively as PreloadResources after applying the optional callback. Note, 62 | * loading script files has been disabled by the library in CakePHP 5. 63 | * 64 | * @param string $path The file system path 65 | * @param callable|null $callback An optional callback which receives SplFileInfo as an argument 66 | * @return $this 67 | */ 68 | public function loadPath(string $path, ?callable $callback = null) 69 | { 70 | $iterator = (new Filesystem())->findRecursive( 71 | $path, 72 | function (SplFileInfo $file) use ($callback) { 73 | if ($file->getExtension() !== 'php') { 74 | return false; 75 | } 76 | 77 | return is_callable($callback) ? $callback($file) : true; 78 | }, 79 | ); 80 | 81 | /** @var \SplFileInfo $file */ 82 | foreach ($iterator as $file) { 83 | $result = $this->isClass($file); 84 | if ($result === true) { 85 | $this->preloadResources[] = new PreloadResource('require_once', $file->getPathname()); 86 | } 87 | } 88 | 89 | return $this; 90 | } 91 | 92 | /** 93 | * Write preloader to the specified path 94 | * 95 | * @param string $path Default file path is ROOT . 'preload.php' 96 | * @return bool 97 | * @throws \RuntimeException 98 | */ 99 | public function write(string $path = ROOT . DS . 'preload.php'): bool 100 | { 101 | if ((file_exists($path) && !is_writable($path))) { 102 | throw new RuntimeException('File path is not writable: ' . $path); 103 | } 104 | 105 | EventManager::instance()->dispatch(new Event('CakePreloader.beforeWrite', $this)); 106 | 107 | return (bool)file_put_contents($path, $this->contents()); 108 | } 109 | 110 | /** 111 | * @param bool $bool Should the preload file continue when run via php-cli? 112 | * @return $this 113 | */ 114 | public function allowCli(bool $bool) 115 | { 116 | $this->allowCli = $bool; 117 | 118 | return $this; 119 | } 120 | 121 | /** 122 | * Returns a string to be written to the preload file 123 | * 124 | * @return string 125 | * @throws \RuntimeException 126 | */ 127 | private function contents(): string 128 | { 129 | ob_start(); 130 | 131 | $title = sprintf("# Preload Generated at %s \n", DateTime::now()); 132 | 133 | if ($this->allowCli) { 134 | $ignores = "['phpdbg']"; 135 | } else { 136 | $ignores = "['cli', 'phpdbg']"; 137 | } 138 | 139 | echo "getPreloadResources() as $resource) { 149 | try { 150 | if ($resource->getType() === 'require_once') { 151 | echo $resource->getResource(); 152 | continue; 153 | } 154 | $scripts[] = $resource->getResource(); 155 | } catch (ResourceNotFoundException $e) { 156 | triggerWarning('Preloader skipped the following: ' . $e->getMessage()); 157 | } 158 | } 159 | 160 | if (!empty($scripts)) { 161 | echo "# Scripts \n"; 162 | echo implode('', $scripts); 163 | } 164 | 165 | $content = ob_get_contents(); 166 | ob_end_clean(); 167 | 168 | if (!is_string($content)) { 169 | throw new RuntimeException('Unable to generate contents for preload'); 170 | } 171 | 172 | return $content; 173 | } 174 | 175 | /** 176 | * Returns false if the file name is not PSR-4, true if it as and is a class, null otherwise. 177 | * 178 | * @param \SplFileInfo $file Instance of SplFileInfo 179 | * @return bool|null 180 | */ 181 | private function isClass(SplFileInfo $file): ?bool 182 | { 183 | if (Inflector::camelize($file->getFilename()) !== $file->getFilename()) { 184 | return false; 185 | } 186 | 187 | $contents = file_get_contents($file->getPathname()); 188 | if (!$contents) { 189 | return null; 190 | } 191 | 192 | $className = str_replace('.php', '', $file->getFilename()); 193 | if (strstr($contents, "class $className") && strstr($contents, 'namespace')) { 194 | return true; 195 | } 196 | 197 | return null; 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/PreloaderService.php: -------------------------------------------------------------------------------- 1 | preloader = $preloader ?? new Preloader(); 22 | } 23 | 24 | /** 25 | * Generate preloader. Returns the path on success, otherwise throws exception. 26 | * 27 | * @param \Cake\Console\Arguments $args CLI Arguments 28 | * @param \Cake\Console\ConsoleIo $io ConsoleIo 29 | * @return string 30 | * @throws \CakePreloader\Exception\PreloadWriteException 31 | */ 32 | public function generate(Arguments $args, ConsoleIo $io): string 33 | { 34 | $this->cakephp(); 35 | $this->packages($args, $io); 36 | $this->app($args); 37 | $this->plugins($args); 38 | 39 | $path = $args->getOption('name') ?? Configure::read('PreloaderConfig.name'); 40 | $path = !empty($path) ? $path : ROOT . DS . 'preload.php'; 41 | 42 | $this->preloader->allowCli((bool)$args->getOption('cli')); 43 | 44 | if ($this->preloader->write($path)) { 45 | return $path; 46 | } 47 | 48 | throw new PreloadWriteException("Writing to $path failed"); 49 | } 50 | 51 | /** 52 | * Loads the CakePHP framework 53 | * 54 | * @return void 55 | */ 56 | private function cakephp(): void 57 | { 58 | $ignorePaths = implode('|', ['src\/Console', 'src\/Command', 'src\/Shell', 'src\/TestSuite']); 59 | 60 | $this->preloader->loadPath(CAKE, function (SplFileInfo $file) use ($ignorePaths) { 61 | return !preg_match("/($ignorePaths)/", $file->getPathname()); 62 | }); 63 | } 64 | 65 | /** 66 | * Adds a list of vendor packages 67 | * 68 | * @param \Cake\Console\Arguments $args The command arguments. 69 | * @param \Cake\Console\ConsoleIo $io ConsoleIo 70 | * @return void 71 | */ 72 | private function packages(Arguments $args, ConsoleIo $io): void 73 | { 74 | $packages = $args->getOption('packages') ?? Configure::read('PreloaderConfig.packages'); 75 | if (empty($packages)) { 76 | return; 77 | } 78 | 79 | if (is_string($packages)) { 80 | $packages = explode(',', (string)$args->getOption('packages')); 81 | } 82 | 83 | $packages = array_map( 84 | function ($package) { 85 | return ROOT . DS . 'vendor' . DS . $package; 86 | }, 87 | $packages, 88 | ); 89 | 90 | $validPackages = array_filter($packages, function ($package) { 91 | if (file_exists($package)) { 92 | return true; 93 | } 94 | }); 95 | 96 | if (count($packages) != count($validPackages)) { 97 | $io->out('One or more packages not found'); 98 | } 99 | 100 | foreach ($validPackages as $package) { 101 | $this->preloader->loadPath($package, function (SplFileInfo $file) use ($args) { 102 | if ($args->getOption('phpunit')) { 103 | return true; 104 | } 105 | 106 | return !strstr($file->getPath(), '/tests/'); 107 | }); 108 | } 109 | } 110 | 111 | /** 112 | * Adds the users APP into the preloader 113 | * 114 | * @param \Cake\Console\Arguments $args The command arguments. 115 | * @return void 116 | */ 117 | private function app(Arguments $args): void 118 | { 119 | if (($args->hasOption('app') && !$args->getOption('app')) && !Configure::read('PreloaderConfig.app')) { 120 | return; 121 | } 122 | 123 | $ignorePaths = ['src\/Console', 'src\/Command']; 124 | if (!$args->getOption('phpunit')) { 125 | $ignorePaths[] = 'tests\/'; 126 | } 127 | 128 | $ignorePattern = implode('|', $ignorePaths); 129 | 130 | $this->preloader->loadPath(APP, function (SplFileInfo $file) use ($ignorePattern) { 131 | return !preg_match("/($ignorePattern)/", $file->getPathname()); 132 | }); 133 | } 134 | 135 | /** 136 | * Adds the users plugins into the preloader 137 | * 138 | * @param \Cake\Console\Arguments $args The command arguments. 139 | * @return void 140 | */ 141 | private function plugins(Arguments $args): void 142 | { 143 | $plugins = $args->getOption('plugins') ?? Configure::read('PreloaderConfig.plugins'); 144 | if (empty($plugins)) { 145 | return; 146 | } 147 | 148 | $paths = []; 149 | if ($plugins === '*' || $plugins === true) { 150 | $paths[] = ROOT . DS . 'plugins'; 151 | } elseif (is_string($plugins)) { 152 | $plugins = explode(',', (string)$args->getOption('plugins')); 153 | } 154 | 155 | if (is_array($plugins)) { 156 | foreach ($plugins as $plugin) { 157 | $paths[] = ROOT . DS . 'plugins' . DS . $plugin . DS . 'src'; 158 | } 159 | } 160 | 161 | $ignorePaths = ['src\/Console', 'src\/Command']; 162 | if (!$args->getOption('phpunit')) { 163 | $ignorePaths[] = 'tests\/'; 164 | } 165 | 166 | $ignorePattern = implode('|', $ignorePaths); 167 | 168 | foreach ($paths as $path) { 169 | $this->preloader->loadPath($path, function (SplFileInfo $file) use ($ignorePattern) { 170 | return !preg_match("/($ignorePattern)/", $file->getPathname()); 171 | }); 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /tests/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM cnizzardini/php-fpm-alpine:8.1-latest 2 | 3 | ARG BRANCH=main 4 | 5 | COPY php.ini /usr/local/etc/php/php.ini 6 | 7 | WORKDIR /srv/app 8 | 9 | COPY --from=composer /usr/bin/composer /usr/bin/composer 10 | 11 | RUN composer create-project --prefer-dist --no-interaction cakephp/app:5.0 . 12 | RUN composer require cnizzardini/cakephp-preloader:$BRANCH 13 | COPY plugins.php /srv/app/config/plugins.php 14 | RUN bin/cake preloader 15 | 16 | CMD ["php-fpm"] -------------------------------------------------------------------------------- /tests/TestCase/Command/PreloaderCommandTest.php: -------------------------------------------------------------------------------- 1 | setAppNamespace('CakePreloader\Test\App'); 26 | 27 | $this->mockService(PreloaderService::class, function () { 28 | return new PreloaderService(new Preloader()); 29 | }); 30 | } 31 | 32 | public function tearDown(): void 33 | { 34 | parent::tearDown(); 35 | if (file_exists(ROOT . DS . 'preload.php')) { 36 | unlink(ROOT . DS . 'preload.php'); 37 | } 38 | if (file_exists(ROOT . DS . 'a-unique-name.php')) { 39 | unlink(ROOT . DS . 'a-unique-name.php'); 40 | } 41 | } 42 | 43 | public function test_default(): void 44 | { 45 | $this->exec('preloader'); 46 | $this->assertExitSuccess(); 47 | $this->assertFileExists(ROOT . DS . 'preload.php'); 48 | 49 | /** @var string $preload */ 50 | $preload = file_get_contents(ROOT . DS . 'preload.php'); 51 | $this->assertTrue(is_string($preload)); 52 | 53 | $this->assertStringContainsString("if (in_array(PHP_SAPI, ['cli', 'phpdbg'], true))", $preload); 54 | 55 | $contains = [ 56 | 'vendor/autoload.php', 57 | 'vendor/cakephp/cakephp/src/Cache/Cache.php', 58 | 'require_once', 59 | //'opcache_compile_file', 60 | ]; 61 | 62 | foreach ($contains as $file) { 63 | $this->assertStringContainsString($file, $preload); 64 | } 65 | 66 | $excludes = [ 67 | 'vendor/cakephp/cakephp/src/Database/Exception.php', 68 | 'vendor/cakephp/cakephp/src/Database/Expression/Comparison.php', 69 | 'vendor/cakephp/cakephp/src/Http/ControllerFactory.php', 70 | 'vendor/cakephp/cakephp/src/Routing/Exception/MissingControllerException.php', 71 | ]; 72 | 73 | foreach ($excludes as $file) { 74 | $this->assertStringNotContainsString($file, $preload); 75 | } 76 | } 77 | 78 | public function test_cli(): void 79 | { 80 | $this->exec('preloader --cli'); 81 | $this->assertExitSuccess(); 82 | $this->assertFileExists(ROOT . DS . 'preload.php'); 83 | 84 | /** @var string $preload */ 85 | $preload = file_get_contents(ROOT . DS . 'preload.php'); 86 | $this->assertTrue(is_string($preload)); 87 | $this->assertStringContainsString("if (in_array(PHP_SAPI, ['phpdbg'], true))", $preload); 88 | } 89 | 90 | public function test_with_name(): void 91 | { 92 | $path = ROOT . DS . 'a-unique-name.php'; 93 | 94 | $this->exec('preloader --name="' . $path . '"'); 95 | $this->assertExitSuccess(); 96 | $this->assertFileExists($path); 97 | } 98 | 99 | public function test_with_app(): void 100 | { 101 | $this->exec('preloader --app --phpunit'); 102 | $this->assertExitSuccess(); 103 | /** @var string $preload */ 104 | $preload = file_get_contents(ROOT . DS . 'preload.php'); 105 | $this->assertStringContainsString('test_app/src/Application.php', $preload); 106 | } 107 | 108 | public function test_with_one_plugin(): void 109 | { 110 | $this->exec('preloader --plugins=MyPluginOneZz --phpunit'); 111 | /** @var string $preload */ 112 | $preload = file_get_contents(ROOT . DS . 'preload.php'); 113 | $this->assertStringContainsString('plugins/MyPluginOneZz/src/Plugin.php', $preload); 114 | } 115 | 116 | public function test_with_multiple_plugins(): void 117 | { 118 | $this->exec('preloader --plugins=MyPluginOneZz,MyPluginTwoZz --phpunit'); 119 | /** @var string $preload */ 120 | $preload = file_get_contents(ROOT . DS . 'preload.php'); 121 | $this->assertStringContainsString('plugins/MyPluginOneZz/src/Plugin.php', $preload); 122 | $this->assertStringContainsString('plugins/MyPluginTwoZz/src/Plugin.php', $preload); 123 | } 124 | 125 | public function test_with_plugins_wildcard(): void 126 | { 127 | $this->exec('preloader --plugins=* --phpunit'); 128 | /** @var string $preload */ 129 | $preload = file_get_contents(ROOT . DS . 'preload.php'); 130 | $this->assertStringContainsString('plugins/MyPluginOneZz/src/Plugin.php', $preload); 131 | $this->assertStringContainsString('plugins/MyPluginTwoZz/src/Plugin.php', $preload); 132 | } 133 | 134 | public function test_with_one_package(): void 135 | { 136 | $this->exec('preloader --packages=vendorone/packageone --phpunit'); 137 | $this->assertExitSuccess(); 138 | 139 | /** @var string $preload */ 140 | $preload = file_get_contents(ROOT . DS . 'preload.php'); 141 | $this->assertStringContainsString('vendorone/packageone/src/VendorOnePackageOneTestClassZz.php', $preload); 142 | } 143 | 144 | public function test_with_multiple_packages(): void 145 | { 146 | $this->exec('preloader --packages=vendorone/packageone,vendortwo/packagetwo --phpunit'); 147 | $this->assertExitSuccess(); 148 | 149 | /** @var string $preload */ 150 | $preload = file_get_contents(ROOT . DS . 'preload.php'); 151 | $this->assertStringContainsString('vendorone/packageone/src/VendorOnePackageOneTestClassZz.php', $preload); 152 | $this->assertStringContainsString('vendortwo/packagetwo/src/VendorTwoPackageTwoTestClassZz.php', $preload); 153 | } 154 | 155 | public function test_before_write_event(): void 156 | { 157 | $eventManager = EventManager::instance()->setEventList(new EventList()); 158 | 159 | $eventManager->on('CakePreloader.beforeWrite', function (Event $event) { 160 | /** @var Preloader $preloader */ 161 | $preloader = $event->getSubject(); 162 | $this->assertInstanceOf(Preloader::class, $preloader); 163 | $preloader->setPreloadResources([ 164 | (new PreloadResource('require_once', __FILE__)), 165 | ]); 166 | }); 167 | 168 | $this->exec('preloader'); 169 | $this->assertEventFired('CakePreloader.beforeWrite'); 170 | 171 | /** @var string $preload */ 172 | $preload = file_get_contents(ROOT . DS . 'preload.php'); 173 | $this->assertStringContainsString(__FILE__, $preload); 174 | } 175 | 176 | public function test_with_config(): void 177 | { 178 | Configure::load('preloader_config_test', 'default'); 179 | 180 | $path = ROOT . DS . 'a-unique-name.php'; 181 | 182 | $this->exec('preloader --phpunit'); 183 | $this->assertExitSuccess(); 184 | $this->assertFileExists($path); 185 | 186 | /** @var string $preload */ 187 | $preload = file_get_contents($path); 188 | 189 | // app 190 | $this->assertStringContainsString('test_app/src/Application.php', $preload); 191 | 192 | // plugins 193 | $this->assertStringContainsString('plugins/MyPluginOneZz/src/Plugin.php', $preload); 194 | $this->assertStringContainsString('plugins/MyPluginTwoZz/src/Plugin.php', $preload); 195 | 196 | // packages 197 | $this->assertStringContainsString('vendorone/packageone/src/VendorOnePackageOneTestClassZz.php', $preload); 198 | $this->assertStringContainsString('vendortwo/packagetwo/src/VendorTwoPackageTwoTestClassZz.php', $preload); 199 | } 200 | 201 | public function test_invalid_file(): void 202 | { 203 | $this->expectException(RuntimeException::class); 204 | $this->exec('preloader --name="/etc/passwd"'); 205 | } 206 | 207 | public function test_package_warning(): void 208 | { 209 | $this->exec('preloader --packages=a,b --phpunit'); 210 | $this->assertOutputContains('One or more packages not found'); 211 | } 212 | 213 | public function test_write_exception(): void 214 | { 215 | $this->mockService(PreloaderService::class, function () { 216 | $mock = $this->createPartialMock(Preloader::class, ['write']); 217 | $mock 218 | ->method('write') 219 | ->willThrowException(new PreloadWriteException()); 220 | 221 | return new PreloaderService($mock); 222 | }); 223 | 224 | $this->exec('preloader --phpunit'); 225 | $this->assertExitError(); 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /tests/TestCase/PluginTest.php: -------------------------------------------------------------------------------- 1 | bootstrap(); 23 | $this->assertTrue(in_array('CakePreloader', Plugin::loaded())); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/TestCase/PreloadResourceTest.php: -------------------------------------------------------------------------------- 1 | expectException(\InvalidArgumentException::class); 14 | new PreloadResource('nope', '/tmp/test.txt'); 15 | } 16 | 17 | public function test_file_does_not_exist() 18 | { 19 | $this->expectException(ResourceNotFoundException::class); 20 | (new PreloadResource('require_once', '/tmp/test.txt'))->getResource(); 21 | } 22 | 23 | public function test_setPath() 24 | { 25 | $resource = new PreloadResource('require_once', '/tmp/test.txt'); 26 | $resource->setPath('/tmp/new_test.txt'); 27 | $this->assertEquals('/tmp/new_test.txt', $resource->getFile()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 'CakePreloader\Test\App', 54 | 'encoding' => 'UTF-8', 55 | 'base' => false, 56 | 'baseUrl' => false, 57 | 'dir' => APP_DIR, 58 | 'webroot' => 'webroot', 59 | 'wwwRoot' => WWW_ROOT, 60 | 'fullBaseUrl' => 'http://localhost', 61 | 'imageBaseUrl' => 'img/', 62 | 'jsBaseUrl' => 'js/', 63 | 'cssBaseUrl' => 'css/', 64 | 'paths' => [ 65 | 'plugins' => [TEST_APP . 'plugins' . DS], 66 | 'templates' => [TEST_APP . 'templates' . DS], 67 | 'locales' => [TEST_APP . 'resources' . DS . 'locales' . DS], 68 | ], 69 | ]); 70 | 71 | Cache::setConfig([ 72 | '_cake_core_' => [ 73 | 'engine' => 'File', 74 | 'prefix' => 'cake_core_', 75 | 'serialize' => true, 76 | ], 77 | '_cake_model_' => [ 78 | 'engine' => 'File', 79 | 'prefix' => 'cake_model_', 80 | 'serialize' => true, 81 | ], 82 | ]); 83 | 84 | // Ensure default test connection is defined 85 | if (!getenv('DB_DSN')) { 86 | putenv('DB_DSN=sqlite:///:memory:'); 87 | } 88 | 89 | ConnectionManager::setConfig('test', ['url' => getenv('DB_DSN')]); 90 | ConnectionManager::setConfig('test_custom_i18n_datasource', ['url' => getenv('DB_DSN')]); 91 | 92 | Configure::write('Session', [ 93 | 'defaults' => 'php', 94 | ]); 95 | 96 | Log::setConfig([ 97 | // 'queries' => [ 98 | // 'className' => 'Console', 99 | // 'stream' => 'php://stderr', 100 | // 'scopes' => ['queriesLog'] 101 | // ], 102 | 'debug' => [ 103 | 'engine' => 'Cake\Log\Engine\FileLog', 104 | 'levels' => ['notice', 'info', 'debug'], 105 | 'file' => 'debug', 106 | 'path' => LOGS, 107 | ], 108 | 'error' => [ 109 | 'engine' => 'Cake\Log\Engine\FileLog', 110 | 'levels' => ['warning', 'error', 'critical', 'alert', 'emergency'], 111 | 'file' => 'error', 112 | 'path' => LOGS, 113 | ], 114 | ]); 115 | 116 | Chronos::setTestNow(Chronos::now()); 117 | Security::setSalt('a-long-but-not-random-value'); 118 | 119 | ini_set('intl.default_locale', 'en_US'); 120 | ini_set('session.gc_divisor', '1'); 121 | 122 | /** 123 | * Define fallback values for required constants and configuration. 124 | * To customize constants and configuration remove this require 125 | * and define the data required by your plugin here. 126 | 127 | require_once $root . '/vendor/cakephp/cakephp/tests/bootstrap.php'; 128 | 129 | if (file_exists($root . '/config/bootstrap.php')) { 130 | require $root . '/config/bootstrap.php'; 131 | 132 | return; 133 | } 134 | 135 | */ 136 | -------------------------------------------------------------------------------- /tests/php.ini: -------------------------------------------------------------------------------- 1 | ; 2 | ; Development 3 | ; 4 | ;extension=intl.so 5 | ;extension=pdo_mysql.so 6 | ;extension=sodium 7 | ;extension=zip.so 8 | ;zend_extension=opcache.so 9 | 10 | [php] 11 | session.auto_start = Off 12 | short_open_tag = Off 13 | opcache.interned_strings_buffer = 16 14 | opcache.max_accelerated_files = 20000 15 | opcache.memory_consumption = 256 16 | opcache.enable_cli = 0 17 | opcache.enable = 1 18 | ; set higher on production, see https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.revalidate-freq 19 | opcache.revalidate_freq = 360 20 | opcache.enable_file_override = 1 21 | opcache.max_file_size = 1000000 22 | opcache.preload_user=root 23 | opcache.preload=/srv/app/preload.php 24 | realpath_cache_size = 4096K 25 | realpath_cache_ttl = 600 26 | expose_php = off 27 | -------------------------------------------------------------------------------- /tests/plugins.php: -------------------------------------------------------------------------------- 1 | [], 6 | ]; 7 | -------------------------------------------------------------------------------- /tests/test_app/config/preloader_config.php: -------------------------------------------------------------------------------- 1 | [ 5 | 6 | ] 7 | ]; 8 | -------------------------------------------------------------------------------- /tests/test_app/config/preloader_config_test.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'name' => ROOT . DS . 'a-unique-name.php', 6 | 'app' => true, 7 | 'packages' => ['vendorone/packageone','vendortwo/packagetwo'], 8 | 'plugins' => ['MyPluginOneZz','MyPluginTwoZz'], 9 | ] 10 | ]; 11 | -------------------------------------------------------------------------------- /tests/test_app/config/routes.php: -------------------------------------------------------------------------------- 1 | addPlugin('CakePreloader'); 13 | } 14 | 15 | public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue 16 | { 17 | return $middlewareQueue; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/test_app/vendor/vendorone/packageone/src/VendorOnePackageOneTestClassZz.php: -------------------------------------------------------------------------------- 1 |