Completed
Push — master ( 8bc9d2...ec70be )
by Márk
01:58
created

build/Burgomaster.php (9 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
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, 0777, 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 View Code Duplication
        if (!is_dir($dir)) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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
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
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
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\"
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
     *
306
     * @return string
307
     */
308
    private function createStub($dest, $autoloaderFilename = 'autoloader.php')
309
    {
310
        $this->startSection('stub');
311
        $this->debug("Creating phar stub at $dest");
312
313
        $alias = basename($dest);
314
        $constName = str_replace('.phar', '', strtoupper($alias)) . '_PHAR';
315
        $stub  = "<?php\n";
316
        $stub .= "define('$constName', true);\n";
317
        $stub .= "require 'phar://$alias/{$autoloaderFilename}';\n";
318
        $stub .= "__HALT_COMPILER();\n";
319
        $this->endSection();
320
321
        return $stub;
322
    }
323
324
    /**
325
     * Creates a phar that automatically registers an autoloader.
326
     *
327
     * Call this only after your staging directory is built.
328
     *
329
     * @param string $dest Where to save the file. The basename of the file
330
     *     is also used as the alias name in the phar
331
     *     (e.g., /path/to/guzzle.phar => guzzle.phar).
332
     * @param string|bool|null $stub The path to the phar stub file. Pass or
333
     *      leave null to automatically have one created for you. Pass false
334
     *      to no use a stub in the generated phar.
335
     * @param string $autoloaderFilename Name of the autolaoder filename.
336
     */
337
    public function createPhar(
338
        $dest,
339
        $stub = null,
340
        $autoloaderFilename = 'autoloader.php'
341
    ) {
342
        $this->startSection('phar');
343
        $this->debug("Creating phar file at $dest");
344
        $this->createDirIfNeeded(dirname($dest));
345
        $phar = new \Phar($dest, 0, basename($dest));
346
        $phar->buildFromDirectory($this->stageDir);
347
348
        if ($stub !== false) {
349
            if (!$stub) {
350
                $stub = $this->createStub($dest, $autoloaderFilename);
351
            }
352
            $phar->setStub($stub);
353
        }
354
355
        $this->debug("Created phar at $dest");
356
        $this->endSection();
357
    }
358
359
    /**
360
     * Creates a zip file containing the staged files of your project.
361
     *
362
     * Call this only after your staging directory is built.
363
     *
364
     * @param string $dest Where to save the zip file
365
     */
366
    public function createZip($dest)
367
    {
368
        $this->startSection('zip');
369
        $this->debug("Creating a zip file at $dest");
370
        $this->createDirIfNeeded(dirname($dest));
371
        chdir($this->stageDir);
372
        $this->exec("zip -r $dest ./");
373
        $this->debug("  > Created at $dest");
374
        chdir(__DIR__);
375
        $this->endSection();
376
    }
377
378
    private function createDirIfNeeded($dir)
379
    {
380 View Code Duplication
        if (!is_dir($dir) && !mkdir($dir, 0755, true)) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
381
            throw new \RuntimeException("Could not create dir: $dir");
382
        }
383
    }
384
}
385