Completed
Pull Request — master (#44)
by Théo
02:06
created

Box::registerFileMapping()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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