Builder   A
last analyzed

Complexity

Total Complexity 33

Size/Duplication

Total Lines 414
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 12

Test Coverage

Coverage 0%

Importance

Changes 7
Bugs 5 Features 2
Metric Value
wmc 33
c 7
b 5
f 2
lcom 1
cbo 12
dl 0
loc 414
ccs 0
cts 142
cp 0
rs 9.3999

17 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 25 2
A setConfigArray() 0 8 3
A getConfig() 0 10 2
A getSystemConfig() 0 4 1
A getBuildProjectTitle() 0 4 1
C execute() 0 69 9
A executeCommand() 0 4 1
A getLastOutput() 0 4 1
A logExecOutput() 0 4 1
A findBinary() 0 4 1
A interpolate() 0 4 1
B setupBuild() 0 31 5
A setLogger() 0 4 1
A log() 0 4 1
A logSuccess() 0 4 1
A logFailure() 0 4 1
B buildPluginFactory() 0 42 1
1
<?php
2
/**
3
 * PHPCI - Continuous Integration for PHP.
4
 *
5
 * @copyright    Copyright 2014, Block 8 Limited.
6
 * @license      https://github.com/Block8/PHPCI/blob/master/LICENSE.md
7
 *
8
 * @link         https://www.phptesting.org/
9
 */
10
11
namespace PHPCI;
12
13
use PHPCI\Helper\BuildInterpolator;
14
use PHPCI\Helper\Lang;
15
use PHPCI\Helper\MailerFactory;
16
use PHPCI\Logging\BuildLogger;
17
use PHPCI\Model\Build;
18
use b8\Config;
19
use b8\Store\Factory;
20
use Psr\Log\LoggerAwareInterface;
21
use Psr\Log\LoggerInterface;
22
use Psr\Log\LogLevel;
23
use PHPCI\Plugin\Util\Factory as PluginFactory;
24
25
/**
26
 * PHPCI Build Runner.
27
 *
28
 * @author   Dan Cryer <[email protected]>
29
 */
30
class Builder implements LoggerAwareInterface
31
{
32
    /**
33
     * @var string
34
     */
35
    public $buildPath;
36
37
    /**
38
     * @var string[]
39
     */
40
    public $ignore = array();
41
42
    /**
43
     * @var string
44
     */
45
    protected $directory;
46
47
    /**
48
     * @var bool
49
     */
50
    protected $verbose = true;
51
52
    /**
53
     * @var \PHPCI\Model\Build
54
     */
55
    protected $build;
56
57
    /**
58
     * @var LoggerInterface
59
     */
60
    protected $logger;
61
62
    /**
63
     * @var array
64
     */
65
    protected $config;
66
67
    /**
68
     * @var string
69
     */
70
    protected $lastOutput;
71
72
    /**
73
     * @var BuildInterpolator
74
     */
75
    protected $interpolator;
76
77
    /**
78
     * @var \PHPCI\Store\BuildStore
79
     */
80
    protected $store;
81
82
    /**
83
     * @var bool
84
     */
85
    public $quiet = false;
86
87
    /**
88
     * @var \PHPCI\Plugin\Util\Executor
89
     */
90
    protected $pluginExecutor;
91
92
    /**
93
     * @var Helper\CommandExecutor
94
     */
95
    protected $commandExecutor;
96
97
    /**
98
     * @var Logging\BuildLogger
99
     */
100
    protected $buildLogger;
101
102
    /**
103
     * Set up the builder.
104
     *
105
     * @param \PHPCI\Model\Build $build
106
     * @param LoggerInterface    $logger
107
     */
108
    public function __construct(Build $build, LoggerInterface $logger = null)
109
    {
110
        $this->build = $build;
111
        $this->store = Factory::getStore('Build');
112
113
        $this->buildLogger = new BuildLogger($logger, $build);
0 ignored issues
show
Bug introduced by
It seems like $logger defined by parameter $logger on line 108 can be null; however, PHPCI\Logging\BuildLogger::__construct() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
114
115
        $pluginFactory = $this->buildPluginFactory($build);
116
        $pluginFactory->addConfigFromFile(PHPCI_DIR.'/pluginconfig.php');
117
        $this->pluginExecutor = new Plugin\Util\Executor($pluginFactory, $this->buildLogger);
118
119
        $executorClass = 'PHPCI\Helper\UnixCommandExecutor';
120
        if (IS_WIN) {
121
            $executorClass = 'PHPCI\Helper\WindowsCommandExecutor';
122
        }
123
124
        $this->commandExecutor = new $executorClass(
125
            $this->buildLogger,
126
            PHPCI_DIR,
127
            $this->quiet,
128
            $this->verbose
129
        );
130
131
        $this->interpolator = new BuildInterpolator();
132
    }
133
134
    /**
135
     * Set the config array, as read from phpci.yml.
136
     *
137
     * @param array|null $config
138
     *
139
     * @throws \Exception
140
     */
141
    public function setConfigArray($config)
142
    {
143
        if (is_null($config) || !is_array($config)) {
144
            throw new \Exception(Lang::get('missing_phpci_yml'));
145
        }
146
147
        $this->config = $config;
148
    }
149
150
    /**
151
     * Access a variable from the phpci.yml file.
152
     *
153
     * @param string
154
     * @param string $key
155
     *
156
     * @return mixed
157
     */
158
    public function getConfig($key)
159
    {
160
        $rtn = null;
161
162
        if (isset($this->config[$key])) {
163
            $rtn = $this->config[$key];
164
        }
165
166
        return $rtn;
167
    }
168
169
    /**
170
     * Access a variable from the config.yml.
171
     *
172
     * @param string $key
173
     *
174
     * @return mixed
175
     */
176
    public function getSystemConfig($key)
177
    {
178
        return Config::getInstance()->get($key);
179
    }
180
181
    /**
182
     * @return string The title of the project being built.
183
     */
184
    public function getBuildProjectTitle()
185
    {
186
        return $this->build->getProject()->getTitle();
187
    }
188
189
    /**
190
     * Run the active build.
191
     */
192
    public function execute()
193
    {
194
        // Update the build in the database, ping any external services.
195
        $this->build->setStatus(Build::STATUS_RUNNING);
196
        $this->build->setStarted(new \DateTime());
197
        $this->store->save($this->build);
198
        $this->build->sendStatusPostback();
199
        $success = true;
200
201
        $previous_build = $this->build->getProject()->getPreviousBuild($this->build->getBranch());
202
203
        $previous_state = Build::STATUS_NEW;
204
205
        if ($previous_build) {
206
            $previous_state = $previous_build->getStatus();
207
        }
208
209
        try {
210
            // Set up the build:
211
            $this->setupBuild();
212
213
            // Run the core plugin stages:
214
            foreach (array('setup', 'test') as $stage) {
215
                $success &= $this->pluginExecutor->executePlugins($this->config, $stage);
216
            }
217
218
            // Set the status so this can be used by complete, success and failure
219
            // stages.
220
            if ($success) {
221
                $this->build->setStatus(Build::STATUS_SUCCESS);
222
            } else {
223
                $this->build->setStatus(Build::STATUS_FAILED);
224
            }
225
226
            // Complete stage plugins are always run
227
            $this->pluginExecutor->executePlugins($this->config, 'complete');
228
229
            if ($success) {
230
                $this->pluginExecutor->executePlugins($this->config, 'success');
231
232
                if ($previous_state == Build::STATUS_FAILED) {
233
                    $this->pluginExecutor->executePlugins($this->config, 'fixed');
234
                }
235
236
                $this->buildLogger->logSuccess(Lang::get('build_success'));
237
            } else {
238
                $this->pluginExecutor->executePlugins($this->config, 'failure');
239
240
                if ($previous_state == Build::STATUS_SUCCESS || $previous_state == Build::STATUS_NEW) {
241
                    $this->pluginExecutor->executePlugins($this->config, 'broken');
242
                }
243
244
                $this->buildLogger->logFailure(Lang::get('build_failed'));
245
            }
246
        } catch (\Exception $ex) {
247
            $this->build->setStatus(Build::STATUS_FAILED);
248
            $this->buildLogger->logFailure(Lang::get('exception').$ex->getMessage());
249
        }
250
251
        // Update the build in the database, ping any external services, etc.
252
        $this->build->sendStatusPostback();
253
        $this->build->setFinished(new \DateTime());
254
255
        // Clean up:
256
        $this->buildLogger->log(Lang::get('removing_build'));
257
        $this->build->removeBuildDirectory();
258
259
        $this->store->save($this->build);
260
    }
261
262
    /**
263
     * Used by this class, and plugins, to execute shell commands.
264
     */
265
    public function executeCommand()
266
    {
267
        return $this->commandExecutor->executeCommand(func_get_args());
0 ignored issues
show
Unused Code introduced by
The call to CommandExecutor::executeCommand() has too many arguments starting with func_get_args().

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
268
    }
269
270
    /**
271
     * Returns the output from the last command run.
272
     */
273
    public function getLastOutput()
274
    {
275
        return $this->commandExecutor->getLastOutput();
276
    }
277
278
    /**
279
     * Specify whether exec output should be logged.
280
     *
281
     * @param bool $enableLog
282
     */
283
    public function logExecOutput($enableLog = true)
284
    {
285
        $this->commandExecutor->logExecOutput = $enableLog;
0 ignored issues
show
Bug introduced by
Accessing logExecOutput on the interface PHPCI\Helper\CommandExecutor suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
286
    }
287
288
    /**
289
     * Find a binary required by a plugin.
290
     *
291
     * @param string $binary
292
     * @param bool   $quiet
293
     *
294
     * @return null|string
295
     */
296
    public function findBinary($binary, $quiet = false)
0 ignored issues
show
Unused Code introduced by
The parameter $quiet 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...
297
    {
298
        return $this->commandExecutor->findBinary($binary, $quiet = false);
299
    }
300
301
    /**
302
     * Replace every occurrence of the interpolation vars in the given string
303
     * Example: "This is build %PHPCI_BUILD%" => "This is build 182".
304
     *
305
     * @param string $input
306
     *
307
     * @return string
308
     */
309
    public function interpolate($input)
310
    {
311
        return $this->interpolator->interpolate($input);
312
    }
313
314
    /**
315
     * Set up a working copy of the project for building.
316
     */
317
    protected function setupBuild()
318
    {
319
        $this->buildPath = $this->build->getBuildPath();
320
321
        $this->interpolator->setupInterpolationVars(
322
            $this->build,
323
            $this->buildPath,
324
            PHPCI_URL
325
        );
326
327
        $this->commandExecutor->setBuildPath($this->buildPath);
328
329
        // Create a working copy of the project:
330
        if (!$this->build->createWorkingCopy($this, $this->buildPath)) {
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class PHPCI\Model\Build as the method createWorkingCopy() does only exist in the following sub-classes of PHPCI\Model\Build: PHPCI\Model\Build\BitbucketBuild, PHPCI\Model\Build\GithubBuild, PHPCI\Model\Build\GitlabBuild, PHPCI\Model\Build\LocalBuild, PHPCI\Model\Build\MercurialBuild, PHPCI\Model\Build\RemoteGitBuild, PHPCI\Model\Build\SubversionBuild. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
331
            throw new \Exception(Lang::get('could_not_create_working'));
332
        }
333
334
        // Does the project's phpci.yml request verbose mode?
335
        if (!isset($this->config['build_settings']['verbose']) || !$this->config['build_settings']['verbose']) {
336
            $this->verbose = false;
337
        }
338
339
        // Does the project have any paths it wants plugins to ignore?
340
        if (isset($this->config['build_settings']['ignore'])) {
341
            $this->ignore = $this->config['build_settings']['ignore'];
342
        }
343
344
        $this->buildLogger->logSuccess(Lang::get('working_copy_created', $this->buildPath));
345
346
        return true;
347
    }
348
349
    /**
350
     * Sets a logger instance on the object.
351
     *
352
     * @param LoggerInterface $logger
353
     *
354
     * @return null
355
     */
356
    public function setLogger(LoggerInterface $logger)
357
    {
358
        $this->buildLogger->setLogger($logger);
359
    }
360
361
    /**
362
     * Write to the build log.
363
     *
364
     * @param $message
365
     * @param string $level
366
     * @param array  $context
367
     */
368
    public function log($message, $level = LogLevel::INFO, $context = array())
369
    {
370
        $this->buildLogger->log($message, $level, $context);
371
    }
372
373
    /**
374
     * Add a success-coloured message to the log.
375
     *
376
     * @param string
377
     * @param string $message
378
     */
379
    public function logSuccess($message)
380
    {
381
        $this->buildLogger->logSuccess($message);
382
    }
383
384
    /**
385
     * Add a failure-coloured message to the log.
386
     *
387
     * @param string     $message
388
     * @param \Exception $exception The exception that caused the error.
389
     */
390
    public function logFailure($message, \Exception $exception = null)
391
    {
392
        $this->buildLogger->logFailure($message, $exception);
393
    }
394
    /**
395
     * Returns a configured instance of the plugin factory.
396
     *
397
     * @param Build $build
398
     *
399
     * @return PluginFactory
400
     */
401
    private function buildPluginFactory(Build $build)
402
    {
403
        $pluginFactory = new PluginFactory();
404
405
        $self = $this;
406
        $pluginFactory->registerResource(
407
            function () use ($self) {
408
                return $self;
409
            },
410
            null,
411
            'PHPCI\Builder'
412
        );
413
414
        $pluginFactory->registerResource(
415
            function () use ($build) {
416
                return $build;
417
            },
418
            null,
419
            'PHPCI\Model\Build'
420
        );
421
422
        $logger = $this->logger;
423
        $pluginFactory->registerResource(
424
            function () use ($logger) {
425
                return $logger;
426
            },
427
            null,
428
            'Psr\Log\LoggerInterface'
429
        );
430
431
        $pluginFactory->registerResource(
432
            function () use ($self) {
433
                $factory = new MailerFactory($self->getSystemConfig('phpci'));
434
435
                return $factory->getSwiftMailerFromConfig();
436
            },
437
            null,
438
            'Swift_Mailer'
439
        );
440
441
        return $pluginFactory;
442
    }
443
}
444