Builder::executeCommand()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
1
<?php
2
3
namespace Fabrica\Tools;
4
5
use Fabrica\Helper\BuildInterpolator;
6
use Fabrica\Helper\MailerFactory;
7
use Fabrica\Tools\Logging\BuildLogger;
8
use Fabrica\Models\Infra\Ci\Build;
9
use Fabrica\Tools\Plugin\Util\Factory as PluginFactory;
10
use Fabrica\Tools\Store\BuildErrorWriter;
11
use Fabrica\Tools\Store\Factory;
12
use Psr\Log\LoggerAwareInterface;
13
use Psr\Log\LoggerInterface;
14
use Psr\Log\LogLevel;
15
16
/**
17
 * @author Ricardo Sierra <[email protected]>
18
 */
19
class Builder implements LoggerAwareInterface
20
{
21
    /**
22
     * @var string
23
     */
24
    public $buildPath;
25
26
    /**
27
     * @var string[]
28
     */
29
    public $ignore = [];
30
31
    /**
32
     * @var string[]
33
     */
34
    public $binaryPath = '';
35
36
    /**
37
     * @var string[]
38
     */
39
    public $priorityPath = 'local';
40
41
    /**
42
     * @var string
43
     */
44
    public $directory;
45
46
    /**
47
     * @var string|null
48
     */
49
    protected $currentStage = null;
50
51
    /**
52
     * @var bool
53
     */
54
    protected $verbose = true;
55
56
    /**
57
     * @var \PHPCensor\Model\Build
58
     */
59
    protected $build;
60
61
    /**
62
     * @var LoggerInterface
63
     */
64
    protected $logger;
65
66
    /**
67
     * @var array
68
     */
69
    protected $config = [];
70
71
    /**
72
     * @var string
73
     */
74
    protected $lastOutput;
75
76
    /**
77
     * @var BuildInterpolator
78
     */
79
    protected $interpolator;
80
81
    /**
82
     * @var \Fabrica\Tools\Store\BuildStore
83
     */
84
    protected $store;
85
86
    /**
87
     * @var \PHPCensor\Plugin\Util\Executor
88
     */
89
    protected $pluginExecutor;
90
91
    /**
92
     * @var Helper\CommandExecutorInterface
93
     */
94
    protected $commandExecutor;
95
96
    /**
97
     * @var Logging\BuildLogger
98
     */
99
    protected $buildLogger;
100
101
    /**
102
     * @var BuildErrorWriter
103
     */
104
    private $buildErrorWriter;
105
106
    /**
107
     * Set up the builder.
108
     *
109
     * @param \PHPCensor\Model\Build $build
110
     * @param LoggerInterface        $logger
111
     */
112
    public function __construct(Build $build, LoggerInterface $logger = null)
113
    {
114
        $this->build = $build;
115
        $this->store = Factory::getStore('Build');
116
117
        $this->buildLogger    = new BuildLogger($logger, $build);
118
        $pluginFactory        = $this->buildPluginFactory($build);
119
        $this->pluginExecutor = new Plugin\Util\Executor($pluginFactory, $this->buildLogger);
0 ignored issues
show
Documentation Bug introduced by
It seems like new \Fabrica\Tools\Plugi...ry, $this->buildLogger) of type object<Fabrica\Tools\Plugin\Util\Executor> is incompatible with the declared type object<PHPCensor\Plugin\Util\Executor> of property $pluginExecutor.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
120
121
        $executorClass         = 'PHPCensor\Helper\CommandExecutor';
122
        $this->commandExecutor = new $executorClass(
123
            $this->buildLogger,
124
            ROOT_DIR,
125
            $this->verbose
126
        );
127
128
        $this->interpolator     = new BuildInterpolator();
129
        $this->buildErrorWriter = new BuildErrorWriter($this->build->getProjectId(), $this->build->getId());
130
    }
131
132
    /**
133
     * @return BuildLogger
134
     */
135
    public function getBuildLogger()
136
    {
137
        return $this->buildLogger;
138
    }
139
140
    /**
141
     * @return null|string
142
     */
143
    public function getCurrentStage()
144
    {
145
        return $this->currentStage;
146
    }
147
148
    /**
149
     * Set the config array, as read from .php-censor.yml
150
     *
151
     * @param array $config
152
     *
153
     * @throws \Exception
154
     */
155
    public function setConfig(array $config)
156
    {
157
        $this->config = $config;
158
    }
159
160
    /**
161
     * Access a variable from the .php-censor.yml file.
162
     *
163
     * @param string $key
164
     *
165
     * @return mixed
166
     */
167
    public function getConfig($key = null)
168
    {
169
        $value = null;
170
        if (null === $key) {
171
            $value = $this->config;
172
        } elseif (isset($this->config[$key])) {
173
            $value = $this->config[$key];
174
        }
175
176
        return $value;
177
    }
178
179
    /**
180
     * Access a variable from the config.yml
181
     *
182
     * @param string $key
183
     *
184
     * @return mixed
185
     */
186
    public function getSystemConfig($key)
187
    {
188
        return Config::getInstance()->get($key);
189
    }
190
191
    /**
192
     * @return string The title of the project being built.
193
     *
194
     * @throws Exception\HttpException
195
     */
196
    public function getBuildProjectTitle()
197
    {
198
        return $this->build->getProject()->getTitle();
199
    }
200
201
    /**
202
     * @throws Exception\HttpException
203
     * @throws Exception\InvalidArgumentException
204
     */
205
    public function execute()
206
    {
207
        $this->build->setStatusRunning();
208
        $this->build->setStartDate(new \DateTime());
209
        $this->store->save($this->build);
210
        $this->build->sendStatusPostback();
211
212
        $success = true;
213
214
        $previousBuild = $this->build->getProject()->getPreviousBuild($this->build->getBranch());
215
        $previousState = Build::STATUS_PENDING;
216
217
        if ($previousBuild) {
218
            $previousState = $previousBuild->getStatus();
219
        }
220
221
        try {
222
            // Set up the build:
223
            $this->setupBuild();
224
225
            // Run the core plugin stages:
226
            foreach ([Build::STAGE_SETUP, Build::STAGE_TEST, Build::STAGE_DEPLOY] as $stage) {
227
                $this->currentStage = $stage;
228
                $success &= $this->pluginExecutor->executePlugins($this->config, $stage);
229
                if (!$success) {
230
                    break;
231
                }
232
            }
233
234
            // Set the status so this can be used by complete, success and failure
235
            // stages.
236
            if ($success) {
237
                $this->build->setStatusSuccess();
238
            } else {
239
                $this->build->setStatusFailed();
240
            }
241
        } catch (\Exception $ex) {
242
            $success = false;
243
            $this->build->setStatusFailed();
244
            $this->buildLogger->logFailure('Exception: ' . $ex->getMessage(), $ex);
245
        }
246
247
        try {
248
            if ($success) {
249
                $this->currentStage = Build::STAGE_SUCCESS;
250
                $this->pluginExecutor->executePlugins($this->config, Build::STAGE_SUCCESS);
251
252 View Code Duplication
                if (Build::STATUS_FAILED === $previousState) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
253
                    $this->currentStage = Build::STAGE_FIXED;
254
                    $this->pluginExecutor->executePlugins($this->config, Build::STAGE_FIXED);
255
                }
256
            } else {
257
                $this->currentStage = Build::STAGE_FAILURE;
258
                $this->pluginExecutor->executePlugins($this->config, Build::STAGE_FAILURE);
259
260 View Code Duplication
                if (Build::STATUS_SUCCESS === $previousState || Build::STATUS_PENDING === $previousState) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
261
                    $this->currentStage = Build::STAGE_BROKEN;
262
                    $this->pluginExecutor->executePlugins($this->config, Build::STAGE_BROKEN);
263
                }
264
            }
265
        } catch (\Exception $ex) {
266
            $this->buildLogger->logFailure('Exception: ' . $ex->getMessage(), $ex);
267
        }
268
269
        $this->buildLogger->log('');
270
        if (Build::STATUS_FAILED === $this->build->getStatus()) {
271
            $this->buildLogger->logFailure('BUILD FAILED!');
272
        } else {
273
            $this->buildLogger->logSuccess('BUILD SUCCESS!');
274
        }
275
276
        // Flush errors to make them available to plugins in complete stage
277
        $this->buildErrorWriter->flush();
278
279
        try {
280
            // Complete stage plugins are always run
281
            $this->currentStage = Build::STAGE_COMPLETE;
282
            $this->pluginExecutor->executePlugins($this->config, Build::STAGE_COMPLETE);
283
        } catch (\Exception $ex) {
284
            $this->buildLogger->logFailure('Exception: ' . $ex->getMessage());
285
        }
286
287
        // Update the build in the database, ping any external services, etc.
288
        $this->build->sendStatusPostback();
289
        $this->build->setFinishDate(new \DateTime());
290
291
        $removeBuilds = (bool)Config::getInstance()->get('php-censor.build.remove_builds', true);
292
        if ($removeBuilds) {
293
            // Clean up:
294
            $this->buildLogger->log('');
295
            $this->buildLogger->logSuccess('REMOVING BUILD.');
296
            $this->build->removeBuildDirectory();
297
        }
298
299
        $this->buildErrorWriter->flush();
300
301
        $this->setErrorTrend();
302
303
        $this->store->save($this->build);
304
    }
305
306
    /**
307
     * @throws Exception\HttpException
308
     * @throws Exception\InvalidArgumentException
309
     */
310
    protected function setErrorTrend()
311
    {
312
        $this->build->setErrorsTotal($this->store->getErrorsCount($this->build->getId()));
313
314
        $trend = $this->store->getBuildErrorsTrend(
315
            $this->build->getId(),
316
            $this->build->getProjectId(),
317
            $this->build->getBranch()
318
        );
319
320 View Code Duplication
        if (isset($trend[1])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
321
            $previousBuild = $this->store->getById($trend[1]['build_id']);
322
            if ($previousBuild 
323
                && !in_array(
324
                    $previousBuild->getStatus(),
325
                    [Build::STATUS_PENDING, Build::STATUS_RUNNING],
326
                    true
327
                )
328
            ) {
329
                $this->build->setErrorsTotalPrevious((int)$trend[1]['count']);
330
            }
331
        }
332
    }
333
334
    /**
335
     * Used by this class, and plugins, to execute shell commands.
336
     *
337
     * @param array ...$params
338
     *
339
     * @return bool
340
     */
341
    public function executeCommand(...$params)
342
    {
343
        return $this->commandExecutor->executeCommand($params);
344
    }
345
346
    /**
347
     * Returns the output from the last command run.
348
     *
349
     * @return string
350
     */
351
    public function getLastOutput()
352
    {
353
        return $this->commandExecutor->getLastOutput();
354
    }
355
356
    /**
357
     * Specify whether exec output should be logged.
358
     *
359
     * @param bool $enableLog
360
     */
361
    public function logExecOutput($enableLog = true)
362
    {
363
        $this->commandExecutor->logExecOutput = $enableLog;
364
    }
365
366
    /**
367
     * Find a binary required by a plugin.
368
     *
369
     * @param  array|string $binary
370
     * @param  string       $priorityPath
371
     * @param  string       $binaryPath
372
     * @param  array        $binaryName
373
     * @return string
374
     *
375
     * @throws \Exception when no binary has been found.
376
     */
377
    public function findBinary($binary, $priorityPath = 'local', $binaryPath = '', $binaryName = [])
378
    {
379
        return $this->commandExecutor->findBinary($binary, $priorityPath, $binaryPath, $binaryName);
380
    }
381
382
    /**
383
     * Replace every occurrence of the interpolation vars in the given string
384
     * Example: "This is build %PHPCI_BUILD%" => "This is build 182"
385
     *
386
     * @param string $input
387
     *
388
     * @return string
389
     */
390
    public function interpolate($input)
391
    {
392
        return $this->interpolator->interpolate($input);
393
    }
394
395
    /**
396
     * Set up a working copy of the project for building.
397
     *
398
     * @throws \Exception
399
     *
400
     * @return bool
401
     */
402
    protected function setupBuild()
403
    {
404
        $this->buildPath = $this->build->getBuildPath();
405
406
        $this->commandExecutor->setBuildPath($this->buildPath);
407
408
        $this->build->handleConfigBeforeClone($this);
409
410
        // Create a working copy of the project:
411
        if (!$this->build->createWorkingCopy($this, $this->buildPath)) {
412
            throw new \Exception('Could not create a working copy.');
413
        }
414
415
        chdir($this->buildPath);
416
417
        $this->interpolator->setupInterpolationVars(
418
            $this->build,
419
            APP_URL
420
        );
421
422
        // Does the project's .php-censor.yml request verbose mode?
423
        if (!isset($this->config['build_settings']['verbose']) || !$this->config['build_settings']['verbose']) {
424
            $this->verbose = false;
425
        }
426
427
        // Does the project have any paths it wants plugins to ignore?
428
        if (!empty($this->config['build_settings']['ignore'])) {
429
            $this->ignore = $this->config['build_settings']['ignore'];
430
        }
431
432
        if (!empty($this->config['build_settings']['binary_path'])) {
433
            $this->binaryPath = rtrim(
0 ignored issues
show
Documentation Bug introduced by
It seems like rtrim($this->interpolate...y_path']), '/\\') . '/' of type string is incompatible with the declared type array<integer,string> of property $binaryPath.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
434
                $this->interpolate($this->config['build_settings']['binary_path']),
435
                '/\\'
436
            ) . '/';
437
        }
438
439
        if (!empty($this->config['build_settings']['priority_path']) 
440
            && in_array(
441
                $this->config['build_settings']['priority_path'],
442
                Plugin::AVAILABLE_PRIORITY_PATHS,
443
                true
444
            )
445
        ) {
446
            $this->priorityPath = $this->config['build_settings']['priority_path'];
447
        }
448
449
        $directory = $this->buildPath;
450
451
        // Does the project have a global directory for plugins ?
452
        if (!empty($this->config['build_settings']['directory'])) {
453
            $directory = $this->config['build_settings']['directory'];
454
        }
455
456
        $this->directory = rtrim(
457
            $this->interpolate($directory),
458
            '/\\'
459
        ) . '/';
460
461
        $this->buildLogger->logSuccess(sprintf('Working copy created: %s', $this->buildPath));
462
463
        return true;
464
    }
465
466
    /**
467
     * Sets a logger instance on the object
468
     *
469
     * @param LoggerInterface $logger
470
     */
471
    public function setLogger(LoggerInterface $logger)
472
    {
473
        $this->buildLogger->setLogger($logger);
474
    }
475
476
    /**
477
     * Write to the build log.
478
     *
479
     * @param string $message
480
     * @param string $level
481
     * @param array  $context
482
     */
483
    public function log($message, $level = LogLevel::INFO, $context = [])
484
    {
485
        $this->buildLogger->log($message, $level, $context);
486
    }
487
488
    /**
489
     * Add a warning-coloured message to the log.
490
     *
491
     * @param string $message
492
     */
493
    public function logWarning($message)
494
    {
495
        $this->buildLogger->logWarning($message);
496
    }
497
498
    /**
499
     * Add a success-coloured message to the log.
500
     *
501
     * @param string $message
502
     */
503
    public function logSuccess($message)
504
    {
505
        $this->buildLogger->logSuccess($message);
506
    }
507
508
    /**
509
     * Add a failure-coloured message to the log.
510
     *
511
     * @param string     $message
512
     * @param \Exception $exception The exception that caused the error.
513
     */
514
    public function logFailure($message, \Exception $exception = null)
515
    {
516
        $this->buildLogger->logFailure($message, $exception);
517
    }
518
519
    /**
520
     * Add a debug-coloured message to the log.
521
     *
522
     * @param string $message
523
     */
524
    public function logDebug($message)
525
    {
526
        $this->buildLogger->logDebug($message);
527
    }
528
529
    /**
530
     * Returns a configured instance of the plugin factory.
531
     *
532
     * @param Build $build
533
     *
534
     * @return PluginFactory
535
     */
536
    private function buildPluginFactory(Build $build)
537
    {
538
        $pluginFactory = new PluginFactory();
539
540
        $self = $this;
541
        $pluginFactory->registerResource(
542
            function () use ($self) {
543
                return $self;
544
            },
545
            null,
546
            'PHPCensor\Builder'
547
        );
548
549
        $pluginFactory->registerResource(
550
            function () use ($build) {
551
                return $build;
552
            },
553
            null,
554
            'PHPCensor\Model\Build'
555
        );
556
557
        $logger = $this->logger;
558
        $pluginFactory->registerResource(
559
            function () use ($logger) {
560
                return $logger;
561
            },
562
            null,
563
            'Psr\Log\LoggerInterface'
564
        );
565
566
        $pluginFactory->registerResource(
567
            function () use ($self) {
568
                $factory = new MailerFactory($self->getSystemConfig('php-censor'));
569
                return $factory->getSwiftMailerFromConfig();
570
            },
571
            null,
572
            'Swift_Mailer'
573
        );
574
575
        return $pluginFactory;
576
    }
577
578
    /**
579
     * @return BuildErrorWriter
580
     */
581
    public function getBuildErrorWriter()
582
    {
583
        return $this->buildErrorWriter;
584
    }
585
}
586