Box   B
last analyzed

Complexity

Total Complexity 48

Size/Duplication

Total Lines 448
Duplicated Lines 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
wmc 48
eloc 172
dl 0
loc 448
rs 8.5599
c 1
b 1
f 0

24 Methods

Rating   Name   Duplication   Size   Complexity  
A getPhar() 0 3 1
A addFile() 0 13 3
A startBuffering() 0 7 1
A removeComposerArtifacts() 0 27 3
A signUsingKey() 0 25 2
A processContents() 0 18 3
A setMetadata() 0 3 1
A registerCompactors() 0 20 4
A processFilesSynchronously() 0 20 1
A sign() 0 22 2
A setAlias() 0 9 1
B compress() 0 34 6
A registerStub() 0 8 1
A registerPlaceholders() 0 19 3
A create() 0 14 1
A registerFileMapping() 0 3 1
A setDefaultStub() 0 3 1
A addFiles() 0 16 4
A setStub() 0 3 1
A endBuffering() 0 39 4
A extractTo() 0 3 1
A signUsingFile() 0 3 1
A __construct() 0 9 1
A count() 0 5 1

How to fix   Complexity   

Complex Class

Complex classes like Box often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Box, and based on these observations, apply Extract Interface, too.

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 Amp\Parallel\Worker\TaskFailureThrowable;
18
use BadMethodCallException;
19
use Countable;
20
use DateTimeImmutable;
21
use Fidry\FileSystem\FS;
22
use Humbug\PhpScoper\Symbol\SymbolsRegistry;
23
use KevinGH\Box\Compactor\Compactors;
24
use KevinGH\Box\Compactor\PhpScoper;
25
use KevinGH\Box\Compactor\Placeholder;
26
use KevinGH\Box\Parallelization\ParallelFileProcessor;
27
use KevinGH\Box\Parallelization\ParallelizationDecider;
28
use KevinGH\Box\Phar\CompressionAlgorithm;
29
use KevinGH\Box\Phar\SigningAlgorithm;
30
use KevinGH\Box\PhpScoper\NullScoper;
31
use KevinGH\Box\PhpScoper\Scoper;
32
use Phar;
33
use RecursiveDirectoryIterator;
34
use RuntimeException;
35
use Seld\PharUtils\Timestamps;
36
use SplFileInfo;
37
use Webmozart\Assert\Assert;
38
use function array_map;
39
use function array_unshift;
40
use function chdir;
41
use function dirname;
42
use function extension_loaded;
43
use function file_exists;
44
use function getcwd;
45
use function is_object;
46
use function openssl_pkey_export;
47
use function openssl_pkey_get_details;
48
use function openssl_pkey_get_private;
49
use function sprintf;
50
51
/**
52
 * Box is a utility class to generate a PHAR.
53
 *
54
 * @private
55
 */
56
final class Box implements Countable
57
{
58
    private Compactors $compactors;
59
    private Placeholder $placeholderCompactor;
60
    private MapFile $mapFile;
61
    private Scoper $scoper;
62
    private bool $buffering = false;
63
64
    /**
65
     * @var array<string, string> Relative file path as key and file contents as value
66
     */
67
    private array $bufferedFiles = [];
68
69
    private function __construct(
70
        private Phar $phar,
71
        private readonly string $pharFilePath,
72
        private readonly bool $enableParallelization,
73
    ) {
74
        $this->compactors = new Compactors();
75
        $this->placeholderCompactor = new Placeholder([]);
76
        $this->mapFile = new MapFile(getcwd(), []);
77
        $this->scoper = new NullScoper();
78
    }
79
80
    /**
81
     * Creates a new PHAR and Box instance.
82
     *
83
     * @param string $pharFilePath The PHAR file name
84
     * @param int    $pharFlags    Flags to pass to the Phar parent class RecursiveDirectoryIterator
85
     * @param string $pharAlias    Alias with which the Phar archive should be referred to in calls to stream functionality
86
     *
87
     * @see RecursiveDirectoryIterator
88
     */
89
    public static function create(
90
        string $pharFilePath,
91
        int $pharFlags = 0,
92
        ?string $pharAlias = null,
93
        bool $enableParallelization = false,
94
    ): self {
95
        // Ensure the parent directory of the PHAR file exists as `new \Phar()` does not create it and would fail
96
        // otherwise.
97
        FS::mkdir(dirname($pharFilePath));
98
99
        return new self(
100
            new Phar($pharFilePath, $pharFlags, $pharAlias),
101
            $pharFilePath,
102
            $enableParallelization,
103
        );
104
    }
105
106
    public function startBuffering(): void
107
    {
108
        Assert::false($this->buffering, 'The buffering must be ended before starting it again');
109
110
        $this->buffering = true;
111
112
        $this->phar->startBuffering();
113
    }
114
115
    /**
116
     * @param callable(SymbolsRegistry, string): void $dumpAutoload
117
     */
118
    public function endBuffering(?callable $dumpAutoload): void
119
    {
120
        Assert::true($this->buffering, 'The buffering must be started before ending it');
121
122
        $dumpAutoload ??= static fn () => null;
123
        $cwd = getcwd();
124
125
        $tmp = FS::makeTmpDir('box', self::class);
126
        chdir($tmp);
127
128
        if ([] === $this->bufferedFiles) {
129
            $this->bufferedFiles = [
130
                '.box_empty' => 'A PHAR cannot be empty so Box adds this file to ensure the PHAR is created still.',
131
            ];
132
        }
133
134
        try {
135
            foreach ($this->bufferedFiles as $file => $contents) {
136
                FS::dumpFile($file, $contents);
137
            }
138
139
            if (null !== $dumpAutoload) {
0 ignored issues
show
introduced by
The condition null !== $dumpAutoload is always true.
Loading history...
140
                $dumpAutoload(
141
                    $this->scoper->getSymbolsRegistry(),
142
                    $this->scoper->getPrefix(),
143
                    $this->scoper->getExcludedFilePaths(),
0 ignored issues
show
Bug introduced by
The method getExcludedFilePaths() does not exist on KevinGH\Box\PhpScoper\Scoper. It seems like you code against a sub-type of said class. However, the method does not exist in KevinGH\Box\PhpScoper\FakeScoper. Are you sure you never get one of those? ( Ignorable by Annotation )

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

143
                    $this->scoper->/** @scrutinizer ignore-call */ 
144
                                   getExcludedFilePaths(),
Loading history...
144
                );
145
            }
146
147
            chdir($cwd);
148
149
            $this->phar->buildFromDirectory($tmp);
150
        } finally {
151
            FS::remove($tmp);
152
        }
153
154
        $this->buffering = false;
155
156
        $this->phar->stopBuffering();
157
    }
158
159
    /**
160
     * @param non-empty-string $normalizedVendorDir Normalized path ("/" path separator and no trailing "/") to the Composer vendor directory
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-string at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string.
Loading history...
161
     */
162
    public function removeComposerArtifacts(string $normalizedVendorDir): void
163
    {
164
        Assert::false($this->buffering, 'The buffering must have ended before removing the Composer artefacts');
165
166
        $composerArtifacts = [
167
            'composer.json',
168
            'composer.lock',
169
            $normalizedVendorDir.'/composer/installed.json',
170
        ];
171
172
        $this->phar->startBuffering();
173
174
        foreach ($composerArtifacts as $composerArtifact) {
175
            $localComposerArtifact = ($this->mapFile)($composerArtifact);
176
177
            $pharFilePath = sprintf(
178
                'phar://%s/%s',
179
                $this->phar->getPath(),
180
                $localComposerArtifact,
181
            );
182
183
            if (file_exists($pharFilePath)) {
184
                $this->phar->delete($localComposerArtifact);
185
            }
186
        }
187
188
        $this->phar->stopBuffering();
189
    }
190
191
    public function compress(CompressionAlgorithm $compressionAlgorithm): ?string
192
    {
193
        Assert::false($this->buffering, 'Cannot compress files while buffering.');
194
195
        $extensionRequired = $compressionAlgorithm->getRequiredExtension();
196
197
        if (null !== $extensionRequired && false === extension_loaded($extensionRequired)) {
198
            throw new RuntimeException(
199
                sprintf(
200
                    'Cannot compress the PHAR with the compression algorithm "%s": the extension "%s" is required but appear to not be loaded',
201
                    $compressionAlgorithm->name,
0 ignored issues
show
Bug introduced by
The property name does not seem to exist on KevinGH\Box\Phar\CompressionAlgorithm.
Loading history...
202
                    $extensionRequired,
203
                ),
204
            );
205
        }
206
207
        try {
208
            if (CompressionAlgorithm::NONE === $compressionAlgorithm) {
209
                $this->phar->decompressFiles();
210
            } else {
211
                $this->phar->compressFiles($compressionAlgorithm->value);
212
            }
213
        } catch (BadMethodCallException $exception) {
214
            $exceptionMessage = 'unable to create temporary file' !== $exception->getMessage()
215
                ? 'Could not compress the PHAR: '.$exception->getMessage()
216
                : sprintf(
217
                    'Could not compress the PHAR: the compression requires too many file descriptors to be opened (%s). Check your system limits or install the posix extension to allow Box to automatically configure it during the compression',
218
                    $this->phar->count(),
219
                );
220
221
            throw new RuntimeException($exceptionMessage, $exception->getCode(), $exception);
222
        }
223
224
        return $extensionRequired;
225
    }
226
227
    public function registerCompactors(Compactors $compactors): void
228
    {
229
        $compactorsArray = $compactors->toArray();
230
231
        foreach ($compactorsArray as $index => $compactor) {
232
            if ($compactor instanceof PhpScoper) {
233
                $this->scoper = $compactor->getScoper();
234
235
                continue;
236
            }
237
238
            if ($compactor instanceof Placeholder) {
239
                // Removes the known Placeholder compactors in favour of the Box one
240
                unset($compactorsArray[$index]);
241
            }
242
        }
243
244
        array_unshift($compactorsArray, $this->placeholderCompactor);
245
246
        $this->compactors = new Compactors(...$compactorsArray);
247
    }
248
249
    /**
250
     * @param scalar[] $placeholders
251
     */
252
    public function registerPlaceholders(array $placeholders): void
253
    {
254
        $message = 'Expected value "%s" to be a scalar or stringable object.';
255
256
        foreach ($placeholders as $index => $placeholder) {
257
            if (is_object($placeholder)) {
258
                Assert::methodExists($placeholder, '__toString', $message);
259
260
                $placeholders[$index] = (string) $placeholder;
261
262
                break;
263
            }
264
265
            Assert::scalar($placeholder, $message);
266
        }
267
268
        $this->placeholderCompactor = new Placeholder($placeholders);
269
270
        $this->registerCompactors($this->compactors);
271
    }
272
273
    public function registerFileMapping(MapFile $fileMapper): void
274
    {
275
        $this->mapFile = $fileMapper;
276
    }
277
278
    public function registerStub(string $file): void
279
    {
280
        $contents = $this->placeholderCompactor->compact(
281
            $file,
282
            FS::getFileContents($file),
283
        );
284
285
        $this->phar->setStub($contents);
286
    }
287
288
    /**
289
     * @param array<SplFileInfo|string> $files
290
     *
291
     * @throws TaskFailureThrowable
292
     */
293
    public function addFiles(array $files, bool $binary): void
294
    {
295
        Assert::true($this->buffering, 'Cannot add files if the buffering has not started.');
296
297
        $files = array_map('strval', $files);
298
299
        if ($binary) {
300
            foreach ($files as $file) {
301
                $this->addFile($file, null, true);
302
            }
303
304
            return;
305
        }
306
307
        foreach ($this->processContents($files) as [$file, $contents]) {
308
            $this->bufferedFiles[$file] = $contents;
309
        }
310
    }
311
312
    /**
313
     * Adds the a file to the PHAR. The contents will first be compacted and have its placeholders
314
     * replaced.
315
     *
316
     * @param null|string $contents If null the content of the file will be used
317
     * @param bool        $binary   When true means the file content shouldn't be processed
318
     *
319
     * @return string File local path
320
     */
321
    public function addFile(string $file, ?string $contents = null, bool $binary = false): string
322
    {
323
        Assert::true($this->buffering, 'Cannot add files if the buffering has not started.');
324
325
        if (null === $contents) {
326
            $contents = FS::getFileContents($file);
327
        }
328
329
        $local = ($this->mapFile)($file);
330
331
        $this->bufferedFiles[$local] = $binary ? $contents : $this->compactors->compact($local, $contents);
332
333
        return $local;
334
    }
335
336
    /**
337
     * @internal
338
     */
339
    public function getPhar(): Phar
340
    {
341
        return $this->phar;
342
    }
343
344
    public function setAlias(string $alias): void
345
    {
346
        $aliasWasAdded = $this->phar->setAlias($alias);
347
348
        Assert::true(
349
            $aliasWasAdded,
350
            sprintf(
351
                'The alias "%s" is invalid. See Phar::setAlias() documentation for more information.',
352
                $alias,
353
            ),
354
        );
355
    }
356
357
    public function setStub(string $stub): void
358
    {
359
        $this->phar->setStub($stub);
360
    }
361
362
    public function setDefaultStub(string $main): void
363
    {
364
        $this->phar->setDefaultStub($main);
365
    }
366
367
    public function setMetadata(mixed $metadata): void
368
    {
369
        $this->phar->setMetadata($metadata);
370
    }
371
372
    public function extractTo(string $directory, bool $overwrite = false): void
373
    {
374
        $this->phar->extractTo($directory, overwrite: $overwrite);
375
    }
376
377
    public function sign(
378
        SigningAlgorithm $signingAlgorithm,
379
        ?DateTimeImmutable $timestamp = null,
380
    ): void {
381
        if (null === $timestamp) {
382
            $this->phar->setSignatureAlgorithm($signingAlgorithm->value);
383
384
            return;
385
        }
386
387
        $phar = $this->phar;
388
        $phar->__destruct();
389
        unset($this->phar);
390
391
        $util = new Timestamps($this->pharFilePath);
392
        $util->updateTimestamps($timestamp);
393
        $util->save(
394
            $this->pharFilePath,
395
            $signingAlgorithm->value,
396
        );
397
398
        $this->phar = new Phar($this->pharFilePath);
399
    }
400
401
    /**
402
     * Signs the PHAR using a private key file.
403
     *
404
     * @param string      $file     the private key file name
405
     * @param null|string $password the private key password
406
     */
407
    public function signUsingFile(string $file, ?string $password = null): void
408
    {
409
        $this->signUsingKey(FS::getFileContents($file), $password);
410
    }
411
412
    /**
413
     * Signs the PHAR using a private key.
414
     *
415
     * @param string      $key      The private key
416
     * @param null|string $password The private key password
417
     */
418
    public function signUsingKey(string $key, ?string $password): void
419
    {
420
        $pubKey = $this->pharFilePath.'.pubkey';
421
422
        Assert::writable(dirname($pubKey));
423
        Assert::true(extension_loaded('openssl'));
424
425
        if (file_exists($pubKey)) {
426
            Assert::file(
427
                $pubKey,
428
                'Cannot create public key: %s already exists and is not a file.',
429
            );
430
        }
431
432
        $resource = openssl_pkey_get_private($key, (string) $password);
433
434
        Assert::notSame(false, $resource, 'Could not retrieve the private key, check that the password is correct.');
435
436
        openssl_pkey_export($resource, $private);
437
438
        $details = openssl_pkey_get_details($resource);
439
440
        $this->phar->setSignatureAlgorithm(Phar::OPENSSL, $private);
441
442
        FS::dumpFile($pubKey, $details['key']);
443
    }
444
445
    /**
446
     * @param string[] $files
447
     *
448
     * @return array array of tuples where the first element is the local file path (path inside the PHAR) and the
449
     *               second element is the processed contents
450
     */
451
    private function processContents(array $files): array
452
    {
453
        $cwd = getcwd();
454
455
        $shouldProcessFilesInParallel = $this->enableParallelization && ParallelizationDecider::shouldProcessFilesInParallel(
456
            $this->scoper,
457
            count($files),
458
        );
459
460
        $processFiles = $shouldProcessFilesInParallel
461
            ? ParallelFileProcessor::processFilesInParallel(...)
462
            : self::processFilesSynchronously(...);
463
464
        return $processFiles(
465
            $files,
466
            $cwd,
467
            $this->mapFile,
468
            $this->compactors,
469
        );
470
    }
471
472
    /**
473
     * @param string[] $files
474
     *
475
     * @return list<array{string, string}>
0 ignored issues
show
Bug introduced by
The type KevinGH\Box\list was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
476
     */
477
    private static function processFilesSynchronously(
0 ignored issues
show
Unused Code introduced by
The method processFilesSynchronously() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
478
        array $files,
479
        string $_cwd,
0 ignored issues
show
Unused Code introduced by
The parameter $_cwd is not used and could be removed. ( Ignorable by Annotation )

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

479
        /** @scrutinizer ignore-unused */ string $_cwd,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
480
        MapFile $mapFile,
481
        Compactors $compactors,
482
    ): array {
483
        $processFile = static function (string $file) use ($mapFile, $compactors): array {
484
            $contents = FS::getFileContents($file);
485
486
            $local = $mapFile($file);
487
488
            $processedContents = $compactors->compact($local, $contents);
489
490
            return [
491
                $local,
492
                $processedContents,
493
            ];
494
        };
495
496
        return array_map($processFile, $files);
0 ignored issues
show
Bug Best Practice introduced by
The expression return array_map($processFile, $files) returns the type array which is incompatible with the documented return type KevinGH\Box\list.
Loading history...
497
    }
498
499
    public function count(): int
500
    {
501
        Assert::false($this->buffering, 'Cannot count the number of files in the PHAR when buffering');
502
503
        return $this->phar->count();
504
    }
505
}
506