Completed
Push — master ( 49db85...a718f3 )
by Greg
02:14
created

Application::runCommandList()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 10
rs 9.4285
cc 3
eloc 6
nc 3
nop 1
1
<?php
2
3
namespace Consolidation\Cgr;
4
5
class Application
6
{
7
    /**
8
     * Run the cgr tool, a safer alternative to `composer global require`.
9
     *
10
     * @param array $argv The global $argv array passed in by PHP
11
     * @param string $home The path to the user's home directory
12
     * @return integer
13
     */
14
    public function run($argv, $home)
15
    {
16
        $commandList = $this->parseArgvAndGetCommandList($argv, $home);
17
        return $this->runCommandList($commandList);
18
    }
19
20
    /**
21
     * Figure out everything we're going to do, but don't do any of it
22
     * yet, just return the command objects to run.
23
     */
24
    public function parseArgvAndGetCommandList($argv, $home)
25
    {
26
        $optionDefaultValues = $this->getDefaultOptionValues($home);
27
        list($argv, $options) = $this->parseOutOurOptions($argv, $optionDefaultValues);
28
        list($projects, $composerArgs) = $this->separateProjectsFromArgs($argv, $options);
0 ignored issues
show
Unused Code introduced by
The call to Application::separateProjectsFromArgs() has too many arguments starting with $options.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
29
30
        $commandList = $this->getCommandStringList($composerArgs, $projects, $options);
31
        return $commandList;
32
    }
33
34
    /**
35
     * Run all of the commands in a list.  Abort early if any fail.
36
     *
37
     * @param array $commandList An array of CommandToExec
38
     * @return integer
39
     */
40
    public function runCommandList($commandList)
41
    {
42
        foreach ($commandList as $command) {
43
            $exitCode = $command->run();
44
            if ($exitCode) {
45
                return $exitCode;
46
            }
47
        }
48
        return 0;
49
    }
50
51
    /**
52
     * Return an array containing a list of commands to execute.  Depending on
53
     * the composition of the aguments and projects parameters, this list will
54
     * contain either a single command string to call through to composer (if
55
     * cgr is being used as a composer alias), or it will contain a list of
56
     * appropriate replacement 'composer global require' commands that install
57
     * each project in its own installation directory, while installing each
58
     * projects' binaries in the global Composer bin directory,
59
     * ~/.composer/vendor/bin.
60
     *
61
     * @param array $composerArgs
62
     * @param array $projects
63
     * @param array $options
64
     * @return CommandToExec
65
     */
66
    public function getCommandStringList($composerArgs, $projects, $options)
67
    {
68
        $command = $options['composer-path'];
69
        if (empty($projects)) {
70
            return array(new CommandToExec($command, $composerArgs));
71
        }
72
        return $this->globalRequire($command, $composerArgs, $projects, $options);
73
    }
74
75
    /**
76
     * Return our list of default option values, with paths relative to
77
     * the provided home directory.
78
     * @param string $home The user's home directory
79
     * @return array
80
     */
81
    public function getDefaultOptionValues($home)
82
    {
83
        return array(
84
            'composer-path' => 'composer',
85
            'base-dir' => "$home/.composer/global",
86
            'bin-dir' => "$home/.composer/vendor/bin",
87
        );
88
    }
89
90
    /**
91
     * We use our own special-purpose argv parser. The options that apply
92
     * to this tool are identified by a simple associative array, where
93
     * the key is the option name, and the value is its default value.
94
     * The result of this function is an array of two items containing:
95
     *  - An array of the items in $argv not used to set an option value
96
     *  - An array of options containing the user-specified or default values
97
     *
98
     * @param array $argv The global $argv passed in by php
99
     * @param array $optionDefaultValues An associative array
100
     * @return array
101
     */
102
    public function parseOutOurOptions($argv, $optionDefaultValues)
103
    {
104
        array_shift($argv);
105
        $passAlongArgvItems = array();
106
        $options = array();
107
        while (!empty($argv)) {
108
            $arg = array_shift($argv);
109
            if ((substr($arg, 0, 2) == '--') && array_key_exists(substr($arg, 2), $optionDefaultValues)) {
110
                $options[substr($arg, 2)] = array_shift($argv);
111
            } else {
112
                $passAlongArgvItems[] = $arg;
113
            }
114
        }
115
        return array($passAlongArgvItems, $options + $optionDefaultValues);
116
    }
117
118
    /**
119
     * After our options are removed by parseOutOurOptions, those items remaining
120
     * in $argv will be separated into a list of projects and versions, and
121
     * anything else that is not a project:version. Returns an array of two
122
     * items containing:
123
     *  - An associative array, where the key is the project name and the value
124
     *    is the version (or an empty string, if no version was specified)
125
     *  - The remaining $argv items not used to build the projects array.
126
     *
127
     * @param array $argv The $argv array from parseOutOurOptions()
128
     * @return array
129
     */
130
    public function separateProjectsFromArgs($argv)
131
    {
132
        $composerArgs = array();
133
        $projects = array();
134
        $sawGlobal = false;
135
        foreach ($argv as $arg) {
136
            if ($arg[0] == '-') {
137
                // Any flags (first character is '-') will just be passed
138
                // through to to composer. Flags interpreted by cgr have
139
                // already been removed from $argv.
140
                $composerArgs[] = $arg;
141
            } elseif (strpos($arg, '/') !== false) {
142
                // Arguments containing a '/' name projects.  We will split
143
                // the project from its version, allowing the separator
144
                // character to be either a '=' or a ':', and then store the
145
                // result in the $projects array.
146
                $projectAndVersion = explode(':', strtr($arg, '=', ':'), 2) + array('', '');
147
                list($project, $version) = $projectAndVersion;
148
                $projects[$project] = $version;
149
            } elseif ($this->isComposerVersion($arg)) {
150
                // If an argument is a composer version, then we will alter
151
                // the last project we saw, attaching this version to it.
152
                // This allows us to handle 'a/b:1.0' and 'a/b 1.0' equivalently.
153
                $keys = array_keys($projects);
154
                $lastProject = array_pop($keys);
155
                unset($projects[$lastProject]);
156
                $projects[$lastProject] = $arg;
157
            } elseif ($arg == 'global') {
158
                // Make note if we see the 'global' command.
159
                $sawGlobal = true;
160
            } else {
161
                // If we see any command other than 'global require',
162
                // then we will pass *all* of the arguments through to
163
                // composer unchanged. We return an empty projects array
164
                // to indicate that this should be a pass-through call
165
                // to composer, rather than one or more calls to
166
                // 'composer require' to install global projects.
167
                if (($sawGlobal == false) || ($arg != 'require')) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
168
                    return array(array(), $argv);
169
                }
170
            }
171
        }
172
        return array($projects, $composerArgs);
173
    }
174
175
    /**
176
     * Provide a safer version of `composer global require`.  Each project
177
     * listed in $projects will be installed into its own project directory.
178
     * The binaries from each project will still be placed in the global
179
     * composer bin directory.
180
     *
181
     * @param string $command The path to composer
182
     * @param array $composerArgs Anything from the global $argv to be passed
183
     *   on to Composer
184
     * @param array $projects A list of projects to install, with the key
185
     *   specifying the project name, and the value specifying its version.
186
     * @param array $options User options from the command line; see
187
     *   $optionDefaultValues in the main() function.
188
     * @return array
189
     */
190
    public function globalRequire($command, $composerArgs, $projects, $options)
191
    {
192
        $globalBaseDir = $options['base-dir'];
193
        $binDir = $options['bin-dir'];
194
        $env = array("COMPOSER_BIN_DIR" => $binDir);
195
        $result = array();
196
        foreach ($projects as $project => $version) {
197
            $installLocation = "$globalBaseDir/$project";
198
            $projectWithVersion = $this->projectWithVersion($project, $version);
199
            $commandToExec = $this->globalRequireOne($command, $composerArgs, $projectWithVersion, $env, $installLocation);
200
            $result[] = $commandToExec;
201
        }
202
        return $result;
203
    }
204
205
    /**
206
     * Return $project:$version, or just $project if there is no $version.
207
     *
208
     * @param string $project The project to install
209
     * @param string $version The version desired
210
     * @return string
211
     */
212
    public function projectWithVersion($project, $version)
213
    {
214
        if (empty($version)) {
215
            return $project;
216
        }
217
        return "$project:$version";
218
    }
219
220
    /**
221
     * Generate command string to call `composer require` to install one project.
222
     *
223
     * @param string $command The path to composer
224
     * @param array $composerArgs The arguments to pass to composer
225
     * @param string $projectWithVersion The project:version to install
226
     * @param array $env Environment to set prior to exec
227
     * @param string $installLocation Location to install the project
228
     * @return CommandToExec
229
     */
230
    public function globalRequireOne($command, $composerArgs, $projectWithVersion, $env, $installLocation)
231
    {
232
        $projectSpecificArgs = array("--working-dir=$installLocation", 'require', $projectWithVersion);
233
        $arguments = array_merge($composerArgs, $projectSpecificArgs);
234
        return new CommandToExec($command, $arguments, $env, $installLocation);
0 ignored issues
show
Documentation introduced by
$installLocation is of type string, but the function expects a boolean.

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...
235
    }
236
237
    /**
238
     * Identify an argument that could be a Composer version string.
239
     *
240
     * @param string $arg The argument to test
241
     * @return boolean
242
     */
243
    public function isComposerVersion($arg)
244
    {
245
        $specialVersionChars = array('^', '~', '<', '>');
246
        return is_numeric($arg[0]) || in_array($arg[0], $specialVersionChars);
247
    }
248
}
249