├── .editorconfig ├── LICENSE.md ├── README.md ├── composer.json └── src └── Compiler.php /.editorconfig: -------------------------------------------------------------------------------- 1 | ; This file is for unifying the coding style for different editors and IDEs. 2 | ; More information at http://editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | indent_size = 4 9 | indent_style = space 10 | end_of_line = LF 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | [*.yml] 15 | indent_size = 2 16 | 17 | [*.md] 18 | trim_trailing_whitespace = false 19 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright (c) 2020 Christian Neff 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PHP PHAR Compiler 2 | ================= 3 | 4 | [![Latest Stable](http://img.shields.io/packagist/v/secondtruth/phar-compiler.svg)](https://packagist.org/p/secondtruth/phar-compiler) 5 | [![Build Status](https://img.shields.io/travis/com/secondtruth/php-phar-compiler.svg)](https://travis-ci.com/github/secondtruth/php-phar-compiler) 6 | [![Scrutinizer](http://img.shields.io/scrutinizer/g/secondtruth/php-phar-compiler.svg)](https://scrutinizer-ci.com/g/secondtruth/php-phar-compiler) 7 | [![Coverage](http://img.shields.io/scrutinizer/coverage/g/secondtruth/php-phar-compiler.svg)](https://scrutinizer-ci.com/g/secondtruth/php-phar-compiler) 8 | [![License](http://img.shields.io/packagist/l/secondtruth/phar-compiler.svg)](https://packagist.org/p/secondtruth/phar-compiler) 9 | 10 | This library provides a generic PHP PHAR compiler. 11 | 12 | 13 | How to use? 14 | ----------- 15 | 16 | ```php 17 | $compiler = new Compiler(PROJECT_PATH); 18 | 19 | $compiler->addIndexFile('bin/mycoolprogram.php'); 20 | $compiler->addDirectory('libraries'); 21 | 22 | $compiler->addFile('vendor/autoload.php'); 23 | $compiler->addDirectory('vendor/composer', '!*.php'); // Exclude non-PHP files 24 | $compiler->addDirectory('vendor/.../Component/Console', ['Tests/*', '!*.php']); 25 | 26 | $compiler->compile("$outputDir/mycoolprogram.phar"); 27 | ``` 28 | 29 | 30 | Installation 31 | ------------ 32 | 33 | ### Install via Composer 34 | 35 | [Install Composer](https://getcomposer.org/doc/00-intro.md#installation-linux-unix-macos) if you don't already have it present on your system. 36 | 37 | To install the library, run the following command and you will get the latest version: 38 | 39 | $ composer require secondtruth/phar-compiler 40 | 41 | 42 | Requirements 43 | ------------ 44 | 45 | * You must have at least PHP version 5.6 installed on your system. 46 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "secondtruth/phar-compiler", 3 | "description": "Generic PHP PHAR compiler", 4 | "homepage": "https://www.secondtruth.de", 5 | "keywords": ["phar", "compiler"], 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Christian Neff", 10 | "email": "christian.neff@gmail.com", 11 | "homepage": "http://www.secondtruth.de", 12 | "role": "Developer" 13 | } 14 | ], 15 | "support": { 16 | "issues": "https://github.com/secondtruth/php-phar-compiler/issues", 17 | "source": "https://github.com/secondtruth/php-phar-compiler" 18 | }, 19 | "require": { 20 | "php": ">=5.6" 21 | }, 22 | "require-dev": { 23 | "phpunit/phpunit": "5.7.*", 24 | "scrutinizer/ocular": "1.6.*" 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "Secondtruth\\Compiler\\": "src/" 29 | } 30 | }, 31 | "autoload-dev": { 32 | "psr-4": { 33 | "Secondtruth\\Compiler\\Tests\\": "tests/" 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Compiler.php: -------------------------------------------------------------------------------- 1 | 17 | * @author Jordi Boggiano 18 | * @author Christian Neff 19 | */ 20 | class Compiler 21 | { 22 | /** 23 | * @var string 24 | */ 25 | protected $path; 26 | 27 | /** 28 | * @var array 29 | */ 30 | protected $files = array(); 31 | 32 | /** 33 | * @var array 34 | */ 35 | protected $index = array(); 36 | 37 | /** 38 | * Creates a Compiler instance. 39 | * 40 | * @param string $path The root path of the project 41 | * @throws \LogicException if the creation of Phar archives is disabled in php.ini. 42 | */ 43 | public function __construct($path) 44 | { 45 | if (ini_get('phar.readonly')) { 46 | throw new \LogicException('Creation of Phar archives is disabled in php.ini. Please make sure that "phar.readonly" is set to "off".'); 47 | } 48 | 49 | $this->path = realpath($path); 50 | } 51 | 52 | /** 53 | * Compiles all files into a single PHAR file. 54 | * 55 | * @param string $outputFile The full name of the file to create 56 | * @throws \LogicException if no index files are defined. 57 | */ 58 | public function compile($outputFile) 59 | { 60 | if (empty($this->index)) { 61 | throw new \LogicException('Cannot compile when no index files are defined.'); 62 | } 63 | 64 | if (file_exists($outputFile)) { 65 | unlink($outputFile); 66 | } 67 | 68 | $name = basename($outputFile); 69 | $phar = new \Phar($outputFile, 0, $name); 70 | $phar->setSignatureAlgorithm(\Phar::SHA1); 71 | $phar->startBuffering(); 72 | 73 | foreach ($this->files as $virtualFile => $fileInfo) { 74 | list($realFile, $strip) = $fileInfo; 75 | $content = file_get_contents($realFile); 76 | 77 | if ($strip) { 78 | $content = $this->stripWhitespace($content); 79 | } 80 | 81 | $phar->addFromString($virtualFile, $content); 82 | } 83 | 84 | foreach ($this->index as $type => $fileInfo) { 85 | list($virtualFile, $realFile) = $fileInfo; 86 | $content = file_get_contents($realFile); 87 | 88 | if ($type == 'cli') { 89 | $content = preg_replace('{^#!/usr/bin/env php\s*}', '', $content); 90 | } 91 | 92 | $phar->addFromString($virtualFile, $content); 93 | } 94 | 95 | $stub = $this->generateStub($name); 96 | $phar->setStub($stub); 97 | 98 | $phar->stopBuffering(); 99 | unset($phar); 100 | } 101 | 102 | /** 103 | * Gets the root path of the project. 104 | * 105 | * @return string 106 | */ 107 | public function getPath() 108 | { 109 | return $this->path; 110 | } 111 | 112 | /** 113 | * Gets list of all added files. 114 | * 115 | * @return array 116 | */ 117 | public function getFiles() 118 | { 119 | return $this->files; 120 | } 121 | 122 | /** 123 | * Adds a file. 124 | * 125 | * @param string $file The name of the file relative to the project root 126 | * @param bool $strip Strip whitespace (Default: TRUE) 127 | */ 128 | public function addFile($file, $strip = true) 129 | { 130 | $realFile = realpath($this->path . DIRECTORY_SEPARATOR . $file); 131 | $this->files[$file] = [$realFile, (bool) $strip]; 132 | } 133 | 134 | /** 135 | * Adds files of the given directory recursively. 136 | * 137 | * @param string $directory The name of the directory relative to the project root 138 | * @param string|array $exclude List of file name patterns to exclude (optional) 139 | * @param bool $strip Strip whitespace (Default: TRUE) 140 | */ 141 | public function addDirectory($directory, $exclude = null, $strip = true) 142 | { 143 | $realPath = realpath($this->path . DIRECTORY_SEPARATOR . $directory); 144 | $iterator = new \RecursiveDirectoryIterator( 145 | $realPath, 146 | \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS | \FilesystemIterator::CURRENT_AS_SELF 147 | ); 148 | 149 | if ((is_string($exclude) || is_array($exclude)) && !empty($exclude)) { 150 | $exclude = (array) $exclude; 151 | $iterator = new \RecursiveCallbackFilterIterator($iterator, function (\RecursiveDirectoryIterator $current) use ($exclude) { 152 | if ($current->isDir()) { 153 | return true; 154 | } 155 | 156 | return $this->filter($current->getSubPathname(), $exclude); 157 | }); 158 | } 159 | 160 | $iterator = new \RecursiveIteratorIterator($iterator); 161 | foreach ($iterator as $file) { 162 | /** @var \SplFileInfo $file */ 163 | $virtualFile = substr($file->getPathName(), strlen($this->path) + 1); 164 | $this->addFile($virtualFile, $strip); 165 | } 166 | } 167 | 168 | /** 169 | * Gets list of defined index files. 170 | * 171 | * @return array 172 | */ 173 | public function getIndexFiles() 174 | { 175 | return $this->index; 176 | } 177 | 178 | /** 179 | * Adds an index file. 180 | * 181 | * @param string $file The name of the file relative to the project root 182 | * @param string $type The SAPI type (Default: 'cli') 183 | */ 184 | public function addIndexFile($file, $type = 'cli') 185 | { 186 | $type = strtolower($type); 187 | 188 | if (!in_array($type, ['cli', 'web'])) { 189 | throw new \InvalidArgumentException(sprintf('Index file type "%s" is invalid, must be one of: cli, web', $type)); 190 | } 191 | 192 | $this->index[$type] = [$file, realpath($this->path . DIRECTORY_SEPARATOR . $file)]; 193 | } 194 | 195 | /** 196 | * Gets list of all supported SAPIs. 197 | * 198 | * @return array 199 | */ 200 | public function getSupportedSapis() 201 | { 202 | return array_keys($this->index); 203 | } 204 | 205 | /** 206 | * Returns whether the compiled program will support the given SAPI type. 207 | * 208 | * @param string $sapi The SAPI type 209 | * @return bool 210 | */ 211 | public function supportsSapi($sapi) 212 | { 213 | return in_array((string) $sapi, $this->getSupportedSapis()); 214 | } 215 | 216 | /** 217 | * Generates the stub. 218 | * 219 | * @param string $name The internal Phar name 220 | * @return string 221 | */ 222 | protected function generateStub($name) 223 | { 224 | $stub = ['#!/usr/bin/env php', 'index['cli'])) { 229 | $file = $this->index['cli'][0]; 230 | $stub[] = " require 'phar://$name/$file';"; 231 | } else { 232 | $stub[] = " exit('This program can not be invoked via the CLI version of PHP, use the Web interface instead.'.PHP_EOL);"; 233 | } 234 | 235 | $stub[] = '} else {'; 236 | 237 | if (isset($this->index['web'])) { 238 | $file = $this->index['web'][0]; 239 | $stub[] = " require 'phar://$name/$file';"; 240 | } else { 241 | $stub[] = " exit('This program can not be invoked via the Web interface, use the CLI version of PHP instead.'.PHP_EOL);"; 242 | } 243 | 244 | $stub[] = '}'; 245 | $stub[] = '__HALT_COMPILER();'; 246 | 247 | return join("\n", $stub); 248 | } 249 | 250 | /** 251 | * Matches the given path. 252 | * 253 | * @param string $path 254 | * @param string $pattern 255 | * @return bool 256 | */ 257 | protected function match($path, $pattern) 258 | { 259 | $inverted = false; 260 | 261 | if ($pattern[0] == '!') { 262 | $pattern = substr($pattern, 1); 263 | $inverted = true; 264 | } 265 | 266 | return fnmatch($pattern, $path) == ($inverted ? false : true); 267 | } 268 | 269 | /** 270 | * Filters the given path. 271 | * 272 | * @param string $path 273 | * @param array $patterns 274 | * @return bool 275 | */ 276 | protected function filter($path, array $patterns) 277 | { 278 | foreach ($patterns as $pattern) { 279 | if ($this->match($path, $pattern)) { 280 | return false; 281 | } 282 | } 283 | 284 | return true; 285 | } 286 | 287 | /** 288 | * Removes whitespace from a PHP source string while preserving line numbers. 289 | * 290 | * @param string $source A PHP string 291 | * @return string The PHP string with the whitespace removed 292 | */ 293 | protected function stripWhitespace($source) 294 | { 295 | if (!function_exists('token_get_all')) { 296 | return $source; 297 | } 298 | 299 | $output = ''; 300 | foreach (token_get_all($source) as $token) { 301 | if (is_string($token)) { 302 | $output .= $token; 303 | } elseif (in_array($token[0], array(T_COMMENT, T_DOC_COMMENT))) { 304 | $output .= str_repeat("\n", substr_count($token[1], "\n")); 305 | } elseif (T_WHITESPACE === $token[0]) { 306 | // reduce wide spaces 307 | $whitespace = preg_replace('{[ \t]+}', ' ', $token[1]); 308 | // normalize newlines to \n 309 | $whitespace = preg_replace('{(?:\r\n|\r|\n)}', "\n", $whitespace); 310 | // trim leading spaces 311 | $whitespace = preg_replace('{\n +}', "\n", $whitespace); 312 | $output .= $whitespace; 313 | } else { 314 | $output .= $token[1]; 315 | } 316 | } 317 | 318 | return $output; 319 | } 320 | } 321 | --------------------------------------------------------------------------------