App   B
last analyzed

Complexity

Total Complexity 44

Size/Duplication

Total Lines 464
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 7
Bugs 0 Features 1
Metric Value
wmc 44
eloc 151
c 7
b 0
f 1
dl 0
loc 464
ccs 151
cts 151
cp 1
rs 8.8798

19 Methods

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

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