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

Box::processContents()   B

Complexity

Conditions 3
Paths 2

Size

Total Lines 31
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

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

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