├── tests
├── test_app
│ ├── config
│ │ ├── routes.php
│ │ ├── preloader_config.php
│ │ └── preloader_config_test.php
│ ├── vendor
│ │ ├── vendorone
│ │ │ └── packageone
│ │ │ │ └── src
│ │ │ │ └── VendorOnePackageOneTestClassZz.php
│ │ └── vendortwo
│ │ │ └── packagetwo
│ │ │ └── src
│ │ │ └── VendorTwoPackageTwoTestClassZz.php
│ ├── src
│ │ └── Application.php
│ └── plugins
│ │ ├── MyPluginOneZz
│ │ └── src
│ │ │ └── Plugin.php
│ │ └── MyPluginTwoZz
│ │ └── src
│ │ └── Plugin.php
├── plugins.php
├── Dockerfile
├── TestCase
│ ├── PluginTest.php
│ ├── PreloadResourceTest.php
│ └── Command
│ │ └── PreloaderCommandTest.php
├── php.ini
└── bootstrap.php
├── .coveralls.yml
├── phpcs.xml
├── phpstan.neon
├── src
├── Exception
│ ├── PreloadWriteException.php
│ └── ResourceNotFoundException.php
├── Plugin.php
├── PreloadResource.php
├── Command
│ └── PreloaderCommand.php
├── Filesystem.php
├── PreloaderService.php
└── Preloader.php
├── .github
├── ISSUE_TEMPLATE
│ ├── ISSUE_TEMPLATE.md
│ └── PULL_REQUEST_TEMPLATE.md
└── workflows
│ ├── merge.yml
│ └── ci.yml
├── phpunit.xml.dist
├── .gitignore
├── LICENSE.md
├── composer.json
├── assets
└── preloader_config.php
├── phpmd.xml
└── README.md
/tests/test_app/config/routes.php:
--------------------------------------------------------------------------------
1 | [],
6 | ];
7 |
--------------------------------------------------------------------------------
/tests/test_app/config/preloader_config.php:
--------------------------------------------------------------------------------
1 | [
5 |
6 | ]
7 | ];
8 |
--------------------------------------------------------------------------------
/phpcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/tests/test_app/vendor/vendorone/packageone/src/VendorOnePackageOneTestClassZz.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/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/test_app/src/Application.php:
--------------------------------------------------------------------------------
1 | addPlugin('CakePreloader');
13 | }
14 |
15 | public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
16 | {
17 | return $middlewareQueue;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/tests/test_app/plugins/MyPluginOneZz/src/Plugin.php:
--------------------------------------------------------------------------------
1 | bootstrap();
23 | $this->assertTrue(in_array('CakePreloader', Plugin::loaded()));
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/Filesystem.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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CakePHP Preloader
2 |
3 | [](https://packagist.org/packages/cnizzardini/cakephp-preloader)
4 | [](https://github.com/cnizzardini/cakephp-preloader/actions/workflows/merge.yml)
5 | [](https://coveralls.io/github/cnizzardini/cakephp-preloader?branch=main)
6 | [](LICENSE.md)
7 | [](https://book.cakephp.org/4/en/index.html)
8 | [](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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------