├── ChangeLog.md ├── LICENSE ├── README.md ├── SECURITY.md ├── composer.json ├── composer.lock ├── example ├── optimized.svg └── unoptimized.svg ├── foal └── src ├── Analyser.php ├── Renderer.php ├── TextRenderer.php ├── bytecode ├── Disassembler.php ├── VldDisassembler.php └── VldParser.php ├── cli ├── Application.php ├── Configuration.php ├── ConfigurationBuilder.php ├── Factory.php └── Version.php ├── exception ├── ConfigurationBuilderException.php ├── Exception.php ├── OpcacheNotLoadedException.php ├── PathsNotConfiguredException.php ├── ProcessException.php └── VldNotLoadedException.php └── file ├── File.php ├── FileCollection.php └── FileCollectionIterator.php /ChangeLog.md: -------------------------------------------------------------------------------- 1 | # ChangeLog 2 | 3 | All notable changes are documented in this file using the [Keep a CHANGELOG](https://keepachangelog.com/) principles. 4 | 5 | ## [0.4.0] - 2024-MM-DD 6 | 7 | ### Added 8 | 9 | * `--paths` option to export execution paths before/after bytecode optimization in DOT format 10 | 11 | ### Changed 12 | 13 | * The PHAR-specific CLI options `--manifest`, `--sbom`, and `--composer-lock` are now included in the help output 14 | 15 | ### Removed 16 | 17 | * `--diff` option to display optimized-away lines as diff 18 | 19 | ## [0.3.0] - 2024-03-24 20 | 21 | ### Added 22 | 23 | * Support for multiple arguments (directories and/or files) 24 | * `--diff` option to display optimized-away lines as diff 25 | 26 | ### Changed 27 | 28 | * [#3](https://github.com/sebastianbergmann/foal/issues/3): Refactor `Analyser` to operate on list of files 29 | 30 | ## [0.2.1] - 2024-03-24 31 | 32 | * No functional changes 33 | 34 | ## [0.2.0] - 2024-03-24 35 | 36 | ### Removed 37 | 38 | * This tool now requires PHP 8.3 39 | 40 | ## [0.1.0] - 2018-12-24 41 | 42 | * Initial release 43 | 44 | [0.4.0]: https://github.com/sebastianbergmann/foal/compare/0.3.0...main 45 | [0.3.0]: https://github.com/sebastianbergmann/foal/compare/0.2.1...0.3.0 46 | [0.2.1]: https://github.com/sebastianbergmann/foal/compare/0.2.0...0.2.1 47 | [0.2.0]: https://github.com/sebastianbergmann/foal/compare/0.1.0...0.2.0 48 | [0.1.0]: https://github.com/sebastianbergmann/foal/compare/820e0c5e988a5f8bf09f38211174bd481d8e5dd9...0.1.0 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2018-2024, Sebastian Bergmann 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![No Maintenance Intended](https://unmaintained.tech/badge.svg)](https://unmaintained.tech/) 2 | [![CI Status](https://github.com/sebastianbergmann/foal/workflows/CI/badge.svg)](https://github.com/sebastianbergmann/foal/actions) 3 | [![codecov](https://codecov.io/gh/sebastianbergmann/foal/branch/main/graph/badge.svg)](https://codecov.io/gh/sebastianbergmann/foal) 4 | 5 | # Find Optimized-Away Lines (FOAL) 6 | 7 | `foal` finds lines of code that are eliminated by OpCache's bytecode optimizer. 8 | 9 | ## Installation 10 | 11 | The recommended way to use this tool is a [PHP Archive (PHAR)](https://php.net/phar): 12 | 13 | ```bash 14 | $ wget https://phar.phpunit.de/foal.phar 15 | 16 | $ php foal.phar --version 17 | ``` 18 | 19 | Furthermore, it is recommended to use [Phive](https://phar.io/) for installing and updating the tool dependencies of your project. 20 | 21 | ## Usage 22 | 23 | **`example.php`** 24 | ```php 25 | =8.2" 25 | }, 26 | "require-dev": { 27 | "phpunit/phpunit": "^11.0" 28 | }, 29 | "type": "library", 30 | "extra": { 31 | "branch-alias": { 32 | "dev-main": "5.0-dev" 33 | } 34 | }, 35 | "autoload": { 36 | "classmap": [ 37 | "src/" 38 | ] 39 | }, 40 | "notification-url": "https://packagist.org/downloads/", 41 | "license": [ 42 | "BSD-3-Clause" 43 | ], 44 | "authors": [ 45 | { 46 | "name": "Sebastian Bergmann", 47 | "email": "sebastian@phpunit.de", 48 | "role": "lead" 49 | } 50 | ], 51 | "description": "FilterIterator implementation that filters files based on a list of suffixes.", 52 | "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", 53 | "keywords": [ 54 | "filesystem", 55 | "iterator" 56 | ], 57 | "support": { 58 | "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", 59 | "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", 60 | "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.0.0" 61 | }, 62 | "funding": [ 63 | { 64 | "url": "https://github.com/sebastianbergmann", 65 | "type": "github" 66 | } 67 | ], 68 | "time": "2024-02-02T06:05:04+00:00" 69 | }, 70 | { 71 | "name": "sebastian/cli-parser", 72 | "version": "3.0.1", 73 | "source": { 74 | "type": "git", 75 | "url": "https://github.com/sebastianbergmann/cli-parser.git", 76 | "reference": "00a74d5568694711f0222e54fb281e1d15fdf04a" 77 | }, 78 | "dist": { 79 | "type": "zip", 80 | "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/00a74d5568694711f0222e54fb281e1d15fdf04a", 81 | "reference": "00a74d5568694711f0222e54fb281e1d15fdf04a", 82 | "shasum": "" 83 | }, 84 | "require": { 85 | "php": ">=8.2" 86 | }, 87 | "require-dev": { 88 | "phpunit/phpunit": "^11.0" 89 | }, 90 | "type": "library", 91 | "extra": { 92 | "branch-alias": { 93 | "dev-main": "3.0-dev" 94 | } 95 | }, 96 | "autoload": { 97 | "classmap": [ 98 | "src/" 99 | ] 100 | }, 101 | "notification-url": "https://packagist.org/downloads/", 102 | "license": [ 103 | "BSD-3-Clause" 104 | ], 105 | "authors": [ 106 | { 107 | "name": "Sebastian Bergmann", 108 | "email": "sebastian@phpunit.de", 109 | "role": "lead" 110 | } 111 | ], 112 | "description": "Library for parsing CLI options", 113 | "homepage": "https://github.com/sebastianbergmann/cli-parser", 114 | "support": { 115 | "issues": "https://github.com/sebastianbergmann/cli-parser/issues", 116 | "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", 117 | "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.1" 118 | }, 119 | "funding": [ 120 | { 121 | "url": "https://github.com/sebastianbergmann", 122 | "type": "github" 123 | } 124 | ], 125 | "time": "2024-03-02T07:26:58+00:00" 126 | }, 127 | { 128 | "name": "sebastian/version", 129 | "version": "5.0.0", 130 | "source": { 131 | "type": "git", 132 | "url": "https://github.com/sebastianbergmann/version.git", 133 | "reference": "13999475d2cb1ab33cb73403ba356a814fdbb001" 134 | }, 135 | "dist": { 136 | "type": "zip", 137 | "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/13999475d2cb1ab33cb73403ba356a814fdbb001", 138 | "reference": "13999475d2cb1ab33cb73403ba356a814fdbb001", 139 | "shasum": "" 140 | }, 141 | "require": { 142 | "php": ">=8.2" 143 | }, 144 | "type": "library", 145 | "extra": { 146 | "branch-alias": { 147 | "dev-main": "5.0-dev" 148 | } 149 | }, 150 | "autoload": { 151 | "classmap": [ 152 | "src/" 153 | ] 154 | }, 155 | "notification-url": "https://packagist.org/downloads/", 156 | "license": [ 157 | "BSD-3-Clause" 158 | ], 159 | "authors": [ 160 | { 161 | "name": "Sebastian Bergmann", 162 | "email": "sebastian@phpunit.de", 163 | "role": "lead" 164 | } 165 | ], 166 | "description": "Library that helps with managing the version number of Git-hosted PHP projects", 167 | "homepage": "https://github.com/sebastianbergmann/version", 168 | "support": { 169 | "issues": "https://github.com/sebastianbergmann/version/issues", 170 | "security": "https://github.com/sebastianbergmann/version/security/policy", 171 | "source": "https://github.com/sebastianbergmann/version/tree/5.0.0" 172 | }, 173 | "funding": [ 174 | { 175 | "url": "https://github.com/sebastianbergmann", 176 | "type": "github" 177 | } 178 | ], 179 | "time": "2024-02-02T06:10:47+00:00" 180 | } 181 | ], 182 | "packages-dev": [], 183 | "aliases": [], 184 | "minimum-stability": "stable", 185 | "stability-flags": [], 186 | "prefer-stable": true, 187 | "prefer-lowest": false, 188 | "platform": { 189 | "php": "^8.3", 190 | "ext-zend-opcache": "*", 191 | "ext-vld": "*" 192 | }, 193 | "platform-dev": [], 194 | "platform-overrides": { 195 | "php": "8.3.0" 196 | }, 197 | "plugin-api-version": "2.6.0" 198 | } 199 | -------------------------------------------------------------------------------- /example/optimized.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | 12 | cluster_file_0x7f4dc8697000 13 | 14 | file example/source.php 15 | 16 | 17 | cluster_0x7f4dc8697000 18 | 19 | __main 20 | 21 | 22 | cluster_0x55d2098be6a0 23 | 24 | f 25 | 26 | 27 | 28 | __main_0 29 | 30 | op #0-0 31 | 32 | line 8-8 33 | 34 | 35 | 36 | __main_EXIT 37 | 38 | __main_EXIT 39 | 40 | 41 | 42 | __main_0->__main_EXIT 43 | 44 | 45 | 46 | 47 | 48 | __main_ENTRY 49 | 50 | __main_ENTRY 51 | 52 | 53 | 54 | __main_ENTRY->__main_0 55 | 56 | 57 | 58 | 59 | 60 | f_0 61 | 62 | op #0-0 63 | 64 | line 6-6 65 | 66 | 67 | 68 | f_EXIT 69 | 70 | f_EXIT 71 | 72 | 73 | 74 | f_0->f_EXIT 75 | 76 | 77 | 78 | 79 | 80 | f_ENTRY 81 | 82 | f_ENTRY 83 | 84 | 85 | 86 | f_ENTRY->f_0 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /example/unoptimized.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | 12 | cluster_file_0x7f478eaa1000 13 | 14 | file example/source.php 15 | 16 | 17 | cluster_0x7f478eaa1000 18 | 19 | __main 20 | 21 | 22 | cluster_0x7f478ea05018 23 | 24 | f 25 | 26 | 27 | 28 | __main_0 29 | 30 | op #0-0 31 | 32 | line 8-8 33 | 34 | 35 | 36 | __main_EXIT 37 | 38 | __main_EXIT 39 | 40 | 41 | 42 | __main_0->__main_EXIT 43 | 44 | 45 | 46 | 47 | 48 | __main_ENTRY 49 | 50 | __main_ENTRY 51 | 52 | 53 | 54 | __main_ENTRY->__main_0 55 | 56 | 57 | 58 | 59 | 60 | f_0 61 | 62 | op #0-2 63 | 64 | line 4-7 65 | 66 | 67 | 68 | f_ENTRY 69 | 70 | f_ENTRY 71 | 72 | 73 | 74 | f_ENTRY->f_0 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /foal: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | ')) { 21 | fwrite( 22 | STDERR, 23 | sprintf( 24 | 'foal X.Y.Z by Sebastian Bergmann.' . PHP_EOL . PHP_EOL . 25 | 'This version of FOAL requires PHP >= 8.3.' . PHP_EOL . 26 | 'You are using PHP %s (%s).' . PHP_EOL, 27 | PHP_VERSION, 28 | PHP_BINARY 29 | ) 30 | ); 31 | 32 | die(1); 33 | } 34 | 35 | if (isset($GLOBALS['_composer_autoload_path'])) { 36 | define('FOAL_COMPOSER_INSTALL', $GLOBALS['_composer_autoload_path']); 37 | 38 | unset($GLOBALS['_composer_autoload_path']); 39 | } else { 40 | foreach (array(__DIR__ . '/../../autoload.php', __DIR__ . '/../vendor/autoload.php', __DIR__ . '/vendor/autoload.php') as $file) { 41 | if (file_exists($file)) { 42 | define('FOAL_COMPOSER_INSTALL', $file); 43 | 44 | break; 45 | } 46 | } 47 | 48 | unset($file); 49 | } 50 | 51 | if (!defined('FOAL_COMPOSER_INSTALL')) { 52 | fwrite( 53 | STDERR, 54 | 'You need to set up the project dependencies using Composer:' . PHP_EOL . PHP_EOL . 55 | ' composer install' . PHP_EOL . PHP_EOL . 56 | 'You can learn all about Composer on https://getcomposer.org/.' . PHP_EOL 57 | ); 58 | 59 | die(1); 60 | } 61 | 62 | require FOAL_COMPOSER_INSTALL; 63 | 64 | exit((new Factory)->createApplication()->run($_SERVER['argv'])); 65 | -------------------------------------------------------------------------------- /src/Analyser.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace SebastianBergmann\FOAL; 11 | 12 | use function array_diff; 13 | use function array_values; 14 | use function assert; 15 | use function file; 16 | 17 | /** 18 | * @internal This class is not covered by the backward compatibility promise for FOAL 19 | */ 20 | final readonly class Analyser 21 | { 22 | private Disassembler $disassembler; 23 | 24 | public function __construct(Disassembler $disassembler) 25 | { 26 | $this->disassembler = $disassembler; 27 | } 28 | 29 | /** 30 | * @param non-empty-list $files 31 | */ 32 | public function analyse(array $files): FileCollection 33 | { 34 | $result = []; 35 | 36 | foreach ($files as $file) { 37 | $sourceLines = file($file); 38 | 39 | assert($sourceLines !== false); 40 | 41 | $result[] = new File( 42 | $file, 43 | $sourceLines, 44 | $this->linesEliminatedByOptimizer($file), 45 | ); 46 | } 47 | 48 | return FileCollection::from(...$result); 49 | } 50 | 51 | /** 52 | * @param non-empty-string $file 53 | * 54 | * @return list 55 | */ 56 | private function linesEliminatedByOptimizer(string $file): array 57 | { 58 | return array_values( 59 | array_diff( 60 | $this->disassembler->linesWithOpcodesBeforeOptimization($file), 61 | $this->disassembler->linesWithOpcodesAfterOptimization($file), 62 | ), 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Renderer.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace SebastianBergmann\FOAL; 11 | 12 | /** 13 | * @internal This interface is not covered by the backward compatibility promise for FOAL 14 | */ 15 | interface Renderer 16 | { 17 | public function render(File $file): string; 18 | } 19 | -------------------------------------------------------------------------------- /src/TextRenderer.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace SebastianBergmann\FOAL; 11 | 12 | use function array_flip; 13 | use function array_key_exists; 14 | use function rtrim; 15 | use function sprintf; 16 | 17 | /** 18 | * @internal This class is not covered by the backward compatibility promise for FOAL 19 | */ 20 | final readonly class TextRenderer implements Renderer 21 | { 22 | public function render(File $file): string 23 | { 24 | $buffer = $file->path() . PHP_EOL; 25 | $sourceLines = $file->sourceLines(); 26 | $eliminatedLines = array_flip($file->linesEliminatedByOptimizer()); 27 | $line = 0; 28 | 29 | foreach ($sourceLines as $sourceLine) { 30 | $line++; 31 | 32 | $buffer .= sprintf( 33 | '%s %-6d %s' . PHP_EOL, 34 | array_key_exists($line, $eliminatedLines) ? '-' : ' ', 35 | $line, 36 | rtrim($sourceLine), 37 | ); 38 | } 39 | 40 | return $buffer; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/bytecode/Disassembler.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace SebastianBergmann\FOAL; 11 | 12 | /** 13 | * @internal This interface is not covered by the backward compatibility promise for FOAL 14 | */ 15 | interface Disassembler 16 | { 17 | /** 18 | * @param non-empty-string $file 19 | * 20 | * @return list 21 | */ 22 | public function linesWithOpcodesBeforeOptimization(string $file): array; 23 | 24 | /** 25 | * @param non-empty-string $file 26 | * 27 | * @return list 28 | */ 29 | public function linesWithOpcodesAfterOptimization(string $file): array; 30 | 31 | /** 32 | * @param non-empty-string $file 33 | * 34 | * @return non-empty-string 35 | */ 36 | public function pathsBeforeOptimization(string $file): string; 37 | 38 | /** 39 | * @param non-empty-string $file 40 | * 41 | * @return non-empty-string 42 | */ 43 | public function pathsAfterOptimization(string $file): string; 44 | } 45 | -------------------------------------------------------------------------------- /src/bytecode/VldDisassembler.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace SebastianBergmann\FOAL; 11 | 12 | use function assert; 13 | use function exec; 14 | use function extension_loaded; 15 | use function file_get_contents; 16 | use function implode; 17 | use function is_string; 18 | 19 | /** 20 | * @internal This class is not covered by the backward compatibility promise for FOAL 21 | */ 22 | final readonly class VldDisassembler implements Disassembler 23 | { 24 | private const string VLD_OPTIONS_COMMON = '-d vld.active=1 -d vld.execute=0 -d vld.verbosity=0'; 25 | private const string VLD_OPTIONS_BYTECODE = self::VLD_OPTIONS_COMMON . ' -d vld.format=1 -d vld.col_sep=\';\''; 26 | private const string VLD_OPTIONS_PATHS = self::VLD_OPTIONS_COMMON . ' -d vld.save_paths=1 -d vld.save_dir=/tmp'; 27 | private const string OPCACHE_OPTIONS_ENABLE = '-d opcache.enable=1 -d opcache.enable_cli=1 -d opcache.optimization_level=-1'; 28 | private const string OPCACHE_OPTIONS_DISABLE = '-d opcache.enable=0 -d opcache.enable_cli=0'; 29 | private VldParser $parser; 30 | 31 | /** 32 | * @throws OpcacheNotLoadedException 33 | * @throws VldNotLoadedException 34 | */ 35 | public function __construct(VldParser $parser) 36 | { 37 | $this->ensureOpCacheIsAvailable(); 38 | $this->ensureVldIsAvailable(); 39 | 40 | $this->parser = $parser; 41 | } 42 | 43 | /** 44 | * @param non-empty-string $file 45 | * 46 | * @return list 47 | */ 48 | public function linesWithOpcodesBeforeOptimization(string $file): array 49 | { 50 | return $this->parser->linesWithOpcodes( 51 | $this->execute( 52 | PHP_BINARY . ' ' . self::OPCACHE_OPTIONS_DISABLE . ' ' . self::VLD_OPTIONS_BYTECODE . ' ' . $file, 53 | ), 54 | ); 55 | } 56 | 57 | /** 58 | * @param non-empty-string $file 59 | * 60 | * @return list 61 | */ 62 | public function linesWithOpcodesAfterOptimization(string $file): array 63 | { 64 | return $this->parser->linesWithOpcodes( 65 | $this->execute( 66 | PHP_BINARY . ' ' . self::OPCACHE_OPTIONS_ENABLE . ' ' . self::VLD_OPTIONS_BYTECODE . ' ' . $file, 67 | ), 68 | ); 69 | } 70 | 71 | /** 72 | * @param non-empty-string $file 73 | * 74 | * @return non-empty-string 75 | */ 76 | public function pathsBeforeOptimization(string $file): string 77 | { 78 | $this->execute( 79 | PHP_BINARY . ' ' . self::OPCACHE_OPTIONS_DISABLE . ' ' . self::VLD_OPTIONS_PATHS . ' ' . $file, 80 | ); 81 | 82 | $buffer = file_get_contents('/tmp/paths.dot'); 83 | 84 | assert(is_string($buffer) && $buffer !== ''); 85 | 86 | return $buffer; 87 | } 88 | 89 | /** 90 | * @param non-empty-string $file 91 | * 92 | * @return non-empty-string 93 | */ 94 | public function pathsAfterOptimization(string $file): string 95 | { 96 | $this->execute( 97 | PHP_BINARY . ' ' . self::OPCACHE_OPTIONS_ENABLE . ' ' . self::VLD_OPTIONS_PATHS . ' ' . $file, 98 | ); 99 | 100 | $buffer = file_get_contents('/tmp/paths.dot'); 101 | 102 | assert(is_string($buffer) && $buffer !== ''); 103 | 104 | return $buffer; 105 | } 106 | 107 | /** 108 | * @throws OpcacheNotLoadedException 109 | */ 110 | private function ensureOpCacheIsAvailable(): void 111 | { 112 | if (!extension_loaded('Zend OPcache')) { 113 | // @codeCoverageIgnoreStart 114 | throw new OpcacheNotLoadedException; 115 | // @codeCoverageIgnoreEnd 116 | } 117 | } 118 | 119 | /** 120 | * @throws VldNotLoadedException 121 | */ 122 | private function ensureVldIsAvailable(): void 123 | { 124 | if (!extension_loaded('vld')) { 125 | // @codeCoverageIgnoreStart 126 | throw new VldNotLoadedException; 127 | // @codeCoverageIgnoreEnd 128 | } 129 | } 130 | 131 | /** 132 | * @return list 133 | */ 134 | private function execute(string $command): array 135 | { 136 | exec($command . ' 2>&1', $output, $returnValue); 137 | 138 | if ($returnValue !== 0) { 139 | // @codeCoverageIgnoreStart 140 | throw new ProcessException(implode("\r\n", $output)); 141 | // @codeCoverageIgnoreEnd 142 | } 143 | 144 | return $output; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/bytecode/VldParser.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace SebastianBergmann\FOAL; 11 | 12 | use function array_unique; 13 | use function explode; 14 | use function sort; 15 | use function str_starts_with; 16 | use function trim; 17 | 18 | /** 19 | * @internal This class is not covered by the backward compatibility promise for FOAL 20 | */ 21 | final readonly class VldParser 22 | { 23 | /** 24 | * @param list $lines 25 | * 26 | * @return list 27 | */ 28 | public function linesWithOpcodes(array $lines): array 29 | { 30 | $linesWithOpcodes = []; 31 | $opArray = false; 32 | 33 | foreach ($lines as $line) { 34 | if (str_starts_with($line, ';line')) { 35 | $opArray = true; 36 | 37 | continue; 38 | } 39 | 40 | if (trim($line) === ';') { 41 | $opArray = false; 42 | } 43 | 44 | if (!$opArray) { 45 | continue; 46 | } 47 | 48 | $linesWithOpcodes[] = (int) explode(';', $line)[1]; 49 | } 50 | 51 | $linesWithOpcodes = array_unique($linesWithOpcodes); 52 | 53 | sort($linesWithOpcodes); 54 | 55 | return $linesWithOpcodes; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/cli/Application.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace SebastianBergmann\FOAL\CLI; 11 | 12 | use const PHP_EOL; 13 | use function array_merge; 14 | use function array_unique; 15 | use function array_values; 16 | use function assert; 17 | use function count; 18 | use function defined; 19 | use function file_put_contents; 20 | use function is_dir; 21 | use function is_file; 22 | use function printf; 23 | use function realpath; 24 | use SebastianBergmann\FileIterator\Facade; 25 | use SebastianBergmann\FOAL\Analyser; 26 | use SebastianBergmann\FOAL\Disassembler; 27 | use SebastianBergmann\FOAL\TextRenderer; 28 | 29 | /** 30 | * @internal This class is not covered by the backward compatibility promise for FOAL 31 | */ 32 | final readonly class Application 33 | { 34 | private Analyser $analyser; 35 | private Disassembler $disassembler; 36 | 37 | public function __construct(Analyser $analyser, Disassembler $disassembler) 38 | { 39 | $this->analyser = $analyser; 40 | $this->disassembler = $disassembler; 41 | } 42 | 43 | /** 44 | * @param list $argv 45 | */ 46 | public function run(array $argv): int 47 | { 48 | $this->printVersion(); 49 | 50 | try { 51 | $configuration = (new ConfigurationBuilder)->build($argv); 52 | // @codeCoverageIgnoreStart 53 | } catch (ConfigurationBuilderException $e) { 54 | print PHP_EOL . $e->getMessage() . PHP_EOL; 55 | 56 | return 1; 57 | // @codeCoverageIgnoreEnd 58 | } 59 | 60 | if ($configuration->version()) { 61 | return 0; 62 | } 63 | 64 | print PHP_EOL; 65 | 66 | if ($configuration->help()) { 67 | $this->help(); 68 | 69 | return 0; 70 | } 71 | 72 | if ($configuration->arguments() === []) { 73 | $this->help(); 74 | 75 | return 1; 76 | } 77 | 78 | $files = []; 79 | 80 | foreach ($configuration->arguments() as $argument) { 81 | $candidate = realpath($argument); 82 | 83 | if ($candidate === false) { 84 | continue; 85 | } 86 | 87 | assert($candidate !== ''); 88 | 89 | if (is_file($candidate)) { 90 | $files[] = $candidate; 91 | 92 | continue; 93 | } 94 | 95 | if (is_dir($candidate)) { 96 | $files = array_merge($files, (new Facade)->getFilesAsArray($candidate, '.php')); 97 | } 98 | } 99 | 100 | if (empty($files)) { 101 | print 'No files found to analyse' . PHP_EOL; 102 | 103 | return 1; 104 | } 105 | 106 | $files = array_values(array_unique($files)); 107 | 108 | if ($configuration->hasPaths()) { 109 | return $this->handlePaths($files, $configuration->paths()); 110 | } 111 | 112 | return $this->handleAnalysis($files); 113 | } 114 | 115 | private function printVersion(): void 116 | { 117 | printf( 118 | 'foal %s by Sebastian Bergmann.' . PHP_EOL, 119 | Version::id(), 120 | ); 121 | } 122 | 123 | private function help(): void 124 | { 125 | print <<<'EOT' 126 | Usage: 127 | foal [options] ... 128 | 129 | --paths Write execution paths before/after bytecode optimization to files in DOT format 130 | 131 | -h|--help Prints this usage information and exits 132 | --version Prints the version and exits 133 | 134 | EOT; 135 | 136 | if (defined('__FOAL_PHAR__')) { 137 | print <<<'EOT' 138 | 139 | --manifest Prints Software Bill of Materials (SBOM) in plain-text format and exits 140 | --sbom Prints Software Bill of Materials (SBOM) in CycloneDX XML format and exits 141 | --composer-lock Prints the composer.lock file used to build the PHAR and exits 142 | 143 | EOT; 144 | } 145 | } 146 | 147 | /** 148 | * @param non-empty-list $files 149 | * @param non-empty-string $target 150 | */ 151 | private function handlePaths(array $files, string $target): int 152 | { 153 | if (count($files) !== 1) { 154 | print 'The --paths option only operates on a source single file' . PHP_EOL; 155 | 156 | return 1; 157 | } 158 | 159 | $unoptimizedFile = $target . '/unoptimized.dot'; 160 | 161 | file_put_contents($unoptimizedFile, $this->disassembler->pathsBeforeOptimization($files[0])); 162 | 163 | printf( 164 | 'Wrote execution paths for %s to %s' . PHP_EOL, 165 | $files[0], 166 | $unoptimizedFile, 167 | ); 168 | 169 | $optimizedFile = $target . '/optimized.dot'; 170 | 171 | file_put_contents($optimizedFile, $this->disassembler->pathsAfterOptimization($files[0])); 172 | 173 | printf( 174 | 'Wrote optimized execution paths for %s to %s' . PHP_EOL, 175 | $files[0], 176 | $optimizedFile, 177 | ); 178 | 179 | return 0; 180 | } 181 | 182 | /** 183 | * @param non-empty-list $files 184 | */ 185 | private function handleAnalysis(array $files): int 186 | { 187 | $files = $this->analyser->analyse($files); 188 | 189 | $renderer = new TextRenderer; 190 | $first = true; 191 | 192 | foreach ($files as $file) { 193 | if (!$first) { 194 | print PHP_EOL; 195 | } 196 | 197 | print $renderer->render($file); 198 | 199 | $first = false; 200 | } 201 | 202 | return 0; 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/cli/Configuration.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace SebastianBergmann\FOAL\CLI; 11 | 12 | /** 13 | * @internal This class is not covered by the backward compatibility promise for FOAL 14 | */ 15 | final readonly class Configuration 16 | { 17 | /** 18 | * @var list 19 | */ 20 | private array $arguments; 21 | 22 | /** 23 | * @var ?non-empty-string 24 | */ 25 | private ?string $paths; 26 | private bool $help; 27 | private bool $version; 28 | 29 | /** 30 | * @param list $arguments 31 | * @param ?non-empty-string $paths 32 | */ 33 | public function __construct(array $arguments, ?string $paths, bool $help, bool $version) 34 | { 35 | $this->arguments = $arguments; 36 | $this->help = $help; 37 | $this->paths = $paths; 38 | $this->version = $version; 39 | } 40 | 41 | /** 42 | * @return list 43 | */ 44 | public function arguments(): array 45 | { 46 | return $this->arguments; 47 | } 48 | 49 | /** 50 | * @phpstan-assert-if-true !null $this->paths 51 | */ 52 | public function hasPaths(): bool 53 | { 54 | return $this->paths !== null; 55 | } 56 | 57 | /** 58 | * @throws PathsNotConfiguredException 59 | * 60 | * @return non-empty-string 61 | */ 62 | public function paths(): string 63 | { 64 | if ($this->paths === null) { 65 | // @codeCoverageIgnoreStart 66 | throw new PathsNotConfiguredException; 67 | // @codeCoverageIgnoreEnd 68 | } 69 | 70 | return $this->paths; 71 | } 72 | 73 | public function help(): bool 74 | { 75 | return $this->help; 76 | } 77 | 78 | public function version(): bool 79 | { 80 | return $this->version; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/cli/ConfigurationBuilder.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace SebastianBergmann\FOAL\CLI; 11 | 12 | use function assert; 13 | use function is_array; 14 | use SebastianBergmann\CliParser\Exception as CliParserException; 15 | use SebastianBergmann\CliParser\Parser as CliParser; 16 | 17 | /** 18 | * @internal This class is not covered by the backward compatibility promise for FOAL 19 | */ 20 | final readonly class ConfigurationBuilder 21 | { 22 | /** 23 | * @param list $argv 24 | * 25 | * @throws ConfigurationBuilderException 26 | */ 27 | public function build(array $argv): Configuration 28 | { 29 | try { 30 | $options = (new CliParser)->parse( 31 | $argv, 32 | 'hv', 33 | [ 34 | 'paths=', 35 | 'help', 36 | 'version', 37 | ], 38 | ); 39 | // @codeCoverageIgnoreStart 40 | } catch (CliParserException $e) { 41 | throw new ConfigurationBuilderException( 42 | $e->getMessage(), 43 | $e->getCode(), 44 | $e, 45 | ); 46 | // @codeCoverageIgnoreEnd 47 | } 48 | 49 | $paths = null; 50 | $help = false; 51 | $version = false; 52 | 53 | foreach ($options[0] as $option) { 54 | assert(is_array($option)); 55 | 56 | switch ($option[0]) { 57 | case '--paths': 58 | $paths = (string) $option[1]; 59 | 60 | assert($paths !== ''); 61 | 62 | break; 63 | 64 | case 'h': 65 | case '--help': 66 | $help = true; 67 | 68 | break; 69 | 70 | case 'v': 71 | case '--version': 72 | $version = true; 73 | 74 | break; 75 | } 76 | } 77 | 78 | return new Configuration( 79 | $options[1], 80 | $paths, 81 | $help, 82 | $version, 83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/cli/Factory.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace SebastianBergmann\FOAL\CLI; 11 | 12 | use SebastianBergmann\FOAL\Analyser; 13 | use SebastianBergmann\FOAL\VldDisassembler; 14 | use SebastianBergmann\FOAL\VldParser; 15 | 16 | /** 17 | * @internal This class is not covered by the backward compatibility promise for FOAL 18 | */ 19 | final readonly class Factory 20 | { 21 | public function createApplication(): Application 22 | { 23 | return new Application( 24 | new Analyser($this->disassembler()), 25 | $this->disassembler(), 26 | ); 27 | } 28 | 29 | private function disassembler(): VldDisassembler 30 | { 31 | return new VldDisassembler( 32 | new VldParser, 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/cli/Version.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace SebastianBergmann\FOAL\CLI; 11 | 12 | use function assert; 13 | use function dirname; 14 | use SebastianBergmann\Version as VersionId; 15 | 16 | /** 17 | * @internal This class is not covered by the backward compatibility promise for FOAL 18 | */ 19 | final class Version 20 | { 21 | private static string $pharVersion = ''; 22 | private static string $version = ''; 23 | 24 | public static function id(): string 25 | { 26 | if (self::$pharVersion !== '') { 27 | // @codeCoverageIgnoreStart 28 | return self::$pharVersion; 29 | // @codeCoverageIgnoreEnd 30 | } 31 | 32 | if (self::$version === '') { 33 | $path = dirname(__DIR__, 2); 34 | 35 | assert($path !== ''); 36 | 37 | self::$version = (new VersionId('0.4', $path))->asString(); 38 | } 39 | 40 | return self::$version; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/exception/ConfigurationBuilderException.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace SebastianBergmann\FOAL\CLI; 11 | 12 | use RuntimeException; 13 | use SebastianBergmann\FOAL\Exception; 14 | 15 | /** 16 | * @internal This class is not covered by the backward compatibility promise for FOAL 17 | */ 18 | final class ConfigurationBuilderException extends RuntimeException implements Exception 19 | { 20 | } 21 | -------------------------------------------------------------------------------- /src/exception/Exception.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace SebastianBergmann\FOAL; 11 | 12 | /** 13 | * @internal This interface is not covered by the backward compatibility promise for FOAL 14 | */ 15 | interface Exception 16 | { 17 | } 18 | -------------------------------------------------------------------------------- /src/exception/OpcacheNotLoadedException.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace SebastianBergmann\FOAL; 11 | 12 | use RuntimeException; 13 | 14 | /** 15 | * @internal This class is not covered by the backward compatibility promise for FOAL 16 | */ 17 | final class OpcacheNotLoadedException extends RuntimeException implements Exception 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /src/exception/PathsNotConfiguredException.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace SebastianBergmann\FOAL\CLI; 11 | 12 | use RuntimeException; 13 | use SebastianBergmann\FOAL\Exception; 14 | 15 | /** 16 | * @internal This class is not covered by the backward compatibility promise for FOAL 17 | */ 18 | final class PathsNotConfiguredException extends RuntimeException implements Exception 19 | { 20 | } 21 | -------------------------------------------------------------------------------- /src/exception/ProcessException.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace SebastianBergmann\FOAL; 11 | 12 | use RuntimeException; 13 | 14 | /** 15 | * @internal This class is not covered by the backward compatibility promise for FOAL 16 | */ 17 | final class ProcessException extends RuntimeException implements Exception 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /src/exception/VldNotLoadedException.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace SebastianBergmann\FOAL; 11 | 12 | use RuntimeException; 13 | 14 | /** 15 | * @internal This class is not covered by the backward compatibility promise for FOAL 16 | */ 17 | final class VldNotLoadedException extends RuntimeException implements Exception 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /src/file/File.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace SebastianBergmann\FOAL; 11 | 12 | /** 13 | * @internal This class is not covered by the backward compatibility promise for FOAL 14 | */ 15 | final readonly class File 16 | { 17 | /** 18 | * @var non-empty-string 19 | */ 20 | private string $path; 21 | 22 | /** 23 | * @var list 24 | */ 25 | private array $sourceLines; 26 | 27 | /** 28 | * @var list 29 | */ 30 | private array $linesEliminatedByOptimizer; 31 | 32 | /** 33 | * @param non-empty-string $path 34 | * @param list $sourceLines 35 | * @param list $linesEliminatedByOptimizer 36 | */ 37 | public function __construct(string $path, array $sourceLines, array $linesEliminatedByOptimizer) 38 | { 39 | $this->path = $path; 40 | $this->sourceLines = $sourceLines; 41 | $this->linesEliminatedByOptimizer = $linesEliminatedByOptimizer; 42 | } 43 | 44 | /** 45 | * @return non-empty-string 46 | */ 47 | public function path(): string 48 | { 49 | return $this->path; 50 | } 51 | 52 | /** 53 | * @return list 54 | */ 55 | public function sourceLines(): array 56 | { 57 | return $this->sourceLines; 58 | } 59 | 60 | /** 61 | * @return list 62 | */ 63 | public function linesEliminatedByOptimizer(): array 64 | { 65 | return $this->linesEliminatedByOptimizer; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/file/FileCollection.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace SebastianBergmann\FOAL; 11 | 12 | use function array_values; 13 | use function count; 14 | use Countable; 15 | use IteratorAggregate; 16 | 17 | /** 18 | * @template-implements IteratorAggregate 19 | * 20 | * @immutable 21 | * 22 | * @internal This class is not covered by the backward compatibility promise for FOAL 23 | */ 24 | final readonly class FileCollection implements Countable, IteratorAggregate 25 | { 26 | /** 27 | * @var list 28 | */ 29 | private array $files; 30 | 31 | public static function from(File ...$files): self 32 | { 33 | return new self(array_values($files)); 34 | } 35 | 36 | /** 37 | * @param list $files 38 | */ 39 | private function __construct(array $files) 40 | { 41 | $this->files = $files; 42 | } 43 | 44 | /** 45 | * @return list 46 | */ 47 | public function asArray(): array 48 | { 49 | return $this->files; 50 | } 51 | 52 | public function getIterator(): FileCollectionIterator 53 | { 54 | return new FileCollectionIterator($this); 55 | } 56 | 57 | public function count(): int 58 | { 59 | return count($this->files); 60 | } 61 | 62 | public function isEmpty(): bool 63 | { 64 | return empty($this->files); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/file/FileCollectionIterator.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace SebastianBergmann\FOAL; 11 | 12 | use Iterator; 13 | 14 | /** 15 | * @template-implements Iterator 16 | * 17 | * @internal This class is not covered by the backward compatibility promise for FOAL 18 | */ 19 | final class FileCollectionIterator implements Iterator 20 | { 21 | /** 22 | * @var list 23 | */ 24 | private readonly array $files; 25 | private int $position = 0; 26 | 27 | public function __construct(FileCollection $collection) 28 | { 29 | $this->files = $collection->asArray(); 30 | } 31 | 32 | public function rewind(): void 33 | { 34 | $this->position = 0; 35 | } 36 | 37 | public function valid(): bool 38 | { 39 | return isset($this->files[$this->position]); 40 | } 41 | 42 | public function key(): int 43 | { 44 | return $this->position; 45 | } 46 | 47 | public function current(): File 48 | { 49 | return $this->files[$this->position]; 50 | } 51 | 52 | public function next(): void 53 | { 54 | $this->position++; 55 | } 56 | } 57 | --------------------------------------------------------------------------------