Completed
Pull Request — master (#5)
by Greg
02:03
created

Application   B

Complexity

Total Complexity 37

Size/Duplication

Total Lines 341
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Importance

Changes 0
Metric Value
wmc 37
lcom 1
cbo 2
dl 0
loc 341
rs 8.6
c 0
b 0
f 0

16 Methods

Rating   Name   Duplication   Size   Complexity  
A run() 0 12 2
A setOutputFile() 0 4 1
A parseArgvAndGetCommandList() 0 8 1
A separateProjectAndGetCommandList() 0 6 1
A runCommandList() 0 10 3
A getCommandsToExec() 0 19 3
A getDefaultOptionValues() 0 9 1
A overlayEnvironmentValues() 0 12 3
A parseOutOurOptions() 0 16 4
C separateProjectsFromArgs() 0 48 8
A generalCommand() 0 14 2
A updateCommand() 0 9 2
A flipProjectsArray() 0 4 1
A projectWithVersion() 0 7 2
A buildGlobalCommand() 0 6 1
A isComposerVersion() 0 5 2
1
<?php
2
3
namespace Consolidation\Cgr;
4
5
class Application
6
{
7
    protected $outputFile = '';
8
9
    /**
10
     * Run the cgr tool, a safer alternative to `composer global require`.
11
     *
12
     * @param array $argv The global $argv array passed in by PHP
13
     * @param string $home The path to the user's home directory
14
     * @return integer
15
     */
16
    public function run($argv, $home)
17
    {
18
        $optionDefaultValues = $this->getDefaultOptionValues($home);
19
        $optionDefaultValues = $this->overlayEnvironmentValues($optionDefaultValues);
20
21
        list($argv, $options) = $this->parseOutOurOptions($argv, $optionDefaultValues);
22
        $commandList = $this->separateProjectAndGetCommandList($argv, $home, $options);
23
        if (empty($commandList)) {
24
            return 1;
25
        }
26
        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...
27
    }
28
29
    /**
30
     * Set up output redirection. Used by tests.
31
     */
32
    public function setOutputFile($outputFile)
33
    {
34
        $this->outputFile = $outputFile;
35
    }
36
37
    /**
38
     * Figure out everything we're going to do, but don't do any of it
39
     * yet, just return the command objects to run.
40
     */
41
    public function parseArgvAndGetCommandList($argv, $home)
42
    {
43
        $optionDefaultValues = $this->getDefaultOptionValues($home);
44
        $optionDefaultValues = $this->overlayEnvironmentValues($optionDefaultValues);
45
46
        list($argv, $options) = $this->parseOutOurOptions($argv, $optionDefaultValues);
47
        return $this->separateProjectAndGetCommandList($argv, $home, $options);
48
    }
49
50
    /**
51
     * Figure out everything we're going to do, but don't do any of it
52
     * yet, just return the command objects to run.
53
     */
54
    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...
55
    {
56
        list($command, $projects, $composerArgs) = $this->separateProjectsFromArgs($argv, $options);
57
        $commandList = $this->getCommandsToExec($command, $composerArgs, $projects, $options);
58
        return $commandList;
59
    }
60
61
    /**
62
     * Run all of the commands in a list.  Abort early if any fail.
63
     *
64
     * @param array $commandList An array of CommandToExec
65
     * @return integer
66
     */
67
    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...
68
    {
69
        foreach ($commandList as $command) {
70
            $exitCode = $command->run($this->outputFile);
71
            if ($exitCode) {
72
                return $exitCode;
73
            }
74
        }
75
        return 0;
76
    }
77
78
    /**
79
     * Return an array containing a list of commands to execute.  Depending on
80
     * the composition of the aguments and projects parameters, this list will
81
     * contain either a single command string to call through to composer (if
82
     * cgr is being used as a composer alias), or it will contain a list of
83
     * appropriate replacement 'composer global require' commands that install
84
     * each project in its own installation directory, while installing each
85
     * projects' binaries in the global Composer bin directory,
86
     * ~/.composer/vendor/bin.
87
     *
88
     * @param array $composerArgs
89
     * @param array $projects
90
     * @param array $options
91
     * @return CommandToExec
92
     */
93
    public function getCommandsToExec($command, $composerArgs, $projects, $options)
94
    {
95
        $execPath = $options['composer-path'];
96
        // If command was not 'global require', 'global update' or
97
        // 'global remove', then call through to the standard composer
98
        // with all of the original args.
99
        if (empty($command)) {
100
            return array(new CommandToExec($execPath, $composerArgs));
0 ignored issues
show
Bug Best Practice introduced by
The return type of return array(new \Consol...cPath, $composerArgs)); (Consolidation\Cgr\CommandToExec[]) 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...
101
        }
102
        // Call requireCommand, updateCommand, or removeCommand, as appropriate.
103
        $methodName = "{$command}Command";
104
        if (method_exists($this, $methodName)) {
105
            return $this->$methodName($execPath, $composerArgs, $projects, $options);
106
        }
107
        else {
108
            // If there is no specific implementation for the requested command, then call 'generalCommand'.
109
            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...
110
        }
111
    }
112
113
    /**
114
     * Return our list of default option values, with paths relative to
115
     * the provided home directory.
116
     * @param string $home The user's home directory
117
     * @return array
118
     */
119
    public function getDefaultOptionValues($home)
120
    {
121
        return array(
122
            'composer' => false,
123
            'composer-path' => 'composer',
124
            'base-dir' => "$home/.composer/global",
125
            'bin-dir' => "$home/.composer/vendor/bin",
126
        );
127
    }
128
129
    /**
130
     * Replace option default values with the corresponding
131
     * environment variable value, if it is set.
132
     */
133
    protected function overlayEnvironmentValues($defaults)
134
    {
135
        foreach ($defaults as $key => $value) {
136
            $envKey = 'CGR_' . strtoupper(strtr($key, '-', '_'));
137
            $envValue = getenv($envKey);
138
            if ($envValue) {
139
                $defaults[$key] = $envValue;
140
            }
141
        }
142
143
        return $defaults;
144
    }
145
146
    /**
147
     * We use our own special-purpose argv parser. The options that apply
148
     * to this tool are identified by a simple associative array, where
149
     * the key is the option name, and the value is its default value.
150
     * The result of this function is an array of two items containing:
151
     *  - An array of the items in $argv not used to set an option value
152
     *  - An array of options containing the user-specified or default values
153
     *
154
     * @param array $argv The global $argv passed in by php
155
     * @param array $optionDefaultValues An associative array
156
     * @return array
157
     */
158
    public function parseOutOurOptions($argv, $optionDefaultValues)
159
    {
160
        $argv0 = array_shift($argv);
161
        $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...
162
        $passAlongArgvItems = array();
163
        $options = array();
164
        while (!empty($argv)) {
165
            $arg = array_shift($argv);
166
            if ((substr($arg, 0, 2) == '--') && array_key_exists(substr($arg, 2), $optionDefaultValues)) {
167
                $options[substr($arg, 2)] = array_shift($argv);
168
            } else {
169
                $passAlongArgvItems[] = $arg;
170
            }
171
        }
172
        return array($passAlongArgvItems, $options + $optionDefaultValues);
173
    }
174
175
    /**
176
     * After our options are removed by parseOutOurOptions, those items remaining
177
     * in $argv will be separated into a list of projects and versions, and
178
     * anything else that is not a project:version. Returns an array of two
179
     * items containing:
180
     *  - An associative array, where the key is the project name and the value
181
     *    is the version (or an empty string, if no version was specified)
182
     *  - The remaining $argv items not used to build the projects array.
183
     *
184
     * @param array $argv The $argv array from parseOutOurOptions()
185
     * @return array
186
     */
187
    public function separateProjectsFromArgs($argv, $options)
188
    {
189
        $cgrCommands = array('require', 'update', 'remove');
190
        $command = 'require';
191
        $composerArgs = array();
192
        $projects = array();
193
        $globalMode = !$options['composer'];
194
        foreach ($argv as $arg) {
195
            if ($arg[0] == '-') {
196
                // Any flags (first character is '-') will just be passed
197
                // through to to composer. Flags interpreted by cgr have
198
                // already been removed from $argv.
199
                $composerArgs[] = $arg;
200
            } elseif (strpos($arg, '/') !== false) {
201
                // Arguments containing a '/' name projects.  We will split
202
                // the project from its version, allowing the separator
203
                // character to be either a '=' or a ':', and then store the
204
                // result in the $projects array.
205
                $projectAndVersion = explode(':', strtr($arg, '=', ':'), 2) + array('', '');
206
                list($project, $version) = $projectAndVersion;
207
                $projects[$project] = $version;
208
            } elseif ($this->isComposerVersion($arg)) {
209
                // If an argument is a composer version, then we will alter
210
                // the last project we saw, attaching this version to it.
211
                // This allows us to handle 'a/b:1.0' and 'a/b 1.0' equivalently.
212
                $keys = array_keys($projects);
213
                $lastProject = array_pop($keys);
214
                unset($projects[$lastProject]);
215
                $projects[$lastProject] = $arg;
216
            } elseif ($arg == 'global') {
217
                // Make note if we see the 'global' command.
218
                $globalMode = true;
219
            } else {
220
                // If we see any command other than 'global [require|update|remove]',
221
                // then we will pass *all* of the arguments through to
222
                // composer unchanged. We return an empty projects array
223
                // to indicate that this should be a pass-through call
224
                // to composer, rather than one or more calls to
225
                // 'composer require' to install global projects.
226
                if ((!$globalMode) || (!in_array($arg, $cgrCommands))) {
227
                    return array('', array(), $argv);
228
                }
229
                // Remember which command we saw
230
                $command = $arg;
231
            }
232
        }
233
        return array($command, $projects, $composerArgs);
234
    }
235
236
    /**
237
     * Provide a safer version of `composer global require`.  Each project
238
     * listed in $projects will be installed into its own project directory.
239
     * The binaries from each project will still be placed in the global
240
     * composer bin directory.
241
     *
242
     * @param string $composerCommand The composer command to run e.g. require
243
     * @param string $execPath The path to composer
244
     * @param array $composerArgs Anything from the global $argv to be passed
245
     *   on to Composer
246
     * @param array $projects A list of projects to install, with the key
247
     *   specifying the project name, and the value specifying its version.
248
     * @param array $options User options from the command line; see
249
     *   $optionDefaultValues in the main() function.
250
     * @return array
251
     */
252
    public function generalCommand($composerCommand, $execPath, $composerArgs, $projects, $options)
253
    {
254
        $globalBaseDir = $options['base-dir'];
255
        $binDir = $options['bin-dir'];
256
        $env = array("COMPOSER_BIN_DIR" => $binDir);
257
        $result = array();
258
        foreach ($projects as $project => $version) {
259
            $installLocation = "$globalBaseDir/$project";
260
            $projectWithVersion = $this->projectWithVersion($project, $version);
261
            $commandToExec = $this->buildGlobalCommand($composerCommand, $execPath, $composerArgs, $projectWithVersion, $env, $installLocation);
262
            $result[] = $commandToExec;
263
        }
264
        return $result;
265
    }
266
267
    /**
268
     * Run `composer global update`. Not only do we want to update the
269
     * "global" Composer project, we also want to update all of the
270
     * "isolated" projects installed via cgr in ~/.composer/global.
271
     *
272
     * @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...
273
     * @param array $composerArgs Anything from the global $argv to be passed
274
     *   on to Composer
275
     * @param array $projects A list of projects to update.
276
     * @param array $options User options from the command line; see
277
     *   $optionDefaultValues in the main() function.
278
     * @return array
279
     */
280
    public function updateCommand($execPath, $composerArgs, $projects, $options)
281
    {
282
        // If 'projects' list is empty, make a list of everything currently installed
283
        if (empty($projects)) {
284
            $projects = FileSystemUtils::allInstalledProjectsInBaseDir($options['base-dir']);
285
            $projects = $this->flipProjectsArray($projects);
286
        }
287
        return $this->generalCommand('update', $execPath, $composerArgs, $projects, $options);
288
    }
289
290
    /**
291
     * Convert from an array of projects to an array where the key is the
292
     * project name, and the value (version) is an empty string.
293
     *
294
     * @param string[] $projects
295
     * @return array
296
     */
297
    public function flipProjectsArray($projects)
298
    {
299
        return array_map(function ($item) { return ''; }, array_flip($projects));
0 ignored issues
show
Unused Code introduced by
The parameter $item 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...
300
    }
301
302
    /**
303
     * Return $project:$version, or just $project if there is no $version.
304
     *
305
     * @param string $project The project to install
306
     * @param string $version The version desired
307
     * @return string
308
     */
309
    public function projectWithVersion($project, $version)
310
    {
311
        if (empty($version)) {
312
            return $project;
313
        }
314
        return "$project:$version";
315
    }
316
317
    /**
318
     * Generate command string to call `composer require` to install one project.
319
     *
320
     * @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...
321
     * @param array $composerArgs The arguments to pass to composer
322
     * @param string $projectWithVersion The project:version to install
323
     * @param array $env Environment to set prior to exec
324
     * @param string $installLocation Location to install the project
325
     * @return CommandToExec
326
     */
327
    public function buildGlobalCommand($composerCommand, $execPath, $composerArgs, $projectWithVersion, $env, $installLocation)
328
    {
329
        $projectSpecificArgs = array("--working-dir=$installLocation", $composerCommand, $projectWithVersion);
330
        $arguments = array_merge($composerArgs, $projectSpecificArgs);
331
        return new CommandToExec($execPath, $arguments, $env, $installLocation);
332
    }
333
334
    /**
335
     * Identify an argument that could be a Composer version string.
336
     *
337
     * @param string $arg The argument to test
338
     * @return boolean
339
     */
340
    public function isComposerVersion($arg)
341
    {
342
        $specialVersionChars = array('^', '~', '<', '>');
343
        return is_numeric($arg[0]) || in_array($arg[0], $specialVersionChars);
344
    }
345
}
346