Compiler::compile()   B
last analyzed

Complexity

Conditions 7
Paths 19

Size

Total Lines 43

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 43
rs 8.2986
c 0
b 0
f 0
cc 7
nc 19
nop 1
1
<?php
2
/*
3
 * PHAR Compiler Library
4
 * Copyright (C) 2021 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
11
namespace Secondtruth\Compiler;
12
13
/**
14
 * The Compiler class creates PHAR archives
15
 *
16
 * @author   Fabien Potencier <[email protected]>
17
 * @author   Jordi Boggiano <[email protected]>
18
 * @author   Christian Neff <[email protected]>
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', '<?php'];
225
        $stub[] = "Phar::mapPhar('$name');";
226
        $stub[] = "if (PHP_SAPI == 'cli') {";
227
228 View Code Duplication
        if (isset($this->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 View Code Duplication
        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