Passed
Push — master ( 8c30ae...ca7377 )
by Théo
02:07
created

Box::registerPlaceholders()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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