Completed
Push — master ( 19620b...82c60a )
by Christian
05:13 queued 11s
created

Compiler::addDirectory()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 6.0073

Importance

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