Completed
Push — master ( 9ca241...bffb9b )
by Greg
02:09
created

Application::setOutputFile()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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