Completed
Push — master ( b9296c...da4eab )
by dotzero
02:59
created

Burgomaster::deepCopy()   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 19
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 19
rs 8.8571
cc 5
eloc 10
nc 6
nop 2
1
<?php
2
3
/**
4
 * Packages the zip and phar file using a staging directory.
5
 *
6
 * @license MIT, Michael Dowling https://github.com/mtdowling
7
 * @license https://github.com/mtdowling/Burgomaster/LICENSE
8
 */
9
class Burgomaster
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
10
{
11
    /** @var string Base staging directory of the project */
12
    public $stageDir;
13
14
    /** @var string Root directory of the project */
15
    public $projectRoot;
16
17
    /** @var array stack of sections */
18
    private $sections = array();
19
20
    /**
21
     * @param string $stageDir    Staging base directory where your packaging
22
     *                            takes place. This folder will be created for
23
     *                            you if it does not exist. If it exists, it
24
     *                            will be deleted and recreated to start fresh.
25
     * @param string $projectRoot Root directory of the project.
26
     *
27
     * @throws \InvalidArgumentException
28
     * @throws \RuntimeException
29
     */
30
    public function __construct($stageDir, $projectRoot = null)
31
    {
32
        $this->startSection('setting_up');
33
        $this->stageDir = $stageDir;
34
        $this->projectRoot = $projectRoot;
35
36
        if (!$this->stageDir || $this->stageDir == '/') {
37
            throw new \InvalidArgumentException('Invalid base directory');
38
        }
39
40
        if (is_dir($this->stageDir)) {
41
            $this->debug("Removing existing directory: $this->stageDir");
42
            echo $this->exec("rm -rf $this->stageDir");
43
        }
44
45
        $this->debug("Creating staging directory: $this->stageDir");
46
47
        if (!mkdir($this->stageDir, 0775, true)) {
48
            throw new \RuntimeException("Could not create {$this->stageDir}");
49
        }
50
51
        $this->stageDir = realpath($this->stageDir);
52
        $this->debug("Creating staging directory at: {$this->stageDir}");
53
54
        if (!is_dir($this->projectRoot)) {
55
            throw new \InvalidArgumentException(
56
                "Project root not found: $this->projectRoot"
57
            );
58
        }
59
60
        $this->endSection();
61
        $this->startSection('staging');
62
63
        chdir($this->projectRoot);
64
    }
65
66
    /**
67
     * Cleanup if the last section was not already closed.
68
     */
69
    public function __destruct()
70
    {
71
        if ($this->sections) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->sections of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
72
            $this->endSection();
73
        }
74
    }
75
76
    /**
77
     * Call this method when starting a specific section of the packager.
78
     *
79
     * This makes the debug messages used in your script more meaningful and
80
     * adds context when things go wrong. Be sure to call endSection() when
81
     * you have finished a section of your packaging script.
82
     *
83
     * @param string $section Part of the packager that is running
84
     */
85
    public function startSection($section)
86
    {
87
        $this->sections[] = $section;
88
        $this->debug('Starting');
89
    }
90
91
    /**
92
     * Call this method when leaving the last pushed section of the packager.
93
     */
94
    public function endSection()
95
    {
96
        if ($this->sections) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->sections of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
97
            $this->debug('Completed');
98
            array_pop($this->sections);
99
        }
100
    }
101
102
    /**
103
     * Prints a debug message to STDERR bound to the current section.
104
     *
105
     * @param string $message Message to echo to STDERR
106
     */
107
    public function debug($message)
108
    {
109
        $prefix = date('c') . ': ';
110
111
        if ($this->sections) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->sections of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
112
            $prefix .= '[' . end($this->sections) . '] ';
113
        }
114
115
        fwrite(STDERR, $prefix . $message . "\n");
116
    }
117
118
    /**
119
     * Copies a file and creates the destination directory if needed.
120
     *
121
     * @param string $from File to copy
122
     * @param string $to   Destination to copy the file to, relative to the
123
     *                     base staging directory.
124
     * @throws \InvalidArgumentException if the file cannot be found
125
     * @throws \RuntimeException if the directory cannot be created.
126
     * @throws \RuntimeException if the file cannot be copied.
127
     */
128
    public function deepCopy($from, $to)
129
    {
130
        if (!is_file($from)) {
131
            throw new \InvalidArgumentException("File not found: {$from}");
132
        }
133
134
        $to = str_replace('//', '/', $this->stageDir . '/' . $to);
135
        $dir = dirname($to);
136
137
        if (!is_dir($dir)) {
138
            if (!mkdir($dir, 0777, true)) {
139
                throw new \RuntimeException("Unable to create directory: $dir");
140
            }
141
        }
142
143
        if (!copy($from, $to)) {
144
            throw new \RuntimeException("Unable to copy $from to $to");
145
        }
146
    }
147
148
    /**
149
     * Recursively copy one folder to another.
150
     *
151
     * Any LICENSE file is automatically copied.
152
     *
153
     * @param string $sourceDir  Source directory to copy from
154
     * @param string $destDir    Directory to copy the files to that is relative
155
     *                           to the the stage base directory.
156
     * @param array  $extensions File extensions to copy from the $sourceDir.
157
     *                           Defaults to "php" files only (e.g., ['php']).
158
     * @throws \InvalidArgumentException if the source directory is invalid.
159
     */
160
    function recursiveCopy(
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
161
        $sourceDir,
162
        $destDir,
163
        $extensions = array('php')
164
    ) {
165
        if (!realpath($sourceDir)) {
166
            throw new \InvalidArgumentException("$sourceDir not found");
167
        }
168
169
        if (!$extensions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $extensions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
170
            throw new \InvalidArgumentException('$extensions is empty!');
171
        }
172
173
        $sourceDir = realpath($sourceDir);
174
        $exts = array_fill_keys($extensions, true);
175
        $iter = new \RecursiveDirectoryIterator($sourceDir);
176
        $iter = new \RecursiveIteratorIterator($iter);
177
        $total = 0;
178
179
        $this->startSection('copy');
180
        $this->debug("Starting to copy files from $sourceDir");
181
182
        foreach ($iter as $file) {
183
            if (isset($exts[$file->getExtension()])
184
                || $file->getBaseName() == 'LICENSE'
185
            ) {
186
                // Remove the source directory from the destination path
187
                $toPath = str_replace($sourceDir, '', (string) $file);
188
                $toPath = $destDir . '/' . $toPath;
189
                $toPath = str_replace('//', '/', $toPath);
190
                $this->deepCopy((string) $file, $toPath);
191
                $total++;
192
            }
193
        }
194
195
        $this->debug("Copied $total files from $sourceDir");
196
        $this->endSection();
197
    }
198
199
    /**
200
     * Execute a command and throw an exception if the return code is not 0.
201
     *
202
     * @param string $command Command to execute
203
     *
204
     * @return string Returns the output of the command as a string
205
     * @throws \RuntimeException on error.
206
     */
207
    public function exec($command)
208
    {
209
        $this->debug("Executing: $command");
210
        $output = $returnValue = null;
211
        exec($command, $output, $returnValue);
212
213
        if ($returnValue != 0) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $returnValue of type integer|null to 0; this is ambiguous as not only 0 == 0 is true, but null == 0 is true, too. Consider using a strict comparison ===.
Loading history...
214
            throw new \RuntimeException('Error executing command: '
215
                . $command . ' : ' . implode("\n", $output));
216
        }
217
218
        return implode("\n", $output);
219
    }
220
221
    /**
222
     * Creates a class-map autoloader to the staging directory in a file
223
     * named autoloader.php
224
     *
225
     * @param array $files Files to explicitly require in the autoloader. This
226
     *                     is similar to Composer's "files" "autoload" section.
227
     * @param string $filename Name of the autoloader file.
228
     * @throws \RuntimeException if the file cannot be written
229
     */
230
    function createAutoloader($files = array(), $filename = 'autoloader.php') {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
231
        $sourceDir = realpath($this->stageDir);
232
        $iter = new \RecursiveDirectoryIterator($sourceDir);
233
        $iter = new \RecursiveIteratorIterator($iter);
234
235
        $this->startSection('autoloader');
236
        $this->debug('Creating classmap autoloader');
237
        $this->debug("Collecting valid PHP files from {$this->stageDir}");
238
239
        $classMap = array();
240
        foreach ($iter as $file) {
241
            if ($file->getExtension() == 'php') {
242
                $location = str_replace($this->stageDir . '/', '', (string) $file);
243
                $className = str_replace('/', '\\', $location);
244
                $className = substr($className, 0, -4);
245
246
                // Remove "src\" or "lib\"
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
247
                if (strpos($className, 'src\\') === 0
248
                    || strpos($className, 'lib\\') === 0
249
                ) {
250
                    $className = substr($className, 4);
251
                }
252
253
                $classMap[$className] = "__DIR__ . '/$location'";
254
                $this->debug("Found $className");
255
            }
256
        }
257
258
        $destFile = $this->stageDir . '/' . $filename;
259
        $this->debug("Writing autoloader to {$destFile}");
260
261
        if (!($h = fopen($destFile, 'w'))) {
262
            throw new \RuntimeException('Unable to open file for writing');
263
        }
264
265
        $this->debug('Writing classmap files');
266
        fwrite($h, "<?php\n\n");
267
        fwrite($h, "\$mapping = array(\n");
268
        foreach ($classMap as $c => $f) {
269
            fwrite($h, "    '$c' => $f,\n");
270
        }
271
        fwrite($h, ");\n\n");
272
        fwrite($h, <<<EOT
273
spl_autoload_register(function (\$class) use (\$mapping) {
274
    if (isset(\$mapping[\$class])) {
275
        require \$mapping[\$class];
276
    }
277
}, true);
278
279
EOT
280
        );
281
282
        fwrite($h, "\n");
283
284
        $this->debug('Writing automatically included files');
285
        foreach ($files as $file) {
286
            fwrite($h, "require __DIR__ . '/$file';\n");
287
        }
288
289
        fclose($h);
290
291
        $this->endSection();
292
    }
293
294
    /**
295
     * Creates a default stub for the phar that includeds the generated
296
     * autoloader.
297
     *
298
     * This phar also registers a constant that can be used to check if you
299
     * are running the phar. The constant is the basename of the $dest variable
300
     * without the extension, with "_PHAR" appended, then converted to all
301
     * caps (e.g., "/foo/guzzle.phar" gets a contant defined as GUZZLE_PHAR.
302
     *
303
     * @param $dest
304
     * @param string $autoloaderFilename Name of the autoloader file.
305
     * @param string $alias The phar alias to use
306
     *
307
     * @return string
308
     */
309
    private function createStub($dest, $autoloaderFilename = 'autoloader.php', $alias = null)
310
    {
311
        $this->startSection('stub');
312
        $this->debug("Creating phar stub at $dest");
313
314
        $alias = $alias ?: basename($dest);
315
        $constName = strtoupper(str_replace('.phar', '', $alias)) . '_PHAR';
316
        $stub  = "<?php\n";
317
        $stub .= "define('$constName', true);\n";
318
        $stub .= "Phar::mapPhar('$alias');\n";
319
        $stub .= "require 'phar://$alias/{$autoloaderFilename}';\n";
320
        $stub .= "__HALT_COMPILER();\n";
321
        $this->endSection();
322
323
        return $stub;
324
    }
325
326
    /**
327
     * Creates a phar that automatically registers an autoloader.
328
     *
329
     * Call this only after your staging directory is built.
330
     *
331
     * @param string $dest Where to save the file. The basename of the file
332
     *     is also used as the alias name in the phar
333
     *     (e.g., /path/to/guzzle.phar => guzzle.phar).
334
     * @param string|bool|null $stub The path to the phar stub file. Pass or
335
     *      leave null to automatically have one created for you. Pass false
336
     *      to no use a stub in the generated phar.
337
     * @param string $autoloaderFilename Name of the autolaoder filename.
338
     */
339
    public function createPhar(
340
        $dest,
341
        $stub = null,
342
        $autoloaderFilename = 'autoloader.php',
343
        $alias = null
344
    ) {
345
        $this->startSection('phar');
346
        $this->debug("Creating phar file at $dest");
347
        $this->createDirIfNeeded(dirname($dest));
348
        $phar = new \Phar($dest, 0, $alias ?: basename($dest));
349
        $phar->buildFromDirectory($this->stageDir);
350
351
        if ($stub !== false) {
352
            if (!$stub) {
353
                $stub = $this->createStub($dest, $autoloaderFilename, $alias);
354
            }
355
            $phar->setStub($stub);
356
        }
357
358
        $this->debug("Created phar at $dest");
359
        $this->endSection();
360
    }
361
362
    /**
363
     * Creates a zip file containing the staged files of your project.
364
     *
365
     * Call this only after your staging directory is built.
366
     *
367
     * @param string $dest Where to save the zip file
368
     */
369
    public function createZip($dest)
370
    {
371
        $this->startSection('zip');
372
        $this->debug("Creating a zip file at $dest");
373
        $this->createDirIfNeeded(dirname($dest));
374
        chdir($this->stageDir);
375
        $this->exec("zip -r $dest ./");
376
        $this->debug("  > Created at $dest");
377
        chdir(__DIR__);
378
        $this->endSection();
379
    }
380
381
    private function createDirIfNeeded($dir)
382
    {
383
        if (!is_dir($dir) && !mkdir($dir, 0755, true)) {
384
            throw new \RuntimeException("Could not create dir: $dir");
385
        }
386
    }
387
}
388