Passed
Pull Request — master (#31)
by Théo
02:18
created

Box   A

Complexity

Total Complexity 32

Size/Duplication

Total Lines 372
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 32
dl 0
loc 372
rs 9.6
c 0
b 0
f 0

15 Methods

Rating   Name   Duplication   Size   Complexity  
B addFile() 0 36 6
B processContents() 0 31 3
A getPhar() 0 3 1
A registerCompactors() 0 11 3
A sign() 0 23 2
A registerStub() 0 8 1
A replacePlaceholders() 0 6 1
A registerPlaceholders() 0 17 3
A create() 0 7 1
A registerFileMapping() 0 4 1
A compactContents() 0 8 1
B addFiles() 0 48 5
A signUsingFile() 0 3 1
A __construct() 0 7 1
A dumpFiles() 0 21 2
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 const DIRECTORY_SEPARATOR;
19
use Humbug\PhpScoper\Console\Configuration as PhpScoperConfiguration;
20
use KevinGH\Box\Compactor\PhpScoper;
21
use KevinGH\Box\Composer\ComposerOrchestrator;
22
use function KevinGH\Box\FileSystem\make_path_absolute;
23
use Phar;
24
use RecursiveDirectoryIterator;
25
use SplFileInfo;
26
use function Amp\ParallelFunctions\parallelMap;
27
use function Amp\Promise\wait;
28
use function array_map;
29
use function chdir;
30
use function is_file;
31
use function KevinGH\Box\FileSystem\copy;
32
use function KevinGH\Box\FileSystem\dump_file;
33
use function KevinGH\Box\FileSystem\file_contents;
34
use function KevinGH\Box\FileSystem\make_path_relative;
35
use function KevinGH\Box\FileSystem\make_tmp_dir;
36
use function KevinGH\Box\FileSystem\mkdir;
37
use function KevinGH\Box\FileSystem\remove;
38
use function KevinGH\Box\FileSystem\rename;
39
40
/**
41
 * Box is a utility class to generate a PHAR.
42
 */
43
final class Box
44
{
45
    private const DEBUG_DIR = '.box';
46
47
    /**
48
     * @var Compactor[]
49
     */
50
    private $compactors = [];
51
52
    /**
53
     * @var string The path to the PHAR file
54
     */
55
    private $file;
56
57
    /**
58
     * @var Phar The PHAR instance
59
     */
60
    private $phar;
61
62
    /**
63
     * @var scalar[] The placeholders with their values
64
     */
65
    private $placeholders = [];
66
67
    /**
68
     * @var string
69
     */
70
    private $basePath;
71
72
    /**
73
     * @var MapFile
74
     */
75
    private $mapFile;
76
77
    /**
78
     * @var null|PhpScoperConfiguration
79
     */
80
    private $phpScoperConfig;
81
82
    private function __construct(Phar $phar, string $file)
83
    {
84
        $this->phar = $phar;
85
        $this->file = $file;
86
87
        $this->basePath = getcwd();
88
        $this->mapFile = function (): void { };
89
    }
90
91
    /**
92
     * Creates a new PHAR and Box instance.
93
     *
94
     * @param string $file  The PHAR file name
95
     * @param int    $flags Flags to pass to the Phar parent class RecursiveDirectoryIterator
96
     * @param string $alias Alias with which the Phar archive should be referred to in calls to stream functionality
97
     *
98
     * @return Box
99
     *
100
     * @see RecursiveDirectoryIterator
101
     */
102
    public static function create(string $file, int $flags = null, string $alias = null): self
103
    {
104
        // Ensure the parent directory of the PHAR file exists as `new \Phar()` does not create it and would fail
105
        // otherwise.
106
        mkdir(dirname($file));
107
108
        return new self(new Phar($file, (int) $flags, $alias), $file);
109
    }
110
111
    /**
112
     * @param Compactor[] $compactors
113
     */
114
    public function registerCompactors(array $compactors): void
115
    {
116
        Assertion::allIsInstanceOf($compactors, Compactor::class);
117
118
        $this->compactors = $compactors;
119
120
        foreach ($this->compactors as $compactor) {
121
            if ($compactor instanceof PhpScoper) {
122
                $this->phpScoperConfig = $compactor->getConfiguration();
123
124
                break;
125
            }
126
        }
127
    }
128
129
    /**
130
     * @param scalar[] $placeholders
131
     */
132
    public function registerPlaceholders(array $placeholders): void
133
    {
134
        $message = 'Expected value "%s" to be a scalar or stringable object.';
135
136
        foreach ($placeholders as $i => $placeholder) {
137
            if (is_object($placeholder)) {
138
                Assertion::methodExists('__toString', $placeholder, $message);
139
140
                $placeholders[$i] = (string) $placeholder;
141
142
                break;
143
            }
144
145
            Assertion::scalar($placeholder, $message);
146
        }
147
148
        $this->placeholders = $placeholders;
149
    }
150
151
    public function registerFileMapping(string $basePath, MapFile $fileMapper): void
152
    {
153
        $this->basePath = $basePath;
154
        $this->mapFile = $fileMapper;
155
    }
156
157
    public function registerStub(string $file): void
158
    {
159
        $contents = self::replacePlaceholders(
160
            $this->placeholders,
161
            file_contents($file)
162
        );
163
164
        $this->phar->setStub($contents);
165
    }
166
167
    /**
168
     * @param SplFileInfo[]|string[] $files
169
     * @param bool                   $binary
170
     */
171
    public function addFiles(array $files, bool $binary): void
172
    {
173
        if ($binary) {
174
            foreach ($files as $file) {
175
                $this->addFile((string) $file, null, $binary);
176
            }
177
178
            return;
179
        }
180
181
        $cwd = getcwd();
182
183
        $filesWithContents = $this->processContents(
184
            array_map(
185
                function ($file): string {
186
                    // Convert files to string as SplFileInfo is not serializable
187
                    return (string) $file;
188
                },
189
                $files
190
            ),
191
            $cwd
192
        );
193
194
        $tmp = make_tmp_dir('box', __CLASS__);
195
        chdir($tmp);
196
197
        try {
198
            foreach ($filesWithContents as $fileWithContents) {
199
                [$file, $contents] = $fileWithContents;
200
201
                dump_file($file, $contents);
202
            }
203
204
            // Dump autoload without dev dependencies
205
            ComposerOrchestrator::dumpAutoload($this->phpScoperConfig);
206
207
            chdir($cwd);
208
209
            $this->phar->buildFromDirectory($tmp);
210
        } finally {
211
            if (is_debug_enabled()) {
212
                $this->dumpFiles(
213
                    make_path_absolute(self::DEBUG_DIR, $cwd),
214
                    $tmp
215
                );
216
            }
217
218
            remove($tmp);
219
        }
220
    }
221
222
    /**
223
     * Adds the a file to the PHAR. The contents will first be compacted and have its placeholders
224
     * replaced.
225
     *
226
     * @param string      $file
227
     * @param null|string $contents If null the content of the file will be used
228
     * @param bool        $binary   When true means the file content shouldn't be processed
229
     *
230
     * @return string File local path
231
     */
232
    public function addFile(string $file, string $contents = null, bool $binary = false): string
233
    {
234
        Assertion::file($file);
235
        Assertion::readable($file);
236
237
        $contents = null === $contents ? file_get_contents($file) : $contents;
238
239
        $relativePath = make_path_relative($file, $this->basePath);
240
        $local = ($this->mapFile)($relativePath);
241
242
        if (null === $local) {
243
            $local = $relativePath;
244
        }
245
246
        if ($binary) {
247
            $this->phar->addFile($file, $local);
248
        } else {
249
            $processedContents = self::compactContents(
250
                $this->compactors,
251
                $local,
252
                self::replacePlaceholders($this->placeholders, $contents)
253
            );
254
255
            $this->phar->addFromString($local, $processedContents);
256
        }
257
258
        if (is_debug_enabled()) {
259
            if (false === $processedContents) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $processedContents does not seem to be defined for all execution paths leading up to this point.
Loading history...
260
                $processedContents = $contents;
261
            }
262
263
            remove(self::DEBUG_DIR);    // Cleanup previous temporary debug directory
264
            dump_file(self::DEBUG_DIR.DIRECTORY_SEPARATOR.$relativePath, $processedContents);
265
        }
266
267
        return $local;
268
    }
269
270
    public function getPhar(): Phar
271
    {
272
        return $this->phar;
273
    }
274
275
    /**
276
     * Signs the PHAR using a private key file.
277
     *
278
     * @param string $file     the private key file name
279
     * @param string $password the private key password
280
     */
281
    public function signUsingFile(string $file, string $password = null): void
282
    {
283
        $this->sign(file_contents($file), $password);
284
    }
285
286
    /**
287
     * Signs the PHAR using a private key.
288
     *
289
     * @param string $key      The private key
290
     * @param string $password The private key password
291
     */
292
    public function sign(string $key, ?string $password): void
293
    {
294
        $pubKey = $this->file.'.pubkey';
295
296
        Assertion::writeable(dirname($pubKey));
297
        Assertion::extensionLoaded('openssl');
298
299
        if (file_exists($pubKey)) {
300
            Assertion::file(
301
                $pubKey,
302
                'Cannot create public key: "%s" already exists and is not a file.'
303
            );
304
        }
305
306
        $resource = openssl_pkey_get_private($key, (string) $password);
307
308
        openssl_pkey_export($resource, $private);
309
310
        $details = openssl_pkey_get_details($resource);
0 ignored issues
show
Bug introduced by
It seems like $resource can also be of type false; however, parameter $key of openssl_pkey_get_details() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

310
        $details = openssl_pkey_get_details(/** @scrutinizer ignore-type */ $resource);
Loading history...
311
312
        $this->phar->setSignatureAlgorithm(Phar::OPENSSL, $private);
313
314
        dump_file($pubKey, $details['key']);
315
    }
316
317
    /**
318
     * @param string[] $files
319
     * @param string   $cwd   Current working directory. As the processes are spawned for parallel processing, the
320
     *                        working directory may change so we pass the working directory in which the processing
321
     *                        is supposed to happen. This should not happen during regular usage as all the files are
322
     *                        absolute but it's possible this class is used with relative paths in which case this is
323
     *                        an issue.
324
     *
325
     * @return array array of tuples where the first element is the local file path (path inside the PHAR) and the
326
     *               second element is the processed contents
327
     */
328
    private function processContents(array $files, string $cwd): array
329
    {
330
        $basePath = $this->basePath;
331
        $mapFile = $this->mapFile;
332
        $placeholders = $this->placeholders;
333
        $compactors = $this->compactors;
334
335
        $processFile = function (string $file) use ($cwd, $basePath, $mapFile, $placeholders, $compactors): array {
336
            chdir($cwd);
337
338
            $contents = file_contents($file);
339
340
            $relativePath = make_path_relative($file, $basePath);
341
            $local = $mapFile($relativePath);
342
343
            if (null === $local) {
344
                $local = $relativePath;
345
            }
346
347
            $processedContents = self::compactContents(
348
                $compactors,
349
                $local,
350
                self::replacePlaceholders($placeholders, $contents)
351
            );
352
353
            return [$local, $processedContents];
354
        };
355
356
        return is_debug_enabled()
357
            ? array_map($processFile, $files)
358
            : wait(parallelMap($files, $processFile))
359
        ;
360
    }
361
362
    /**
363
     * Replaces the placeholders with their values.
364
     *
365
     * @param array  $placeholders
366
     * @param string $contents     the contents
367
     *
368
     * @return string the replaced contents
369
     */
370
    private static function replacePlaceholders(array $placeholders, string $contents): string
371
    {
372
        return str_replace(
373
            array_keys($placeholders),
374
            array_values($placeholders),
375
            $contents
376
        );
377
    }
378
379
    private static function compactContents(array $compactors, string $file, string $contents): string
380
    {
381
        return array_reduce(
382
            $compactors,
383
            function (string $contents, Compactor $compactor) use ($file): string {
384
                return $compactor->compact($file, $contents);
385
            },
386
            $contents
387
        );
388
    }
389
390
    /**
391
     * Dumps the files added to the PHAR into a directory at the project level to allow the user to easily have a look.
392
     * At this point only the main script should have already been registered into the dump target.
393
     */
394
    private function dumpFiles(string $debugDir, string $tmp): void
395
    {
396
        $mainScript = current(
397
            array_filter(
398
                scandir($debugDir, 1),
399
                function (string $file): bool {
400
                    return false === in_array($file, ['.', '..'], true);
401
                }
402
            )
403
        );
404
405
        $sourceMainScript = $debugDir.DIRECTORY_SEPARATOR.$mainScript;
406
        $targetMainScript = $tmp.DIRECTORY_SEPARATOR.$mainScript;
407
408
        if (is_file($mainScript)) {
409
            copy($sourceMainScript, $targetMainScript);
410
        } else {
411
            rename($sourceMainScript, $targetMainScript);
412
        }
413
414
        rename($tmp, $debugDir, true);
415
    }
416
}
417