Deployer   A
last analyzed

Complexity

Total Complexity 35

Size/Duplication

Total Lines 320
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 32

Test Coverage

Coverage 71.3%

Importance

Changes 0
Metric Value
dl 0
loc 320
ccs 82
cts 115
cp 0.713
rs 9.6
c 0
b 0
f 0
wmc 35
lcom 1
cbo 32

15 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 95 4
A get() 0 4 1
A setDefault() 0 4 1
A getDefault() 0 4 2
A hasDefault() 0 4 1
A addDefault() 0 12 3
A init() 0 11 1
A addConsoleCommands() 0 12 3
A __get() 0 8 2
A getConsole() 0 4 1
A getInput() 0 4 1
A getOutput() 0 4 1
A getHelper() 0 4 1
A run() 0 31 2
C collectAnonymousStats() 0 35 11
1
<?php
2
/* (c) Anton Medvedev <[email protected]>
3
 *
4
 * For the full copyright and license information, please view the LICENSE
5
 * file that was distributed with this source code.
6
 */
7
8
namespace Deployer;
9
10
use Deployer\Collection\Collection;
11
use Deployer\Console\Application;
12
use Deployer\Console\AutocompleteCommand;
13
use Deployer\Console\CommandEvent;
14
use Deployer\Console\DebugCommand;
15
use Deployer\Console\InitCommand;
16
use Deployer\Console\Output\Informer;
17
use Deployer\Console\Output\OutputWatcher;
18
use Deployer\Console\RunCommand;
19
use Deployer\Console\SshCommand;
20
use Deployer\Console\TaskCommand;
21
use Deployer\Console\WorkerCommand;
22
use Deployer\Executor\ParallelExecutor;
23
use Deployer\Executor\SeriesExecutor;
24
use Deployer\Logger\Handler\FileHandler;
25
use Deployer\Logger\Handler\NullHandler;
26
use Deployer\Logger\Logger;
27
use Deployer\Task;
28
use Deployer\Utility\ProcessOutputPrinter;
29
use Deployer\Utility\ProcessRunner;
30
use Deployer\Utility\Reporter;
31
use Deployer\Utility\Rsync;
32
use Pimple\Container;
33
use Symfony\Component\Console;
34
use Symfony\Component\Console\Input\ArgvInput;
35
use Symfony\Component\Console\Output\ConsoleOutput;
36
use Symfony\Component\Console\Style\SymfonyStyle;
37
use function Deployer\Support\array_merge_alternate;
38
39
/**
40
 * Deployer class represents DI container for configuring
41
 *
42
 * @property Application console
43
 * @property Task\TaskCollection|Task\Task[] tasks
44
 * @property Host\HostCollection|Collection|Host\Host[] hosts
45
 * @property Collection config
46
 * @property Rsync rsync
47
 * @property Ssh\Client sshClient
48
 * @property ProcessRunner processRunner
49
 * @property Task\ScriptManager scriptManager
50
 * @property Host\HostSelector hostSelector
51
 * @property SeriesExecutor seriesExecutor
52
 * @property ParallelExecutor parallelExecutor
53
 * @property Informer informer
54
 * @property Logger logger
55
 * @property ProcessOutputPrinter pop
56
 * @property Collection fail
57
 */
58
class Deployer extends Container
59
{
60
    /**
61
     * Global instance of deployer. It's can be accessed only after constructor call.
62
     * @var Deployer
63
     */
64
    private static $instance;
65
66
    /**
67
     * @param Application $console
68
     */
69 31
    public function __construct(Application $console)
70
    {
71 31
        parent::__construct();
72
73
        /******************************
74
         *           Console          *
75
         ******************************/
76
77
        $this['console'] = function () use ($console) {
78 16
            $console->catchIO(function ($input, $output) {
79 16
                $this['input'] = $input;
80 16
                $this['output'] =  new OutputWatcher($output);
81 16
                return [$this['input'], $this['output']];
82 16
            });
83 16
            return $console;
84
        };
85
86
        /******************************
87
         *           Config           *
88
         ******************************/
89
90 31
        $this['config'] = function () {
91 31
            return new Collection();
92
        };
93 31
        $this->config['ssh_multiplexing'] = true;
94 31
        $this->config['default_stage'] = null;
95
96
        /******************************
97
         *            Core            *
98
         ******************************/
99
100 10
        $this['pop'] = function ($c) {
101 10
            return new ProcessOutputPrinter($c['output'], $c['logger']);
102
        };
103
        $this['sshClient'] = function ($c) {
104
            return new Ssh\Client($c['output'], $c['pop'], $c['config']['ssh_multiplexing']);
105
        };
106
        $this['rsync'] = function ($c) {
107
            return new Rsync($c['pop']);
108
        };
109 10
        $this['processRunner'] = function ($c) {
110 10
            return new ProcessRunner($c['pop']);
111
        };
112 20
        $this['tasks'] = function () {
113 20
            return new Task\TaskCollection();
114
        };
115 20
        $this['hosts'] = function () {
116 20
            return new Host\HostCollection();
117
        };
118 18
        $this['scriptManager'] = function ($c) {
119 18
            return new Task\ScriptManager($c['tasks']);
120
        };
121 16
        $this['hostSelector'] = function ($c) {
122 16
            $defaultStage = $c['config']['default_stage'];
123 16
            if (is_object($defaultStage) && ($defaultStage instanceof \Closure)) {
124
                $defaultStage = call_user_func($defaultStage);
125
            }
126 16
            return new Host\HostSelector($c['hosts'], $defaultStage);
127
        };
128 11
        $this['fail'] = function () {
129 11
            return new Collection();
130
        };
131 16
        $this['informer'] = function ($c) {
132 16
            return new Informer($c['output']);
133
        };
134 11
        $this['seriesExecutor'] = function ($c) {
135 11
            return new SeriesExecutor($c['input'], $c['output'], $c['informer']);
136
        };
137 5
        $this['parallelExecutor'] = function ($c) {
138 5
            return new ParallelExecutor($c['input'], $c['output'], $c['informer'], $c['console']);
139
        };
140
141
        /******************************
142
         *           Logger           *
143
         ******************************/
144
145 11
        $this['log_handler'] = function () {
146 11
            return !empty($this->config['log_file'])
147
                ? new FileHandler($this->config['log_file'])
148 11
                : new NullHandler();
149
        };
150 11
        $this['logger'] = function () {
151 11
            return new Logger($this['log_handler']);
152
        };
153
154
        /******************************
155
         *        Init command        *
156
         ******************************/
157
158 16
        $this['init_command'] = function () {
159 16
            return new InitCommand();
160
        };
161
162 31
        self::$instance = $this;
163 31
    }
164
165
    /**
166
     * @return Deployer
167
     */
168 33
    public static function get()
169
    {
170 33
        return self::$instance;
171
    }
172
173
    /**
174
     * @param string $name
175
     * @param mixed $value
176
     */
177 15
    public static function setDefault($name, $value)
178
    {
179 15
        Deployer::get()->config[$name] = $value;
180 15
    }
181
182
    /**
183
     * @param string $name
184
     * @param mixed $default
185
     * @return mixed
186
     */
187 15
    public static function getDefault($name, $default = null)
188
    {
189 15
        return self::hasDefault($name) ? Deployer::get()->config[$name] : $default;
190
    }
191
192
    /**
193
     * @param string $name
194
     * @return boolean
195
     */
196 15
    public static function hasDefault($name)
197
    {
198 15
        return isset(Deployer::get()->config[$name]);
199
    }
200
201
    /**
202
     * @param string $name
203
     * @param array $array
204
     */
205 2
    public static function addDefault($name, $array)
206
    {
207 2
        if (self::hasDefault($name)) {
208 2
            $config = self::getDefault($name);
209 2
            if (!is_array($config)) {
210 1
                throw new \RuntimeException("Configuration parameter `$name` isn't array.");
211
            }
212 1
            self::setDefault($name, array_merge_alternate($config, $array));
213
        } else {
214
            self::setDefault($name, $array);
215
        }
216 1
    }
217
218
    /**
219
     * Init console application
220
     */
221 16
    public function init()
222
    {
223 16
        $this->addConsoleCommands();
224 16
        $this->getConsole()->add(new WorkerCommand($this));
225 16
        $this->getConsole()->add($this['init_command']);
226 16
        $this->getConsole()->add(new SshCommand($this));
227 16
        $this->getConsole()->add(new RunCommand($this));
228 16
        $this->getConsole()->add(new DebugCommand($this));
229 16
        $this->getConsole()->add(new AutocompleteCommand());
230 16
        $this->getConsole()->afterRun([$this, 'collectAnonymousStats']);
231 16
    }
232
233
    /**
234
     * Transform tasks to console commands.
235
     */
236 16
    public function addConsoleCommands()
237
    {
238 16
        $this->getConsole()->addUserArgumentsAndOptions();
239
240 16
        foreach ($this->tasks as $name => $task) {
241 16
            if ($task->isPrivate()) {
242 11
                continue;
243
            }
244
245 16
            $this->getConsole()->add(new TaskCommand($name, $task->getDescription(), $this));
246
        }
247 16
    }
248
249
    /**
250
     * @param string $name
251
     * @return mixed
252
     * @throws \InvalidArgumentException
253
     */
254 36
    public function __get($name)
255
    {
256 36
        if (isset($this[$name])) {
257 36
            return $this[$name];
258
        } else {
259 1
            throw new \InvalidArgumentException("Property \"$name\" does not exist.");
260
        }
261
    }
262
263
    /**
264
     * @return Application
265
     */
266 16
    public function getConsole()
267
    {
268 16
        return $this['console'];
269
    }
270
271
    /**
272
     * @return Console\Input\InputInterface
273
     */
274
    public function getInput()
275
    {
276
        return $this['input'];
277
    }
278
279
    /**
280
     * @return Console\Output\OutputInterface
281
     */
282
    public function getOutput()
283
    {
284
        return $this['output'];
285
    }
286
287
    /**
288
     * @param string $name
289
     * @return Console\Helper\HelperInterface
290
     */
291
    public function getHelper($name)
292
    {
293
        return $this->getConsole()->getHelperSet()->get($name);
294
    }
295
296
    /**
297
     * Run Deployer
298
     *
299
     * @param string $version
300
     * @param string $deployFile
301
     */
302
    public static function run($version, $deployFile)
303
    {
304
        // Init Deployer
305
        $console = new Application('Deployer', $version);
306
        $input = new ArgvInput();
307
        $output = new ConsoleOutput();
308
        $deployer = new self($console);
309
310
        // Pretty-print uncaught exceptions in symfony-console
311
        set_exception_handler(function ($e) use ($input, $output, $deployer) {
312
            $io = new SymfonyStyle($input, $output);
313
            $io->block($e->getMessage(), get_class($e), 'fg=white;bg=red', ' ', true);
314
            $io->block($e->getTraceAsString());
315
316
            $deployer->logger->log('['. get_class($e) .'] '. $e->getMessage());
317
            $deployer->logger->log($e->getTraceAsString());
318
            exit(1);
319
        });
320
321
        // Require deploy.php file
322
        if (is_readable($deployFile)) {
323
            // Prevent variable leak into deploy.php file
324
            call_user_func(function () use ($deployFile) {
325
                require $deployFile;
326
            });
327
        }
328
329
        // Run Deployer
330
        $deployer->init();
331
        $console->run($input, $output);
332
    }
333
334
    /**
335
     * Collect anonymous stats about Deployer usage for improving developer experience.
336
     * If you are not comfortable with this, you will always be able to disable this
337
     * by setting `allow_anonymous_stats` to false in your deploy.php file.
338
     *
339
     * @param CommandEvent $commandEvent
340
     * @codeCoverageIgnore
341
     */
342
    public function collectAnonymousStats(CommandEvent $commandEvent)
343
    {
344
        if ($this->config->has('allow_anonymous_stats') && $this->config['allow_anonymous_stats'] === false) {
345
            return;
346
        }
347
348
        $stats = [
349
            'status' => 'success',
350
            'command_name' => $commandEvent->getCommand()->getName(),
351
            'project_hash' => empty($this->config['repository']) ? null : sha1($this->config['repository']),
352
            'hosts_count' => $this->hosts->count(),
353
            'deployer_version' => $this->getConsole()->getVersion(),
354
            'deployer_phar' => $this->getConsole()->isPharArchive(),
355
            'php_version' => phpversion(),
356
            'extension_pcntl' => extension_loaded('pcntl'),
357
            'extension_curl' => extension_loaded('curl'),
358
            'os' => defined('PHP_OS_FAMILY') ? PHP_OS_FAMILY : (stristr(PHP_OS, 'DAR') ? 'OSX' : (stristr(PHP_OS, 'WIN') ? 'WIN' : (stristr(PHP_OS, 'LINUX') ? 'LINUX' : PHP_OS))),
359
            'exception' => null,
360
        ];
361
362
        if ($commandEvent->getException() !== null) {
363
            $stats['status'] = 'error';
364
            $stats['exception'] = get_class($commandEvent->getException());
365
        }
366
367
        if ($stats['command_name'] === 'init') {
368
            $stats['allow_anonymous_stats'] = $GLOBALS['allow_anonymous_stats'] ?? false;
369
        }
370
371
        if (in_array($stats['command_name'], ['worker', 'list', 'help'], true)) {
372
            return;
373
        }
374
375
        Reporter::report($stats);
376
    }
377
}
378