Completed
Push — master ( bffb9b...4839a5 )
by Greg
02:40
created

Application::overlayEnvironmentValues()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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