Completed
Push — master ( 600eb8...cf92d2 )
by Greg
02:07
created

Application::parseOutOurOptions()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 15
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 15
rs 9.2
cc 4
eloc 11
nc 3
nop 2
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
        $optionDefaultValues = $this->getDefaultOptionValues($home);
17
        list($argv, $options) = $this->parseOutOurOptions($argv, $optionDefaultValues);
18
        $commandList = $this->separateProjectAndGetCommandList($argv, $home, $options);
19
        return $this->runCommandList($commandList, $options);
20
    }
21
22
    /**
23
     * Figure out everything we're going to do, but don't do any of it
24
     * yet, just return the command objects to run.
25
     */
26
    public function parseArgvAndGetCommandList($argv, $home)
27
    {
28
        $optionDefaultValues = $this->getDefaultOptionValues($home);
29
        list($argv, $options) = $this->parseOutOurOptions($argv, $optionDefaultValues);
30
        return $this->separateProjectAndGetCommandList($argv, $home, $options);
31
    }
32
33
    /**
34
     * Figure out everything we're going to do, but don't do any of it
35
     * yet, just return the command objects to run.
36
     */
37
    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...
38
    {
39
        list($projects, $composerArgs) = $this->separateProjectsFromArgs($argv);
40
        $commandList = $this->getCommandStringList($composerArgs, $projects, $options);
41
        return $commandList;
42
    }
43
44
    /**
45
     * Run all of the commands in a list.  Abort early if any fail.
46
     *
47
     * @param array $commandList An array of CommandToExec
48
     * @return integer
49
     */
50
    public function runCommandList($commandList, $options)
51
    {
52
        foreach ($commandList as $command) {
53
            $exitCode = $command->run($options['cgr-output']);
54
            if ($exitCode) {
55
                return $exitCode;
56
            }
57
        }
58
        return 0;
59
    }
60
61
    /**
62
     * Return an array containing a list of commands to execute.  Depending on
63
     * the composition of the aguments and projects parameters, this list will
64
     * contain either a single command string to call through to composer (if
65
     * cgr is being used as a composer alias), or it will contain a list of
66
     * appropriate replacement 'composer global require' commands that install
67
     * each project in its own installation directory, while installing each
68
     * projects' binaries in the global Composer bin directory,
69
     * ~/.composer/vendor/bin.
70
     *
71
     * @param array $composerArgs
72
     * @param array $projects
73
     * @param array $options
74
     * @return CommandToExec
75
     */
76
    public function getCommandStringList($composerArgs, $projects, $options)
77
    {
78
        $command = $options['composer-path'];
79
        if (empty($projects)) {
80
            return array(new CommandToExec($command, $composerArgs));
81
        }
82
        return $this->globalRequire($command, $composerArgs, $projects, $options);
83
    }
84
85
    /**
86
     * Return our list of default option values, with paths relative to
87
     * the provided home directory.
88
     * @param string $home The user's home directory
89
     * @return array
90
     */
91
    public function getDefaultOptionValues($home)
92
    {
93
        return array(
94
            'composer-path' => 'composer',
95
            'base-dir' => "$home/.composer/global",
96
            'bin-dir' => "$home/.composer/vendor/bin",
97
            'cgr-output' => '',
98
        );
99
    }
100
101
    /**
102
     * We use our own special-purpose argv parser. The options that apply
103
     * to this tool are identified by a simple associative array, where
104
     * the key is the option name, and the value is its default value.
105
     * The result of this function is an array of two items containing:
106
     *  - An array of the items in $argv not used to set an option value
107
     *  - An array of options containing the user-specified or default values
108
     *
109
     * @param array $argv The global $argv passed in by php
110
     * @param array $optionDefaultValues An associative array
111
     * @return array
112
     */
113
    public function parseOutOurOptions($argv, $optionDefaultValues)
114
    {
115
        array_shift($argv);
116
        $passAlongArgvItems = array();
117
        $options = array();
118
        while (!empty($argv)) {
119
            $arg = array_shift($argv);
120
            if ((substr($arg, 0, 2) == '--') && array_key_exists(substr($arg, 2), $optionDefaultValues)) {
121
                $options[substr($arg, 2)] = array_shift($argv);
122
            } else {
123
                $passAlongArgvItems[] = $arg;
124
            }
125
        }
126
        return array($passAlongArgvItems, $options + $optionDefaultValues);
127
    }
128
129
    /**
130
     * After our options are removed by parseOutOurOptions, those items remaining
131
     * in $argv will be separated into a list of projects and versions, and
132
     * anything else that is not a project:version. Returns an array of two
133
     * items containing:
134
     *  - An associative array, where the key is the project name and the value
135
     *    is the version (or an empty string, if no version was specified)
136
     *  - The remaining $argv items not used to build the projects array.
137
     *
138
     * @param array $argv The $argv array from parseOutOurOptions()
139
     * @return array
140
     */
141
    public function separateProjectsFromArgs($argv)
142
    {
143
        $composerArgs = array();
144
        $projects = array();
145
        $sawGlobal = false;
146
        foreach ($argv as $arg) {
147
            if ($arg[0] == '-') {
148
                // Any flags (first character is '-') will just be passed
149
                // through to to composer. Flags interpreted by cgr have
150
                // already been removed from $argv.
151
                $composerArgs[] = $arg;
152
            } elseif (strpos($arg, '/') !== false) {
153
                // Arguments containing a '/' name projects.  We will split
154
                // the project from its version, allowing the separator
155
                // character to be either a '=' or a ':', and then store the
156
                // result in the $projects array.
157
                $projectAndVersion = explode(':', strtr($arg, '=', ':'), 2) + array('', '');
158
                list($project, $version) = $projectAndVersion;
159
                $projects[$project] = $version;
160
            } elseif ($this->isComposerVersion($arg)) {
161
                // If an argument is a composer version, then we will alter
162
                // the last project we saw, attaching this version to it.
163
                // This allows us to handle 'a/b:1.0' and 'a/b 1.0' equivalently.
164
                $keys = array_keys($projects);
165
                $lastProject = array_pop($keys);
166
                unset($projects[$lastProject]);
167
                $projects[$lastProject] = $arg;
168
            } elseif ($arg == 'global') {
169
                // Make note if we see the 'global' command.
170
                $sawGlobal = true;
171
            } else {
172
                // If we see any command other than 'global require',
173
                // then we will pass *all* of the arguments through to
174
                // composer unchanged. We return an empty projects array
175
                // to indicate that this should be a pass-through call
176
                // to composer, rather than one or more calls to
177
                // 'composer require' to install global projects.
178
                if ((!$sawGlobal) || ($arg != 'require')) {
179
                    return array(array(), $argv);
180
                }
181
            }
182
        }
183
        return array($projects, $composerArgs);
184
    }
185
186
    /**
187
     * Provide a safer version of `composer global require`.  Each project
188
     * listed in $projects will be installed into its own project directory.
189
     * The binaries from each project will still be placed in the global
190
     * composer bin directory.
191
     *
192
     * @param string $command The path to composer
193
     * @param array $composerArgs Anything from the global $argv to be passed
194
     *   on to Composer
195
     * @param array $projects A list of projects to install, with the key
196
     *   specifying the project name, and the value specifying its version.
197
     * @param array $options User options from the command line; see
198
     *   $optionDefaultValues in the main() function.
199
     * @return array
200
     */
201
    public function globalRequire($command, $composerArgs, $projects, $options)
202
    {
203
        $globalBaseDir = $options['base-dir'];
204
        $binDir = $options['bin-dir'];
205
        $env = array("COMPOSER_BIN_DIR" => $binDir);
206
        $result = array();
207
        foreach ($projects as $project => $version) {
208
            $installLocation = "$globalBaseDir/$project";
209
            $projectWithVersion = $this->projectWithVersion($project, $version);
210
            $commandToExec = $this->globalRequireOne($command, $composerArgs, $projectWithVersion, $env, $installLocation);
211
            $result[] = $commandToExec;
212
        }
213
        return $result;
214
    }
215
216
    /**
217
     * Return $project:$version, or just $project if there is no $version.
218
     *
219
     * @param string $project The project to install
220
     * @param string $version The version desired
221
     * @return string
222
     */
223
    public function projectWithVersion($project, $version)
224
    {
225
        if (empty($version)) {
226
            return $project;
227
        }
228
        return "$project:$version";
229
    }
230
231
    /**
232
     * Generate command string to call `composer require` to install one project.
233
     *
234
     * @param string $command The path to composer
235
     * @param array $composerArgs The arguments to pass to composer
236
     * @param string $projectWithVersion The project:version to install
237
     * @param array $env Environment to set prior to exec
238
     * @param string $installLocation Location to install the project
239
     * @return CommandToExec
240
     */
241
    public function globalRequireOne($command, $composerArgs, $projectWithVersion, $env, $installLocation)
242
    {
243
        $projectSpecificArgs = array("--working-dir=$installLocation", 'require', $projectWithVersion);
244
        $arguments = array_merge($composerArgs, $projectSpecificArgs);
245
        return new CommandToExec($command, $arguments, $env, $installLocation);
246
    }
247
248
    /**
249
     * Identify an argument that could be a Composer version string.
250
     *
251
     * @param string $arg The argument to test
252
     * @return boolean
253
     */
254
    public function isComposerVersion($arg)
255
    {
256
        $specialVersionChars = array('^', '~', '<', '>');
257
        return is_numeric($arg[0]) || in_array($arg[0], $specialVersionChars);
258
    }
259
}
260