Completed
Pull Request — master (#199)
by Théo
04:05 queued 01:24
created

Box::startBuffering()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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

325
        $details = openssl_pkey_get_details(/** @scrutinizer ignore-type */ $resource);
Loading history...
326
327
        $this->phar->setSignatureAlgorithm(Phar::OPENSSL, $private);
328
329
        dump_file($pubKey, $details['key']);
330
    }
331
332
    /**
333
     * @param string[] $files
334
     *
335
     * @return array array of tuples where the first element is the local file path (path inside the PHAR) and the
336
     *               second element is the processed contents
337
     */
338
    private function processContents(array $files): array
339
    {
340
        $basePath = $this->basePath;
341
        $mapFile = $this->mapFile;
342
        $placeholders = $this->placeholders;
343
        $compactors = $this->compactors;
344
        $bootstrap = $GLOBALS['_BOX_BOOTSTRAP'] ?? function (): void {};
345
        $cwd = getcwd();
346
347
        $processFile = function (string $file) use ($cwd, $basePath, $mapFile, $placeholders, $compactors, $bootstrap): array {
348
            chdir($cwd);
349
            $bootstrap();
350
351
            $contents = file_contents($file);
352
353
            $relativePath = make_path_relative($file, $basePath);
354
            $local = $mapFile($relativePath);
355
356
            if (null === $local) {
357
                $local = $relativePath;
358
            }
359
360
            $processedContents = self::compactContents(
361
                $compactors,
362
                $local,
363
                self::replacePlaceholders($placeholders, $contents)
364
            );
365
366
            return [$local, $processedContents];
367
        };
368
369
        return is_parallel_processing_enabled() && false === ($this->scoper instanceof NullScoper)
370
            ? wait(parallelMap($files, $processFile))
371
            : array_map($processFile, $files)
372
        ;
373
    }
374
375
    /**
376
     * Replaces the placeholders with their values.
377
     *
378
     * @param array  $placeholders
379
     * @param string $contents     the contents
380
     *
381
     * @return string the replaced contents
382
     */
383
    private static function replacePlaceholders(array $placeholders, string $contents): string
384
    {
385
        return str_replace(
386
            array_keys($placeholders),
387
            array_values($placeholders),
388
            $contents
389
        );
390
    }
391
392
    private static function compactContents(array $compactors, string $file, string $contents): string
393
    {
394
        return array_reduce(
395
            $compactors,
396
            function (string $contents, Compactor $compactor) use ($file): string {
397
                return $compactor->compact($file, $contents);
398
            },
399
            $contents
400
        );
401
    }
402
}
403