Completed
Pull Request — master (#11)
by Greg
02:34
created

Application::help()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 31
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 31
rs 8.8571
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 1
1
<?php
2
3
namespace Consolidation\Cgr;
4
5
/**
6
 * Note that this command is deliberately written using only php-native
7
 * libraries, and no external dependencies whatsoever, so that it may
8
 * be installed via `composer global require` without causing any conflicts
9
 * with any other project.
10
 *
11
 * This technique is NOT recommended for other tools. Use Symfony Console
12
 * directly, or, better yet, use Robo (http://robo.li) as a framework.
13
 * See: http://robo.li/framework/
14
 */
15
class Application
16
{
17
    protected $outputFile = '';
18
19
    /**
20
     * Run the cgr tool, a safer alternative to `composer global require`.
21
     *
22
     * @param array $argv The global $argv array passed in by PHP
23
     * @param string $home The path to the user's home directory
24
     * @return integer
25
     */
26
    public function run($argv, $home)
27
    {
28
        $optionDefaultValues = $this->getDefaultOptionValues($home);
29
        $optionDefaultValues = $this->overlayEnvironmentValues($optionDefaultValues);
30
31
        list($argv, $options) = $this->parseOutOurOptions($argv, $optionDefaultValues);
32
33
        if (reset($argv) == 'help') {
34
            $this->help($argv);
35
            return 0;
36
        }
37
38
        $commandList = $this->separateProjectAndGetCommandList($argv, $home, $options);
39
        if (empty($commandList)) {
40
            return 1;
41
        }
42
        return $this->runCommandList($commandList, $options);
0 ignored issues
show
Documentation introduced by
$commandList is of type object<Consolidation\Cgr\CommandToExec>, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
43
    }
44
45
    public function help($argv)
0 ignored issues
show
Unused Code introduced by
The parameter $argv is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
46
    {
47
        // Future: support 'help <command>'?
48
        print <<<EOT
49
The 'cgr' tool is a "safer" alternative to 'composer global require'.
50
Installing projects with cgr helps avoid dependency conflicts between
51
different tools.  Use 'cgr' wherever 'composer global require' is recommended.
52
53
Examples:
54
55
Install a project:
56
------------------
57
$ cgr drush/drush
58
59
Update a project:
60
-----------------
61
$ cgr update drush/drush
62
63
Update all projects installed via 'cgr':
64
----------------------------------------
65
$ cgr update
66
67
Remove a project:
68
-----------------
69
$ cgr remove drush/drush
70
71
For more information, see: https://github.com/consolidation/cgr
72
73
74
EOT;
75
    }
76
77
    /**
78
     * Set up output redirection. Used by tests.
79
     */
80
    public function setOutputFile($outputFile)
81
    {
82
        $this->outputFile = $outputFile;
83
    }
84
85
    /**
86
     * Figure out everything we're going to do, but don't do any of it
87
     * yet, just return the command objects to run.
88
     */
89
    public function parseArgvAndGetCommandList($argv, $home)
90
    {
91
        $optionDefaultValues = $this->getDefaultOptionValues($home);
92
        $optionDefaultValues = $this->overlayEnvironmentValues($optionDefaultValues);
93
94
        list($argv, $options) = $this->parseOutOurOptions($argv, $optionDefaultValues);
95
        return $this->separateProjectAndGetCommandList($argv, $home, $options);
96
    }
97
98
    /**
99
     * Figure out everything we're going to do, but don't do any of it
100
     * yet, just return the command objects to run.
101
     */
102
    public function separateProjectAndGetCommandList($argv, $home, $options)
0 ignored issues
show
Unused Code introduced by
The parameter $home is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
103
    {
104
        list($command, $projects, $composerArgs) = $this->separateProjectsFromArgs($argv, $options);
105
106
        // If command was unknown, then exit with an error message
107
        if (empty($command)) {
108
            print "Unknown command: " . implode(' ', $composerArgs) . "\n";
109
            exit(1);
0 ignored issues
show
Coding Style Compatibility introduced by
The method separateProjectAndGetCommandList() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
110
        }
111
112
        $commandList = $this->getCommandsToExec($command, $composerArgs, $projects, $options);
113
        return $commandList;
114
    }
115
116
    /**
117
     * Run all of the commands in a list.  Abort early if any fail.
118
     *
119
     * @param array $commandList An array of CommandToExec
120
     * @return integer
121
     */
122
    public function runCommandList($commandList, $options)
0 ignored issues
show
Unused Code introduced by
The parameter $options is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
123
    {
124
        foreach ($commandList as $command) {
125
            $exitCode = $command->run($this->outputFile);
126
            if ($exitCode) {
127
                return $exitCode;
128
            }
129
        }
130
        return 0;
131
    }
132
133
    /**
134
     * Return an array containing a list of commands to execute.  Depending on
135
     * the composition of the aguments and projects parameters, this list will
136
     * contain either a single command string to call through to composer (if
137
     * cgr is being used as a composer alias), or it will contain a list of
138
     * appropriate replacement 'composer global require' commands that install
139
     * each project in its own installation directory, while installing each
140
     * projects' binaries in the global Composer bin directory,
141
     * ~/.composer/vendor/bin.
142
     *
143
     * @param array $composerArgs
144
     * @param array $projects
145
     * @param array $options
146
     * @return CommandToExec
147
     */
148
    public function getCommandsToExec($command, $composerArgs, $projects, $options)
149
    {
150
        $execPath = $options['composer-path'];
151
152
        // Call requireCommand, updateCommand, or removeCommand, as appropriate.
153
        $methodName = "{$command}Command";
154
        if (method_exists($this, $methodName)) {
155
            return $this->$methodName($execPath, $composerArgs, $projects, $options);
156
        } else {
157
            // If there is no specific implementation for the requested command, then call 'generalCommand'.
158
            return $this->generalCommand($command, $execPath, $composerArgs, $projects, $options);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->generalCom..., $projects, $options); (array) is incompatible with the return type documented by Consolidation\Cgr\Application::getCommandsToExec of type Consolidation\Cgr\CommandToExec.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
159
        }
160
    }
161
162
    /**
163
     * Return our list of default option values, with paths relative to
164
     * the provided home directory.
165
     * @param string $home The user's home directory
166
     * @return array
167
     */
168
    public function getDefaultOptionValues($home)
169
    {
170
        return array(
171
            'composer' => false,
172
            'composer-path' => 'composer',
173
            'base-dir' => "$home/.composer/global",
174
            'bin-dir' => "$home/.composer/vendor/bin",
175
            'stability' => false,
176
        );
177
    }
178
179
    /**
180
     * Replace option default values with the corresponding
181
     * environment variable value, if it is set.
182
     */
183
    protected function overlayEnvironmentValues($defaults)
184
    {
185
        foreach ($defaults as $key => $value) {
186
            $envKey = 'CGR_' . strtoupper(strtr($key, '-', '_'));
187
            $envValue = getenv($envKey);
188
            if ($envValue) {
189
                $defaults[$key] = $envValue;
190
            }
191
        }
192
193
        return $defaults;
194
    }
195
196
    /**
197
     * We use our own special-purpose argv parser. The options that apply
198
     * to this tool are identified by a simple associative array, where
199
     * the key is the option name, and the value is its default value.
200
     * The result of this function is an array of two items containing:
201
     *  - An array of the items in $argv not used to set an option value
202
     *  - An array of options containing the user-specified or default values
203
     *
204
     * @param array $argv The global $argv passed in by php
205
     * @param array $optionDefaultValues An associative array
206
     * @return array
207
     */
208
    public function parseOutOurOptions($argv, $optionDefaultValues)
209
    {
210
        $argv0 = array_shift($argv);
211
        $options['composer'] = (strpos($argv0, 'composer') !== false);
0 ignored issues
show
Coding Style Comprehensibility introduced by
$options was never initialized. Although not strictly required by PHP, it is generally a good practice to add $options = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
212
        $passAlongArgvItems = array();
213
        $options = array();
214
        while (!empty($argv)) {
215
            $arg = array_shift($argv);
216
            if ((substr($arg, 0, 2) == '--') && array_key_exists(substr($arg, 2), $optionDefaultValues)) {
217
                $options[substr($arg, 2)] = array_shift($argv);
218
            } else {
219
                $passAlongArgvItems[] = $arg;
220
            }
221
        }
222
        return array($passAlongArgvItems, $options + $optionDefaultValues);
223
    }
224
225
    /**
226
     * After our options are removed by parseOutOurOptions, those items remaining
227
     * in $argv will be separated into a list of projects and versions, and
228
     * anything else that is not a project:version. Returns an array of two
229
     * items containing:
230
     *  - An associative array, where the key is the project name and the value
231
     *    is the version (or an empty string, if no version was specified)
232
     *  - The remaining $argv items not used to build the projects array.
233
     *
234
     * @param array $argv The $argv array from parseOutOurOptions()
235
     * @return array
236
     */
237
    public function separateProjectsFromArgs($argv, $options)
238
    {
239
        $cgrCommands = array('require', 'update', 'remove');
240
        $command = 'require';
241
        $composerArgs = array();
242
        $projects = array();
243
        $globalMode = !$options['composer'];
244
        foreach ($argv as $arg) {
245
            if ($arg[0] == '-') {
246
                // Any flags (first character is '-') will just be passed
247
                // through to to composer. Flags interpreted by cgr have
248
                // already been removed from $argv.
249
                $composerArgs[] = $arg;
250
            } elseif (strpos($arg, '/') !== false) {
251
                // Arguments containing a '/' name projects.  We will split
252
                // the project from its version, allowing the separator
253
                // character to be either a '=' or a ':', and then store the
254
                // result in the $projects array.
255
                $projectAndVersion = explode(':', strtr($arg, '=', ':'), 2) + array('', '');
256
                list($project, $version) = $projectAndVersion;
257
                $projects[$project] = $version;
258
            } elseif ($this->isComposerVersion($arg)) {
259
                // If an argument is a composer version, then we will alter
260
                // the last project we saw, attaching this version to it.
261
                // This allows us to handle 'a/b:1.0' and 'a/b 1.0' equivalently.
262
                $keys = array_keys($projects);
263
                $lastProject = array_pop($keys);
264
                unset($projects[$lastProject]);
265
                $projects[$lastProject] = $arg;
266
            } elseif ($arg == 'global') {
267
                // Make note if we see the 'global' command.
268
                $globalMode = true;
269
            } else {
270
                // If we see any command other than 'global [require|update|remove]',
271
                // then we will pass *all* of the arguments through to
272
                // composer unchanged. We return an empty projects array
273
                // to indicate that this should be a pass-through call
274
                // to composer, rather than one or more calls to
275
                // 'composer require' to install global projects.
276
                if ((!$globalMode) || (!in_array($arg, $cgrCommands))) {
277
                    return array('', array(), $argv);
278
                }
279
                // Remember which command we saw
280
                $command = $arg;
281
            }
282
        }
283
        return array($command, $projects, $composerArgs);
284
    }
285
286
    /**
287
     * Provide a safer version of `composer global require`.  Each project
288
     * listed in $projects will be installed into its own project directory.
289
     * The binaries from each project will still be placed in the global
290
     * composer bin directory.
291
     *
292
     * @param string $execPath The path to composer
293
     * @param array $composerArgs Anything from the global $argv to be passed
294
     *   on to Composer
295
     * @param array $projects A list of projects to install, with the key
296
     *   specifying the project name, and the value specifying its version.
297
     * @param array $options User options from the command line; see
298
     *   $optionDefaultValues in the main() function.
299
     * @return array
300
     */
301
    public function requireCommand($execPath, $composerArgs, $projects, $options)
302
    {
303
        $stabilityCommands = array();
304
        if ($options['stability']) {
305
            $stabilityCommands = $this->configureProjectStability($execPath, $composerArgs, $projects, $options);
306
        }
307
        $requireCommands = $this->generalCommand('require', $execPath, $composerArgs, $projects, $options);
308
        return array_merge($stabilityCommands, $requireCommands);
309
    }
310
311
    /**
312
     * General command handler.
313
     *
314
     * @param string $composerCommand The composer command to run e.g. require
315
     * @param string $execPath The path to composer
316
     * @param array $composerArgs Anything from the global $argv to be passed
317
     *   on to Composer
318
     * @param array $projects A list of projects to install, with the key
319
     *   specifying the project name, and the value specifying its version.
320
     * @param array $options User options from the command line; see
321
     *   $optionDefaultValues in the main() function.
322
     * @return array
323
     */
324
    public function generalCommand($composerCommand, $execPath, $composerArgs, $projects, $options)
325
    {
326
        $globalBaseDir = $options['base-dir'];
327
        $binDir = $options['bin-dir'];
328
        $env = array("COMPOSER_BIN_DIR" => $binDir);
329
        $result = array();
330
        foreach ($projects as $project => $version) {
331
            $installLocation = "$globalBaseDir/$project";
332
            $projectWithVersion = $this->projectWithVersion($project, $version);
333
            $commandToExec = $this->buildGlobalCommand($composerCommand, $execPath, $composerArgs, $projectWithVersion, $env, $installLocation);
334
            $result[] = $commandToExec;
335
        }
336
        return $result;
337
    }
338
339
    /**
340
     * If --stability VALUE is provided, then run a `composer config minimum-stability VALUE`
341
     * command to configure composer.json appropriately.
342
     *
343
     * @param string $execPath The path to composer
344
     * @param array $composerArgs Anything from the global $argv to be passed
345
     *   on to Composer
346
     * @param array $projects A list of projects to install, with the key
347
     *   specifying the project name, and the value specifying its version.
348
     * @param array $options User options from the command line; see
349
     *   $optionDefaultValues in the main() function.
350
     * @return array
351
     */
352
    public function configureProjectStability($execPath, $composerArgs, $projects, $options)
353
    {
354
        $globalBaseDir = $options['base-dir'];
355
        $stability = $options['stability'];
356
        $result = array();
357
        $env = array();
358
359
        foreach ($projects as $project => $version) {
360
            $installLocation = "$globalBaseDir/$project";
361
            FileSystemUtils::mkdirParents($installLocation);
362
            if (!file_exists("$installLocation/composer.json")) {
363
                file_put_contents("$installLocation/composer.json", '{}');
364
            }
365
            $result[] = $this->buildConfigCommand($execPath, $composerArgs, 'minimum-stability', $stability, $env, $installLocation);
366
        }
367
368
        return $result;
369
    }
370
371
    /**
372
     * Run `composer global update`. Not only do we want to update the
373
     * "global" Composer project, we also want to update all of the
374
     * "isolated" projects installed via cgr in ~/.composer/global.
375
     *
376
     * @param string $command The path to composer
0 ignored issues
show
Bug introduced by
There is no parameter named $command. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
377
     * @param array $composerArgs Anything from the global $argv to be passed
378
     *   on to Composer
379
     * @param array $projects A list of projects to update.
380
     * @param array $options User options from the command line; see
381
     *   $optionDefaultValues in the main() function.
382
     * @return array
383
     */
384
    public function updateCommand($execPath, $composerArgs, $projects, $options)
385
    {
386
        // If 'projects' list is empty, make a list of everything currently installed
387
        if (empty($projects)) {
388
            $projects = FileSystemUtils::allInstalledProjectsInBaseDir($options['base-dir']);
389
            $projects = $this->flipProjectsArray($projects);
390
        }
391
        return $this->generalCommand('update', $execPath, $composerArgs, $projects, $options);
392
    }
393
394
    /**
395
     * Convert from an array of projects to an array where the key is the
396
     * project name, and the value (version) is an empty string.
397
     *
398
     * @param string[] $projects
399
     * @return array
400
     */
401
    public function flipProjectsArray($projects)
402
    {
403
        return array_map(function () {
404
            return '';
405
        }, array_flip($projects));
406
    }
407
408
    /**
409
     * Return $project:$version, or just $project if there is no $version.
410
     *
411
     * @param string $project The project to install
412
     * @param string $version The version desired
413
     * @return string
414
     */
415
    public function projectWithVersion($project, $version)
416
    {
417
        if (empty($version)) {
418
            return $project;
419
        }
420
        return "$project:$version";
421
    }
422
423
    /**
424
     * Generate command string to call `composer COMMAND` to install one project.
425
     *
426
     * @param string $command The path to composer
0 ignored issues
show
Documentation introduced by
There is no parameter named $command. Did you maybe mean $composerCommand?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
427
     * @param array $composerArgs The arguments to pass to composer
428
     * @param string $projectWithVersion The project:version to install
429
     * @param array $env Environment to set prior to exec
430
     * @param string $installLocation Location to install the project
431
     * @return CommandToExec
432
     */
433
    public function buildGlobalCommand($composerCommand, $execPath, $composerArgs, $projectWithVersion, $env, $installLocation)
434
    {
435
        $projectSpecificArgs = array("--working-dir=$installLocation", $composerCommand, $projectWithVersion);
436
        $arguments = array_merge($composerArgs, $projectSpecificArgs);
437
        return new CommandToExec($execPath, $arguments, $env, $installLocation);
438
    }
439
440
    /**
441
     * Generate command string to call `composer config KEY VALUE` to install one project.
442
     *
443
     * @param string $execPath The path to composer
444
     * @param array $composerArgs The arguments to pass to composer
445
     * @param string $key The config item to set
446
     * @param string $value The value to set the config item to
447
     * @param array $env Environment to set prior to exec
448
     * @param string $installLocation Location to install the project
449
     * @return CommandToExec
450
     */
451
    public function buildConfigCommand($execPath, $composerArgs, $key, $value, $env, $installLocation)
452
    {
453
        $projectSpecificArgs = array("--working-dir=$installLocation", 'config', $key, $value);
454
        $arguments = array_merge($composerArgs, $projectSpecificArgs);
455
        return new CommandToExec($execPath, $arguments, $env, $installLocation);
456
    }
457
458
    /**
459
     * Identify an argument that could be a Composer version string.
460
     *
461
     * @param string $arg The argument to test
462
     * @return boolean
463
     */
464
    public function isComposerVersion($arg)
465
    {
466
        // Allow for 'dev-master', et. al.
467
        if (substr($arg, 0, 4) == 'dev-') {
468
            return true;
469
        }
470
        $specialVersionChars = array('^', '~', '<', '>');
471
        return is_numeric($arg[0]) || in_array($arg[0], $specialVersionChars);
472
    }
473
}
474