Completed
Pull Request — master (#37)
by Théo
31:28 queued 20:15
created

StubGenerator::mung()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 6
nc 1
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the box project.
7
 *
8
 * (c) Kevin Herrera <[email protected]>
9
 *     Théo Fidry <[email protected]>
10
 *
11
 * This source file is subject to the MIT license that is bundled
12
 * with this source code in the file LICENSE.
13
 */
14
15
namespace KevinGH\Box;
16
17
use Assert\Assertion;
18
use Herrera\Annotations\Tokenizer;
19
use KevinGH\Box\Compactor\Php;
20
21
/**
22
 * Generates a new PHP bootstrap loader stub for a PHAR.
23
 */
24
final class StubGenerator
25
{
26
    /**
27
     * @var string[] the list of server variables that are allowed to be modified
28
     */
29
    private const ALLOWED_MUNG = [
30
        'PHP_SELF',
31
        'REQUEST_URI',
32
        'SCRIPT_FILENAME',
33
        'SCRIPT_NAME',
34
    ];
35
36
    /**
37
     * @var string The alias to be used in "phar://" URLs
38
     */
39
    private $alias = '';
40
41
    /**
42
     * @var null|string The top header comment banner text
43
     */
44
    private $banner = <<<'BANNER'
45
46
Generated by Box.
47
48
@link https://github.com/humbug/box
49
50
BANNER;
51
52
    /**
53
     * @var bool Embed the Extract class in the stub?
54
     */
55
    private $extract = false;
56
57
    /**
58
     * @var array The processed extract code
59
     */
60
    private $extractCode = [];
61
62
    /**
63
     * @var bool Force the use of the Extract class?
64
     */
65
    private $extractForce = false;
66
67
    /**
68
     * @var null|string The location within the PHAR of index script
69
     */
70
    private $index;
71
72
    /**
73
     * @var bool Use the Phar::interceptFileFuncs() method?
74
     */
75
    private $intercept = false;
76
77
    /**
78
     * @var array The map for file extensions and their mimetypes
79
     */
80
    private $mimetypes = [];
81
82
    /**
83
     * The list of server variables to modify.
84
     *
85
     * @var array
86
     */
87
    private $mung = [];
88
89
    /**
90
     * @var null|string The location of the script to run when a file is not found
91
     */
92
    private $notFound;
93
94
    /**
95
     * @var null|string The rewrite function name
96
     */
97
    private $rewrite;
98
99
    /**
100
     * @var string The shebang line
101
     */
102
    private $shebang = '#!/usr/bin/env php';
103
104
    /**
105
     * Use Phar::webPhar() instead of Phar::mapPhar()?
106
     *
107
     * @var bool
108
     */
109
    private $web = false;
110
111
    /**
112
     * Creates a new instance of the stub generator.
113
     *
114
     * @return StubGenerator the stub generator
115
     */
116
    public static function create()
117
    {
118
        return new static();
119
    }
120
121
    /**
122
     * @return string The stub
123
     */
124
    public function generate(): string
125
    {
126
        $stub = [];
127
128
        if ('' !== $this->shebang) {
129
            $stub[] = $this->shebang;
130
        }
131
132
        $stub[] = '<?php';
133
134
        if (null !== $this->banner) {
135
            $stub[] = $this->getBanner();
136
        }
137
138
        if ($this->extract) {
139
            $stub[] = implode("\n", $this->extractCode['constants']);
140
141
            if ($this->extractForce) {
142
                $stub = array_merge($stub, $this->getExtractSections());
143
            }
144
        }
145
146
        $stub = array_merge($stub, $this->getPharSections());
147
148
        if ($this->extract) {
149
            if ($this->extractForce) {
150
                if ($this->index && !$this->web) {
151
                    $stub[] = "require \"\$dir/{$this->index}\";";
152
                }
153
            } else {
154
                end($stub);
155
156
                $stub[key($stub)] .= ' else {';
157
158
                $stub = array_merge($stub, $this->getExtractSections());
159
160
                if ($this->index) {
161
                    $stub[] = "require \"\$dir/{$this->index}\";";
162
                }
163
164
                $stub[] = '}';
165
            }
166
167
            $stub[] = implode("\n", $this->extractCode['class']);
168
        }
169
170
        $stub[] = '__HALT_COMPILER();';
171
172
        return implode("\n", $stub);
173
    }
174
175
    public function alias(string $alias): self
176
    {
177
        $this->alias = $alias;
178
179
        return $this;
180
    }
181
182
    public function banner(?string $banner): self
183
    {
184
        $this->banner = $banner;
185
186
        return $this;
187
    }
188
189
    public function extract(bool $extract, bool $force = false): self
190
    {
191
        $this->extract = $extract;
192
        $this->extractForce = $force;
193
194
        if ($extract) {
195
            $this->extractCode = [
196
                'constants' => [],
197
                'class' => [],
198
            ];
199
200
            $compactor = new Php(new Tokenizer());
201
202
            $file = __DIR__.'/Box_Extract.php';
203
204
            $code = file_get_contents($file);
205
            $code = $compactor->compact($file, $code);
206
            $code = preg_replace('/\n+/', "\n", $code);
207
            $code = explode("\n", $code);
208
            $code = array_slice($code, 2);
209
210
            foreach ($code as $i => $line) {
211
                if ((0 === strpos($line, 'use'))
212
                    && (false === strpos($line, '\\'))
213
                ) {
214
                    unset($code[$i]);
215
                } elseif (0 === strpos($line, 'define')) {
216
                    $this->extractCode['constants'][] = $line;
217
                } else {
218
                    $this->extractCode['class'][] = $line;
219
                }
220
            }
221
        }
222
223
        return $this;
224
    }
225
226
    public function index(?string $index): self
227
    {
228
        $this->index = $index;
229
230
        return $this;
231
    }
232
233
    public function intercept(bool $intercept): self
234
    {
235
        $this->intercept = $intercept;
236
237
        return $this;
238
    }
239
240
    public function mimetypes(array $mimetypes): self
241
    {
242
        $this->mimetypes = $mimetypes;
243
244
        return $this;
245
    }
246
247
    public function mung(array $list): self
248
    {
249
        Assertion::allInArray(
250
            $list,
251
            self::ALLOWED_MUNG,
252
            'The $_SERVER variable "%s" is not allowed.'
253
        );
254
255
        $this->mung = $list;
256
257
        return $this;
258
    }
259
260
    public function notFound(?string $script): self
261
    {
262
        $this->notFound = $script;
263
264
        return $this;
265
    }
266
267
    public function rewrite(?string $function): self
268
    {
269
        $this->rewrite = $function;
270
271
        return $this;
272
    }
273
274
    public function shebang(string $shebang): self
275
    {
276
        $this->shebang = $shebang;
277
278
        return $this;
279
    }
280
281
    public function web(bool $web): self
282
    {
283
        $this->web = $web;
284
285
        return $this;
286
    }
287
288
    /**
289
     * Escapes an argument so it can be written as a string in a call.
290
     *
291
     * @param string $arg
292
     * @param string $quote
293
     *
294
     * @return string The escaped argument
295
     */
296
    private function arg(string $arg, string $quote = "'"): string
297
    {
298
        return $quote.addcslashes($arg, $quote).$quote;
299
    }
300
301
    /**
302
     * @return string The alias map
303
     */
304
    private function getAlias(): string
305
    {
306
        $stub = '';
307
        $prefix = '';
308
309
        if ($this->extractForce) {
310
            $prefix = '$dir/';
311
        }
312
313
        if ($this->web) {
314
            $stub .= 'Phar::webPhar('.$this->arg($this->alias);
315
316
            if ($this->index) {
317
                $stub .= ', '.$this->arg($prefix.$this->index, '"');
318
319
                if ($this->notFound) {
320
                    $stub .= ', '.$this->arg($prefix.$this->notFound, '"');
321
322
                    if ($this->mimetypes) {
323
                        $stub .= ', '.var_export(
324
                            $this->mimetypes,
325
                            true
326
                        );
327
328
                        if ($this->rewrite) {
329
                            $stub .= ', '.$this->arg($this->rewrite);
330
                        }
331
                    }
332
                }
333
            }
334
335
            $stub .= ');';
336
        } else {
337
            $stub .= 'Phar::mapPhar('.$this->arg($this->alias).');';
338
        }
339
340
        return $stub;
341
    }
342
343
    /**
344
     * @return string the processed banner
345
     */
346
    private function getBanner(): string
347
    {
348
        // TODO: review how the banner is processed. Right now the doc says it can be a string
349
        // already enclosed in comments and if not it will be enclosed automatically.
350
        //
351
        // What needs to be done here?
352
        // - Test with a simple one liner banner
353
        // - Test with a banner enclosed in comments
354
        // - Test with a banner enclosed in phpdoc
355
        //
356
        // Then comes the question of multiline banners: I guess it works if contains `\n`?
357
        // Need tests for that anyway.
358
        //
359
        // Maybe a more user-friendly way to deal with multi-line banners would be to allow
360
        // an array of strings instead of just a string.
361
        //
362
363
        $banner = "/**\n * ";
364
        $banner .= str_replace(
365
            " \n",
366
            "\n",
367
            str_replace("\n", "\n * ", $this->banner)
368
        );
369
370
        $banner .= "\n */";
371
372
        return $banner;
373
    }
374
375
    /**
376
     * @return string[] The self extracting sections of the stub
377
     */
378
    private function getExtractSections(): array
379
    {
380
        return [
381
            '$extract = new Extract(__FILE__, Extract::findStubLength(__FILE__));',
382
            '$dir = $extract->go();',
383
            'set_include_path($dir . PATH_SEPARATOR . get_include_path());',
384
        ];
385
    }
386
387
    /**
388
     * @return string[] The sections of the stub that use the PHAR class
389
     */
390
    private function getPharSections(): array
391
    {
392
        $stub = [
393
            'if (class_exists(\'Phar\')) {',
394
            $this->getAlias(),
395
        ];
396
397
        if ($this->intercept) {
398
            $stub[] = 'Phar::interceptFileFuncs();';
399
        }
400
401
        if ($this->mung) {
402
            $stub[] = 'Phar::mungServer('.var_export($this->mung, true).');';
403
        }
404
405
        if ($this->index && !$this->web && !$this->extractForce) {
406
            $stub[] = "require 'phar://' . __FILE__ . '/{$this->index}';";
407
        }
408
409
        $stub[] = '}';
410
411
        return $stub;
412
    }
413
}
414