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

Box::compactContents()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 5
nc 1
nop 3
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 function chdir;
19
use Composer\Console\Application as ComposerApplication;
20
use function getcwd;
21
use Phar;
22
use RecursiveDirectoryIterator;
23
use SplFileInfo;
24
use function Amp\ParallelFunctions\parallelMap;
25
use function Amp\Promise\wait;
26
use function KevinGH\Box\FileSystem\dump_file;
27
use function KevinGH\Box\FileSystem\file_contents;
28
use function KevinGH\Box\FileSystem\make_path_absolute;
29
use function KevinGH\Box\FileSystem\make_tmp_dir;
30
use function KevinGH\Box\FileSystem\mkdir;
31
use function KevinGH\Box\FileSystem\remove;
32
use Symfony\Component\Console\Input\ArrayInput;
33
use Symfony\Component\Console\Output\NullOutput;
34
35
/**
36
 * Box is a utility class to generate a PHAR.
37
 */
38
final class Box
39
{
40
    /**
41
     * @var Compactor[]
42
     */
43
    private $compactors = [];
44
45
    /**
46
     * @var string The path to the PHAR file
47
     */
48
    private $file;
49
50
    /**
51
     * @var Phar The PHAR instance
52
     */
53
    private $phar;
54
55
    /**
56
     * @var scalar[] The placeholders with their values
57
     */
58
    private $placeholders = [];
59
60
    /**
61
     * @var RetrieveRelativeBasePath
62
     */
63
    private $retrieveRelativeBasePath;
64
65
    /**
66
     * @var MapFile
67
     */
68
    private $mapFile;
69
70
    private function __construct(Phar $phar, string $file)
71
    {
72
        $this->phar = $phar;
73
        $this->file = $file;
74
75
        $this->retrieveRelativeBasePath = function (string $path) { return $path; };
0 ignored issues
show
Documentation Bug introduced by
It seems like function(...) { /* ... */ } of type callable is incompatible with the declared type KevinGH\Box\RetrieveRelativeBasePath of property $retrieveRelativeBasePath.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
76
        $this->mapFile = function (): void { };
77
    }
78
79
    /**
80
     * Creates a new PHAR and Box instance.
81
     *
82
     * @param string $file  The PHAR file name
83
     * @param int    $flags Flags to pass to the Phar parent class RecursiveDirectoryIterator
84
     * @param string $alias Alias with which the Phar archive should be referred to in calls to stream functionality
85
     *
86
     * @return Box
87
     *
88
     * @see RecursiveDirectoryIterator
89
     */
90
    public static function create(string $file, int $flags = null, string $alias = null): self
91
    {
92
        // Ensure the parent directory of the PHAR file exists as `new \Phar()` does not create it and would fail
93
        // otherwise.
94
        mkdir(dirname($file));
95
96
        return new self(new Phar($file, (int) $flags, $alias), $file);
97
    }
98
99
    /**
100
     * @param Compactor[] $compactors
101
     */
102
    public function registerCompactors(array $compactors): void
103
    {
104
        Assertion::allIsInstanceOf($compactors, Compactor::class);
105
106
        $this->compactors = $compactors;
107
    }
108
109
    /**
110
     * @param scalar[] $placeholders
111
     */
112
    public function registerPlaceholders(array $placeholders): void
113
    {
114
        $message = 'Expected value "%s" to be a scalar or stringable object.';
115
116
        foreach ($placeholders as $i => $placeholder) {
117
            if (is_object($placeholder)) {
118
                Assertion::methodExists('__toString', $placeholder, $message);
119
120
                $placeholders[$i] = (string) $placeholder;
121
122
                break;
123
            }
124
125
            Assertion::scalar($placeholder, $message);
126
        }
127
128
        $this->placeholders = $placeholders;
129
    }
130
131
    public function registerFileMapping(RetrieveRelativeBasePath $retrieveRelativeBasePath, MapFile $fileMapper): void
132
    {
133
        $this->retrieveRelativeBasePath = $retrieveRelativeBasePath;
134
        $this->mapFile = $fileMapper;
135
    }
136
137
    public function registerStub(string $file): void
138
    {
139
        $contents = self::replacePlaceholders(
140
            $this->placeholders,
141
            file_contents($file)
142
        );
143
144
        $this->phar->setStub($contents);
145
    }
146
147
    /**
148
     * @param SplFileInfo[]|string[] $files
149
     * @param bool                   $binary
150
     */
151
    public function addFiles(array $files, bool $binary): void
152
    {
153
        if ($binary) {
154
            foreach ($files as $file) {
155
                $this->addFile((string) $file, null, $binary);
156
            }
157
158
            return;
159
        }
160
161
        $tmp = make_tmp_dir('box', __CLASS__);
162
163
        $fileWithContents = $this->processContents(
164
            array_map(
165
                function ($file): string {
166
                    // Convert files to string as SplFileInfo is not serializable
167
                    return (string) $file;
168
                },
169
                $files
170
            )
171
        );
172
173
        try {
174
            foreach ($fileWithContents as $fileWithContents) {
175
                [$file, $contents] = $fileWithContents;
176
177
                dump_file(
178
                    make_path_absolute($file, $tmp),
179
                    $contents
180
                );
181
            }
182
183
            $cwd = getcwd();
184
            chdir($tmp);
185
186
            $this->dumpAutoload();  // Dump autoload without dev dependencies
187
188
            chdir($cwd);
189
190
            $this->phar->buildFromDirectory($tmp);
191
        } finally {
192
            remove($tmp);
193
        }
194
    }
195
196
    /**
197
     * Adds the a file to the PHAR. The contents will first be compacted and have its placeholders
198
     * replaced.
199
     *
200
     * @param string      $file
201
     * @param null|string $contents If null the content of the file will be used
202
     * @param bool        $binary   When true means the file content shouldn't be processed
203
     *
204
     * @return string File local path
205
     */
206
    public function addFile(string $file, string $contents = null, bool $binary = false): string
207
    {
208
        Assertion::file($file);
209
        Assertion::readable($file);
210
211
        $contents = null === $contents ? file_get_contents($file) : $contents;
212
213
        $relativePath = ($this->retrieveRelativeBasePath)($file);
214
        $local = ($this->mapFile)($relativePath);
215
216
        if (null === $local) {
217
            $local = $relativePath;
218
        }
219
220
        if ($binary) {
221
            $this->phar->addFile($file, $local);
222
        } else {
223
            $processedContents = self::compactContents(
224
                $this->compactors,
225
                $local,
226
                self::replacePlaceholders($this->placeholders, $contents)
227
            );
228
229
            $this->phar->addFromString($local, $processedContents);
230
        }
231
232
        return $local;
233
    }
234
235
    public function getPhar(): Phar
236
    {
237
        return $this->phar;
238
    }
239
240
    /**
241
     * Signs the PHAR using a private key file.
242
     *
243
     * @param string $file     the private key file name
244
     * @param string $password the private key password
245
     */
246
    public function signUsingFile(string $file, string $password = null): void
247
    {
248
        $this->sign(file_contents($file), $password);
249
    }
250
251
    /**
252
     * Signs the PHAR using a private key.
253
     *
254
     * @param string $key      The private key
255
     * @param string $password The private key password
256
     */
257
    public function sign(string $key, ?string $password): void
258
    {
259
        $pubKey = $this->file.'.pubkey';
260
261
        Assertion::writeable(dirname($pubKey));
262
        Assertion::extensionLoaded('openssl');
263
264
        if (file_exists($pubKey)) {
265
            Assertion::file(
266
                $pubKey,
267
                'Cannot create public key: "%s" already exists and is not a file.'
268
            );
269
        }
270
271
        $resource = openssl_pkey_get_private($key, (string) $password);
272
273
        openssl_pkey_export($resource, $private);
274
275
        $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

275
        $details = openssl_pkey_get_details(/** @scrutinizer ignore-type */ $resource);
Loading history...
276
277
        $this->phar->setSignatureAlgorithm(Phar::OPENSSL, $private);
278
279
        dump_file($pubKey, $details['key']);
280
    }
281
282
    /**
283
     * @param string[] $files
284
     *
285
     * @return array array of tuples where the first element is the local file path (path inside the PHAR) and the
286
     *               second element is the processed contents
287
     */
288
    private function processContents(array $files): array
289
    {
290
        $retrieveRelativeBasePath = $this->retrieveRelativeBasePath;
291
        $mapFile = $this->mapFile;
292
        $placeholders = $this->placeholders;
293
        $compactors = $this->compactors;
294
295
        $processFile = function (string $file) use ($retrieveRelativeBasePath, $mapFile, $placeholders, $compactors): array {
296
            $contents = file_contents($file);
297
298
            $relativePath = $retrieveRelativeBasePath($file);
299
            $local = $mapFile($relativePath);
300
301
            if (null === $local) {
302
                $local = $relativePath;
303
            }
304
305
            $processedContents = self::compactContents(
306
                $compactors,
307
                $local,
308
                self::replacePlaceholders($placeholders, $contents)
309
            );
310
311
            return [$local, $processedContents];
312
        };
313
314
        return wait(parallelMap($files, $processFile));
315
    }
316
317
    /**
318
     * Replaces the placeholders with their values.
319
     *
320
     * @param array  $placeholders
321
     * @param string $contents     the contents
322
     *
323
     * @return string the replaced contents
324
     */
325
    private static function replacePlaceholders(array $placeholders, string $contents): string
326
    {
327
        return str_replace(
328
            array_keys($placeholders),
329
            array_values($placeholders),
330
            $contents
331
        );
332
    }
333
334
    private static function compactContents(array $compactors, string $file, string $contents): string
335
    {
336
        return array_reduce(
337
            $compactors,
338
            function (string $contents, Compactor $compactor) use ($file): string {
339
                return $compactor->compact($file, $contents);
340
            },
341
            $contents
342
        );
343
    }
344
345
    private function dumpAutoload(): void
346
    {
347
        $composerApplication = new ComposerApplication();
348
        $composerApplication->doRun(new ArrayInput([]), new NullOutput());
349
350
        $composer = $composerApplication->getComposer();
351
        $installationManager = $composer->getInstallationManager();
352
        $localRepo = $composer->getRepositoryManager()->getLocalRepository();
353
        $package = $composer->getPackage();
354
        $config = $composer->getConfig();
355
356
        $generator = $composer->getAutoloadGenerator();
357
        $generator->setDevMode(false);
358
        $generator->setClassMapAuthoritative(true);
359
360
        $generator->dump($config, $localRepo, $package, $installationManager, 'composer', true);
361
    }
362
}
363