Completed
Push — master ( 7509a8...0de34b )
by Christian
03:17
created

Compiler::stripWhitespace()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 27
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 6.73

Importance

Changes 0
Metric Value
dl 0
loc 27
ccs 16
cts 22
cp 0.7272
rs 8.439
c 0
b 0
f 0
cc 6
eloc 17
nc 6
nop 1
crap 6.73
1
<?php
2
/**
3
 * Compiler Library
4
 * Copyright (C) 2015 secondtruth
5
 *
6
 * Permission to use, copy, modify, and/or distribute this software for
7
 * any purpose with or without fee is hereby granted, provided that the
8
 * above copyright notice and this permission notice appear in all copies.
9
 *
10
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE
13
 * FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
14
 * DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER
15
 * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
16
 * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17
 *
18
 * @package  Secondtruth\Compiler
19
 * @version  1.1
20
 * @link     http://www.secpndtruth.de
21
 * @license  ISC License <http://opensource.org/licenses/ISC>
22
 */
23
24
namespace Secondtruth\Compiler;
25
26
/**
27
 * The Compiler class creates PHAR archives
28
 *
29
 * @author   Fabien Potencier <[email protected]>
30
 * @author   Jordi Boggiano <[email protected]>
31
 * @author   Christian Neff <[email protected]>
32
 */
33
class Compiler
34
{
35
    /**
36
     * @var string
37
     */
38
    protected $path;
39
40
    /**
41
     * @var array
42
     */
43
    protected $files = array();
44
45
    /**
46
     * @var array
47
     */
48
    protected $index = array();
49
50
    /**
51
     * Creates a Compiler instance.
52
     *
53
     * @param string $path The root path of the project
54
     * @throws \LogicException if the creation of Phar archives is disabled in php.ini.
55
     */
56 9
    public function __construct($path)
57
    {
58 9
        if (ini_get('phar.readonly')) {
59
            throw new \LogicException('Creation of Phar archives is disabled in php.ini. Please make sure that "phar.readonly" is set to "off".');
60
        }
61
62 9
        $this->path = realpath($path);
63 9
    }
64
65
    /**
66
     * Compiles all files into a single PHAR file.
67
     *
68
     * @param string $outputfile The full name of the file to create
69
     * @throws \LogicException if no index files are defined.
70
     */
71 9
    public function compile($outputfile)
72
    {
73 9
        if (empty($this->index)) {
74
            throw new \LogicException('Cannot compile when no index files are defined.');
75
        }
76
77 9
        if (file_exists($outputfile)) {
78
            unlink($outputfile);
79
        }
80
81 9
        $name = basename($outputfile);
82 9
        $phar = new \Phar($outputfile, 0, $name);
83 9
        $phar->setSignatureAlgorithm(\Phar::SHA1);
84 9
        $phar->startBuffering();
85
86 9
        foreach ($this->files as $virtualfile => $fileinfo) {
87 9
            list($realfile, $strip) = $fileinfo;
88 9
            $content = file_get_contents($realfile);
89
90 9
            if ($strip) {
91 9
                $content = $this->stripWhitespace($content);
92 9
            }
93
94 9
            $phar->addFromString($virtualfile, $content);
95 9
        }
96
97 9
        foreach ($this->index as $type => $fileinfo) {
98 9
            list($virtualfile, $realfile) = $fileinfo;
99 9
            $content = file_get_contents($realfile);
100
101 9
            if ($type == 'cli') {
102 9
                $content = preg_replace('{^#!/usr/bin/env php\s*}', '', $content);
103 9
            }
104
105 9
            $phar->addFromString($virtualfile, $content);
106 9
        }
107
108 9
        $stub = $this->generateStub($name);
109 9
        $phar->setStub($stub);
110
111 9
        $phar->stopBuffering();
112 9
        unset($phar);
113 9
    }
114
115
    /**
116
     * Gets the root path of the project.
117
     *
118
     * @return string
119
     */
120
    public function getPath()
121
    {
122
        return $this->path;
123
    }
124
125
    /**
126
     * Gets list of all added files.
127
     *
128
     * @return array
129
     */
130
    public function getFiles()
131
    {
132
        return $this->files;
133
    }
134
135
    /**
136
     * Adds a file.
137
     *
138
     * @param string $file The name of the file relative to the project root
139
     * @param bool $strip Strip whitespace (Default: TRUE)
140
     */
141 9
    public function addFile($file, $strip = true)
142
    {
143 9
        $realfile = realpath($this->path . DIRECTORY_SEPARATOR . $file);
144 9
        $this->files[$file] = [$realfile, (bool) $strip];
145 9
    }
146
147
    /**
148
     * Adds files of the given directory recursively.
149
     *
150
     * @param string $directory The name of the directory relative to the project root
151
     * @param string|array $exclude List of file name patterns to exclude (optional)
152
     * @param bool $strip Strip whitespace (Default: TRUE)
153
     */
154 6
    public function addDirectory($directory, $exclude = null, $strip = true)
155
    {
156 6
        $realpath = realpath($this->path . DIRECTORY_SEPARATOR . $directory);
157 6
        $iterator = new \RecursiveDirectoryIterator($realpath, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS);
158
159 6
        if ((is_string($exclude) || is_array($exclude)) && !empty($exclude)) {
160 3
            $iterator = new \RecursiveCallbackFilterIterator($iterator, function ($current) use ($exclude, $realpath) {
161 3
                if ($current->isDir()) {
162
                    return true;
163
                }
164
165 3
                $subpath = substr($current->getPathName(), strlen($realpath) + 1);
166
167 3
                return $this->filter($subpath, (array) $exclude);
168 3
            });
169 3
        }
170
171 6
        $iterator = new \RecursiveIteratorIterator($iterator);
172 6
        foreach ($iterator as $file) {
173 6
            $virtualfile = substr($file->getPathName(), strlen($this->path) + 1);
174 6
            $this->addFile($virtualfile, $strip);
175 6
        }
176 6
    }
177
178
    /**
179
     * Gets list of defined index files.
180
     *
181
     * @return array
182
     */
183
    public function getIndexFiles()
184
    {
185
        return $this->index;
186
    }
187
188
    /**
189
     * Adds an index file.
190
     *
191
     * @param string $file The name of the file relative to the project root
192
     * @param string $type The SAPI type (Default: 'cli')
193
     */
194 9
    public function addIndexFile($file, $type = 'cli')
195
    {
196 9
        $type = strtolower($type);
197
198 9
        if (!in_array($type, ['cli', 'web'])) {
199
            throw new \InvalidArgumentException(sprintf('Index file type "%s" is invalid, must be one of: cli, web', $type));
200
        }
201
202 9
        $this->index[$type] = [$file, realpath($this->path . DIRECTORY_SEPARATOR . $file)];
203 9
    }
204
205
    /**
206
     * Gets list of all supported SAPIs.
207
     *
208
     * @return array
209
     */
210
    public function getSupportedSapis()
211
    {
212
        return array_keys($this->index);
213
    }
214
215
    /**
216
     * Returns whether the compiled program will support the given SAPI type.
217
     *
218
     * @param string $sapi The SAPI type
219
     * @return bool
220
     */
221
    public function supportsSapi($sapi)
222
    {
223
        return in_array((string) $sapi, $this->getSupportedSapis());
224
    }
225
226
    /**
227
     * Generates the stub.
228
     *
229
     * @param string $name The internal Phar name
230
     * @return string
231
     */
232 9
    protected function generateStub($name)
233
    {
234 9
        $stub = array('#!/usr/bin/env php', '<?php');
235 9
        $stub[] = "Phar::mapPhar('$name');";
236 9
        $stub[] = "if (PHP_SAPI == 'cli') {";
237
238 9 View Code Duplication
        if (isset($this->index['cli'])) {
239 9
            $file = $this->index['cli'][0];
240 9
            $stub[] = " require 'phar://$name/$file';";
241 9
        } else {
242
            $stub[] = " exit('This program can not be invoked via the CLI version of PHP, use the Web interface instead.'.PHP_EOL);";
243
        }
244
245 9
        $stub[] = '} else {';
246
247 9 View Code Duplication
        if (isset($this->index['web'])) {
248
            $file = $this->index['web'][0];
249
            $stub[] = " require 'phar://$name/$file';";
250
        } else {
251 9
            $stub[] = " exit('This program can not be invoked via the Web interface, use the CLI version of PHP instead.'.PHP_EOL);";
252
        }
253
254 9
        $stub[] = '}';
255 9
        $stub[] = '__HALT_COMPILER();';
256
257 9
        return join("\n", $stub);
258
    }
259
260
    /**
261
     * Filters the given path.
262
     *
263
     * @param array $patterns
264
     * @return bool
265
     */
266 3
    protected function filter($path, array $patterns)
267
    {
268 3
        foreach ($patterns as $pattern) {
269 3
            if ($pattern[0] == '!' ? !fnmatch(substr($pattern, 1), $path) : fnmatch($pattern, $path)) {
270 3
                return false;
271
            }
272 3
        }
273
274 3
        return true;
275
    }
276
277
    /**
278
     * Removes whitespace from a PHP source string while preserving line numbers.
279
     *
280
     * @param string $source A PHP string
281
     * @return string The PHP string with the whitespace removed
282
     */
283 9
    protected function stripWhitespace($source)
284
    {
285 9
        if (!function_exists('token_get_all')) {
286
            return $source;
287
        }
288
289 9
        $output = '';
290 9
        foreach (token_get_all($source) as $token) {
291 9
            if (is_string($token)) {
292 9
                $output .= $token;
293 9
            } elseif (in_array($token[0], array(T_COMMENT, T_DOC_COMMENT))) {
294
                $output .= str_repeat("\n", substr_count($token[1], "\n"));
295 9
            } elseif (T_WHITESPACE === $token[0]) {
296
                // reduce wide spaces
297 9
                $whitespace = preg_replace('{[ \t]+}', ' ', $token[1]);
298
                // normalize newlines to \n
299 9
                $whitespace = preg_replace('{(?:\r\n|\r|\n)}', "\n", $whitespace);
300
                // trim leading spaces
301 9
                $whitespace = preg_replace('{\n +}', "\n", $whitespace);
302 9
                $output .= $whitespace;
303 9
            } else {
304 9
                $output .= $token[1];
305
            }
306 9
        }
307
308 9
        return $output;
309
    }
310
}
311