Passed
Pull Request — master (#48)
by Théo
02:13
created

Box::addFile()   B

Complexity

Conditions 4
Paths 8

Size

Total Lines 27
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

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