Passed
Push — master ( 88d9d6...ec4c8d )
by Tom
04:42
created

App   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 480
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 8
Bugs 0 Features 1
Metric Value
eloc 159
c 8
b 0
f 1
dl 0
loc 480
ccs 168
cts 168
cp 1
rs 8.8
wmc 45

19 Methods

Rating   Name   Duplication   Size   Complexity  
A parseDeployMode() 0 14 2
A parseBasename() 0 10 2
A getWorkingDirectory() 0 10 2
A parseExec() 0 15 3
A parseReference() 0 5 1
A getRunPipeline() 0 27 4
A parseFile() 0 20 5
A changeWorkingDir() 0 8 2
A parseStreams() 0 11 2
B parsePath() 0 36 8
A parseWorkingDir() 0 11 2
A parseRemainingOptions() 0 7 2
A __construct() 0 4 1
A run() 0 78 3
A info() 0 4 1
A error() 0 4 1
A create() 0 5 1
A verbose() 0 4 2
A main() 0 14 1

How to fix   Complexity   

Complex Class

Complex classes like App often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use App, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/* this file is part of pipelines */
4
5
namespace Ktomk\Pipelines\Utility;
6
7
use InvalidArgumentException;
8
use Ktomk\Pipelines\Cli\Args;
9
use Ktomk\Pipelines\Cli\ArgsException;
10
use Ktomk\Pipelines\Cli\Exec;
11
use Ktomk\Pipelines\Cli\Streams;
12
use Ktomk\Pipelines\File\File;
13
use Ktomk\Pipelines\File\ParseException;
14
use Ktomk\Pipelines\File\Pipeline;
15
use Ktomk\Pipelines\Lib;
16
use Ktomk\Pipelines\LibFs;
17
use Ktomk\Pipelines\LibFsPath;
18
use Ktomk\Pipelines\LibFsStream;
19
use Ktomk\Pipelines\Project;
20
use Ktomk\Pipelines\Runner\Directories;
21
use Ktomk\Pipelines\Runner\Flags;
22
use Ktomk\Pipelines\Runner\Reference;
23
use Ktomk\Pipelines\Runner\Runner;
24
use Ktomk\Pipelines\Runner\RunOpts;
25
26
class App implements StatusRunnable
27
{
28
    const BBPL_BASENAME = 'bitbucket-pipelines.yml';
29
30
    const UTILITY_NAME = 'pipelines';
31
32
    const VERSION = '@.@.@';
33
34
    /**
35
     * @var Args
36
     */
37
    private $arguments;
38
39
    /**
40
     * @var Streams
41
     */
42
    private $streams;
43
44
    /**
45
     * @var bool
46
     */
47
    private $verbose = true;
48
49
    /**
50
     * @var Help
51
     */
52
    private $help;
53
54
    /**
55
     * @return App
56
     */
57 1
    public static function create()
58
    {
59 1
        $streams = Streams::create();
60
61 1
        return new self($streams);
62
    }
63
64 30
    public function __construct(Streams $streams)
65
    {
66 30
        $this->streams = $streams;
67 30
        $this->help = new Help($streams);
68 30
    }
69
70
    /**
71
     * @param array $arguments including the utility name in the first argument
72
     *
73
     * @throws InvalidArgumentException
74
     *
75
     * @return int 0-255
76
     */
77 29
    public function main(array $arguments)
78
    {
79 29
        $args = Args::create($arguments);
80
81 29
        $this->verbose = $args->hasOption(array('v', 'verbose'));
82 29
        $this->arguments = $args;
83
84 29
        $handler = new ExceptionHandler(
85 29
            $this->streams,
86 29
            $this->help,
87 29
            $args->hasOption('debug')
88
        );
89
90 29
        return $handler->handleStatus($this);
91
    }
92
93
    /**
94
     * @throws \RuntimeException
95
     * @throws InvalidArgumentException
96
     * @throws ParseException
97
     * @throws ArgsException
98
     * @throws StatusException
99
     * @throws \UnexpectedValueException
100
     *
101
     * @return int
102
     */
103 29
    public function run()
104
    {
105 29
        $args = $this->arguments;
106
107 29
        $this->help->run($args);
108
109 27
        $exec = $this->parseExec();
110
111 27
        $keep = KeepOptions::bind($args)->run();
112
113 26
        $cache = CacheOptions::bind($args)->run();
114
115 26
        $basename = $this->parseBasename();
116
117 25
        $workingDir = $this->parseWorkingDir();
118
119 24
        $path = $this->parsePath($basename, $workingDir);
120
121 21
        $project = new Project($workingDir);
122
123
        // TODO: obtain project dir information etc. from VCS
124
        // $vcs = new Vcs();
125
126 21
        $runOpts = RunnerOptions::bind($args, $this->streams)->run();
127 18
        $project->setPrefix($runOpts->getPrefix());
128
129 18
        DockerOptions::bind($args, $exec, $project->getPrefix(), $this->streams)->run();
130
131 17
        $noRun = $args->hasOption('no-run');
132
133 17
        $deployMode = $this->parseDeployMode();
134
135 16
        $pipelines = File::createFromFile($path);
136
137 15
        ValidationOptions::bind($args, $this->streams, $pipelines)->run();
138
139 15
        $fileOptions = FileOptions::bind($args, $this->streams, $pipelines)->run();
140
141 11
        $reference = $this->parseReference();
142
143 11
        $env = EnvParser::create($this->arguments)
144 11
            ->parse(Lib::env($_SERVER), $reference, $project->getPath());
145
146 11
        $directories = new Directories(Lib::env($_SERVER), $project);
147
148 11
        ServiceOptions::bind(
149 11
            $args,
150 11
            $this->streams,
151
            $pipelines,
152
            $exec,
153
            $env,
154
            $runOpts,
155
            $directories
156 11
        )->run();
157
158 11
        $pipelineId = $args->getOptionArgument(
159 11
            'pipeline',
160 11
            $pipelines->searchIdByReference($reference) ?: 'default'
161
        );
162
163 11
        $streams = $this->parseStreams();
164
165 11
        $this->parseRemainingOptions();
166
167 10
        $pipeline = $this->getRunPipeline($pipelines, $pipelineId, $fileOptions, $runOpts);
168
169 7
        $flags = Flags::createForUtility($keep, $deployMode, $cache);
170
171 7
        $runner = Runner::createEx($runOpts, $directories, $exec, $flags, $env, $streams);
172
173 7
        if ($noRun) {
174 2
            $this->verbose('info: not running the pipeline per --no-run option');
175 2
            $status = 0;
176
        } else {
177 5
            $status = $runner->run($pipeline);
178
        }
179
180 7
        return $status;
181
    }
182
183
    /**
184
     * @throws StatusException
185
     *
186
     * @return string
187
     */
188 24
    private function getWorkingDirectory()
189
    {
190 24
        $workingDir = \getcwd();
191 24
        if (false === $workingDir) {
192
            // @codeCoverageIgnoreStart
193
            throw new StatusException('fatal: obtain working directory', 1);
194
            // @codeCoverageIgnoreEnd
195
        }
196
197 24
        return $workingDir;
198
    }
199
200
    /**
201
     * @throws InvalidArgumentException
202
     * @throws StatusException
203
     * @throws ArgsException
204
     *
205
     * @return string basename for bitbucket-pipelines.yml
206
     */
207 26
    private function parseBasename()
208
    {
209 26
        $args = $this->arguments;
210
211 26
        $basename = $args->getStringOptionArgument('basename', self::BBPL_BASENAME);
212 26
        if (!LibFsPath::isBasename($basename)) {
213 1
            throw new StatusException(sprintf("not a basename: '%s'", $basename), 1);
214
        }
215
216 25
        return $basename;
217
    }
218
219
    /**
220
     * @throws InvalidArgumentException
221
     * @throws ArgsException
222
     * @throws StatusException
223
     *
224
     * @return string deploy mode ('copy', 'mount')
225
     */
226 17
    private function parseDeployMode()
227
    {
228 17
        $args = $this->arguments;
229
230 17
        $deployMode = $args->getStringOptionArgument('deploy', 'copy');
231
232 17
        if (!in_array($deployMode, array('mount', 'copy'), true)) {
233 1
            throw new StatusException(
234 1
                sprintf("unknown deploy mode '%s'\n", $deployMode),
235 1
                1
236
            );
237
        }
238
239 16
        return $deployMode;
240
    }
241
242
    /**
243
     * @throws InvalidArgumentException
244
     *
245
     * @return Exec
246
     */
247 27
    private function parseExec()
248
    {
249 27
        $args = $this->arguments;
250
251 27
        $debugPrinter = null;
252 27
        if ($this->verbose) {
253 5
            $debugPrinter = $this->streams;
254
        }
255 27
        $exec = new Exec($debugPrinter);
256
257 27
        if ($args->hasOption('dry-run')) {
258 6
            $exec->setActive(false);
259
        }
260
261 27
        return $exec;
262
    }
263
264
    /**
265
     * @param string $basename
266
     * @param string $workingDir
267
     *
268
     * @throws InvalidArgumentException
269
     * @throws StatusException
270
     * @throws ArgsException
271
     *
272
     * @return string file
273
     */
274 24
    private function parseFile($basename, &$workingDir)
275
    {
276 24
        $args = $this->arguments;
277
278
        /** @var null|string $file as bitbucket-pipelines.yml to process */
279 24
        $file = $args->getOptionArgument('file', null);
280 24
        if (null === $file && null !== $file = LibFs::fileLookUp($basename, $workingDir)) {
281
            /** @var string $file */
282 17
            $buffer = dirname($file);
283 17
            if ($buffer !== $workingDir) {
284 1
                $this->changeWorkingDir($buffer);
285 1
                $workingDir = $this->getWorkingDirectory();
286
            }
287
        }
288
289 24
        if (empty($file)) {
290 1
            throw new StatusException('file can not be empty', 1);
291
        }
292
293 23
        return $file;
294
    }
295
296
    /**
297
     * @param string $basename
298
     * @param string $workingDir
299
     *
300
     * @throws InvalidArgumentException
301
     * @throws ArgsException
302
     * @throws StatusException
303
     *
304
     * @return string path
305
     */
306 24
    private function parsePath($basename, &$workingDir)
307
    {
308 24
        $buffer = (string)$workingDir;
309
310 24
        $file = $this->parseFile($basename, $workingDir);
311
312 23
        $this->verbose(sprintf(
313 23
            'info: project directory is %s',
314 23
            $workingDir === $buffer
315 22
                ? sprintf("'%s'", $workingDir)
316 23
                : sprintf("'%s' (OLDPWD: '%s')", $workingDir, $buffer)
317
        ));
318
319 23
        if ($file !== $basename && self::BBPL_BASENAME !== $basename) {
320 1
            $this->verbose('info: --file overrides non-default --basename');
321
        }
322
323
        // full path to bitbucket-pipelines.yml to process
324 23
        $path = LibFsPath::isAbsolute($file)
325 18
            ? $file
326 23
            : $workingDir . '/' . $file;
327
328
        // support stdin and process substitution for pipelines file
329 23
        if ($file !== LibFsStream::mapFile($file)) {
330 1
            $this->verbose(sprintf('info: reading pipelines from %s', '-' === $file ? 'stdin' : $file));
331
332 1
            return $file;
333
        }
334
335 22
        if (!LibFs::isReadableFile($file)) {
336 2
            throw new StatusException(sprintf('not a readable file: %s', $file), 1);
337
        }
338
339 20
        $this->verbose(sprintf("info: pipelines file is '%s'", $path));
340
341 20
        return $path;
342
    }
343
344
    /**
345
     * @throws InvalidArgumentException
346
     * @throws ArgsException
347
     *
348
     * @return Reference
349
     */
350 11
    private function parseReference()
351
    {
352 11
        $trigger = $this->arguments->getOptionArgument('trigger');
353
354 11
        return Reference::create($trigger);
355
    }
356
357
    /**
358
     * give error about unknown option, show usage and exit status of 1
359
     *
360
     * @throws ArgsException
361
     *
362
     * @return void
363
     */
364 11
    private function parseRemainingOptions()
365
    {
366 11
        $option = $this->arguments->getFirstRemainingOption();
367
368 11
        if ($option) {
369 1
            throw new ArgsException(
370 1
                sprintf('unknown option: %s', $option)
371
            );
372
        }
373 10
    }
374
375
    /**
376
     * @throws InvalidArgumentException
377
     *
378
     * @return Streams
379
     */
380 11
    private function parseStreams()
381
    {
382 11
        $streams = $this->streams;
383
384
        // --verbatim show only errors for own runner actions, show everything from pipeline verbatim
385 11
        if ($this->arguments->hasOption('verbatim')) {
386 1
            $streams = new Streams();
387 1
            $streams->copyHandle($this->streams, 2);
388
        }
389
390 11
        return $streams;
391
    }
392
393
    /**
394
     * @throws InvalidArgumentException
395
     * @throws StatusException
396
     * @throws ArgsException
397
     *
398
     * @return string current working directory
399
     */
400 25
    private function parseWorkingDir()
401
    {
402 25
        $args = $this->arguments;
403
404 25
        $buffer = $args->getStringOptionArgument('working-dir', '');
405
406 25
        if ('' !== $buffer) {
407 2
            $this->changeWorkingDir($buffer);
408
        }
409
410 24
        return $this->getWorkingDirectory();
411
    }
412
413
    /**
414
     * @param string $directory
415
     *
416
     * @throws StatusException
417
     *
418
     * @return void
419
     */
420 2
    private function changeWorkingDir($directory)
421
    {
422 2
        $message = sprintf('changing working directory to %s', $directory);
423 2
        $this->verbose(sprintf('info: %s', $message));
424
425 2
        $result = chdir($directory);
426 2
        if (false === $result) {
427 1
            throw new StatusException(sprintf('fatal: %s', $message), 2);
428
        }
429 1
    }
430
431
    /**
432
     * Obtain pipeline to run from file while handling error output
433
     *
434
     * @param File $pipelines
435
     * @param string $pipelineId
436
     * @param FileOptions $fileOptions
437
     * @param RunOpts $runOpts
438
     *
439
     * @throws ParseException
440
     * @throws StatusException
441
     *
442
     * @return Pipeline on success
443
     */
444 10
    private function getRunPipeline(File $pipelines, $pipelineId, FileOptions $fileOptions, RunOpts $runOpts)
445
    {
446 10
        $this->verbose(sprintf("info: running pipeline '%s'", $pipelineId));
447
448 10
        $pipeline = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $pipeline is dead and can be removed.
Loading history...
449
450
        try {
451 10
            $pipeline = $pipelines->getById($pipelineId);
452 2
        } catch (ParseException $e) {
453 1
            $this->error(sprintf("pipelines: error: pipeline id '%s'", $pipelineId));
454
455 1
            throw $e;
456 1
        } catch (InvalidArgumentException $e) {
457 1
            $this->error(sprintf("pipelines: pipeline '%s' unavailable", $pipelineId));
458 1
            $this->info('Pipelines are:');
459 1
            $fileOptions->showPipelines($pipelines);
460
461 1
            throw new StatusException('', 1);
462
        }
463
464 8
        if (!$pipeline) {
465 1
            throw new StatusException('no pipeline to run!', 1);
466
        }
467
468 7
        $pipeline->setStepsExpression($runOpts->getSteps());
469
470 7
        return $pipeline;
471
    }
472
473
    /**
474
     * @param string $message
475
     *
476
     * @return void
477
     */
478 2
    private function error($message)
479
    {
480 2
        $this->streams->err(
481 2
            sprintf("%s\n", $message)
482
        );
483 2
    }
484
485
    /**
486
     * @param string $message
487
     *
488
     * @return void
489
     */
490 6
    private function info($message)
491
    {
492 6
        $this->streams->out(
493 6
            sprintf("%s\n", $message)
494
        );
495 6
    }
496
497
    /**
498
     * @param string $message
499
     *
500
     * @return void
501
     */
502 24
    private function verbose($message)
503
    {
504 24
        if ($this->verbose) {
505 5
            $this->info($message);
506
        }
507 24
    }
508
}
509