Completed
Pull Request — master (#108)
by Théo
03:03
created

Box::dumpFiles()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 21
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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

302
        $details = openssl_pkey_get_details(/** @scrutinizer ignore-type */ $resource);
Loading history...
303
304
        $this->phar->setSignatureAlgorithm(Phar::OPENSSL, $private);
305
306
        dump_file($pubKey, $details['key']);
307
    }
308
309
    /**
310
     * @param string[] $files
311
     * @param string   $cwd   Current working directory. As the processes are spawned for parallel processing, the
312
     *                        working directory may change so we pass the working directory in which the processing
313
     *                        is supposed to happen. This should not happen during regular usage as all the files are
314
     *                        absolute but it's possible this class is used with relative paths in which case this is
315
     *                        an issue.
316
     *
317
     * @return array array of tuples where the first element is the local file path (path inside the PHAR) and the
318
     *               second element is the processed contents
319
     */
320
    private function processContents(array $files, string $cwd): array
321
    {
322
        $basePath = $this->basePath;
323
        $mapFile = $this->mapFile;
324
        $placeholders = $this->placeholders;
325
        $compactors = $this->compactors;
326
        $bootstrap = $GLOBALS['bootstrap'] ?? function (): void {};
327
328
        $processFile = function (string $file) use ($cwd, $basePath, $mapFile, $placeholders, $compactors, $bootstrap): array {
329
            chdir($cwd);
330
            $bootstrap();
331
332
            $contents = file_contents($file);
333
334
            $relativePath = make_path_relative($file, $basePath);
335
            $local = $mapFile($relativePath);
336
337
            if (null === $local) {
338
                $local = $relativePath;
339
            }
340
341
            $processedContents = self::compactContents(
342
                $compactors,
343
                $local,
344
                self::replacePlaceholders($placeholders, $contents)
345
            );
346
347
            return [$local, $processedContents];
348
        };
349
350
        return is_debug_enabled()
351
            ? array_map($processFile, $files)
352
            : wait(parallelMap($files, $processFile))
353
        ;
354
    }
355
356
    /**
357
     * Replaces the placeholders with their values.
358
     *
359
     * @param array  $placeholders
360
     * @param string $contents     the contents
361
     *
362
     * @return string the replaced contents
363
     */
364
    private static function replacePlaceholders(array $placeholders, string $contents): string
365
    {
366
        return str_replace(
367
            array_keys($placeholders),
368
            array_values($placeholders),
369
            $contents
370
        );
371
    }
372
373
    private static function compactContents(array $compactors, string $file, string $contents): string
374
    {
375
        return array_reduce(
376
            $compactors,
377
            function (string $contents, Compactor $compactor) use ($file): string {
378
                return $compactor->compact($file, $contents);
379
            },
380
            $contents
381
        );
382
    }
383
}
384