Deployer::hasDefault()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 4
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
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 function Deployer\Support\array_merge_alternate;
28
use Deployer\Task;
29
use Deployer\Utility\ProcessOutputPrinter;
30
use Deployer\Utility\ProcessRunner;
31
use Deployer\Utility\Reporter;
32
use Deployer\Utility\Rsync;
33
use Pimple\Container;
34
use Symfony\Component\Console;
35
use Symfony\Component\Console\Input\ArgvInput;
36
use Symfony\Component\Console\Output\ConsoleOutput;
37
use Symfony\Component\Console\Style\SymfonyStyle;
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 32
    public function __construct(Application $console)
70
    {
71 32
        parent::__construct();
72
73
        /******************************
74
         *           Console          *
75
         ******************************/
76
77
        $this['console'] = function () use ($console) {
78 17
            $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 17
            });
83 17
            return $console;
84
        };
85
86
        /******************************
87
         *           Config           *
88
         ******************************/
89
90 32
        $this['config'] = function () {
91 32
            return new Collection();
92
        };
93 32
        $this->config['ssh_multiplexing'] = true;
94 32
        $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 21
        $this['tasks'] = function () {
113 21
            return new Task\TaskCollection();
114
        };
115 21
        $this['hosts'] = function () {
116 21
            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 12
        $this['fail'] = function () {
129 12
            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 17
        $this['init_command'] = function () {
159 17
            return new InitCommand();
160
        };
161
162 32
        self::$instance = $this;
163 32
    }
164
165
    /**
166
     * @return Deployer
167
     */
168 34
    public static function get()
169
    {
170 34
        return self::$instance;
171
    }
172
173
    /**
174
     * @param string $name
175
     * @param mixed $value
176
     */
177 16
    public static function setDefault($name, $value)
178
    {
179 16
        Deployer::get()->config[$name] = $value;
180 16
    }
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 17
    public function init()
222
    {
223 17
        $this->addConsoleCommands();
224 17
        $this->getConsole()->add(new WorkerCommand($this));
225 17
        $this->getConsole()->add($this['init_command']);
226 17
        $this->getConsole()->add(new SshCommand($this));
227 17
        $this->getConsole()->add(new RunCommand($this));
228 17
        $this->getConsole()->add(new DebugCommand($this));
229 17
        $this->getConsole()->add(new AutocompleteCommand());
230 17
        $this->getConsole()->afterRun([$this, 'collectAnonymousStats']);
231 17
    }
232
233
    /**
234
     * Transform tasks to console commands.
235
     */
236 17
    public function addConsoleCommands()
237
    {
238 17
        $this->getConsole()->addUserArgumentsAndOptions();
239
240 17
        foreach ($this->tasks as $name => $task) {
241 17
            if ($task->isPrivate()) {
242 12
                continue;
243
            }
244
245 17
            $this->getConsole()->add(new TaskCommand($name, $task->getDescription(), $this));
246
        }
247 17
    }
248
249
    /**
250
     * @param string $name
251
     * @return mixed
252
     * @throws \InvalidArgumentException
253
     */
254 37
    public function __get($name)
255
    {
256 37
        if (isset($this[$name])) {
257 37
            return $this[$name];
258
        } else {
259 1
            throw new \InvalidArgumentException("Property \"$name\" does not exist.");
260
        }
261
    }
262
263
    /**
264
     * @return Application
265
     */
266 17
    public function getConsole()
267
    {
268 17
        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
        self::loadRecipe($deployFile);
323
324
        // Run Deployer
325
        $deployer->init();
326
        $console->run($input, $output);
327
    }
328
329
    /**
330
     * Collect anonymous stats about Deployer usage for improving developer experience.
331
     * If you are not comfortable with this, you will always be able to disable this
332
     * by setting `allow_anonymous_stats` to false in your deploy.php file.
333
     *
334
     * @param CommandEvent $commandEvent
335
     * @codeCoverageIgnore
336
     */
337
    public function collectAnonymousStats(CommandEvent $commandEvent)
338
    {
339
        if ($this->config->has('allow_anonymous_stats') && $this->config['allow_anonymous_stats'] === false) {
340
            return;
341
        }
342
343
        $stats = [
344
            'status' => 'success',
345
            'command_name' => $commandEvent->getCommand()->getName(),
346
            'project_hash' => empty($this->config['repository']) ? null : sha1($this->config['repository']),
347
            'hosts_count' => $this->hosts->count(),
348
            'deployer_version' => $this->getConsole()->getVersion(),
349
            'deployer_phar' => $this->getConsole()->isPharArchive(),
350
            'php_version' => phpversion(),
351
            'extension_pcntl' => extension_loaded('pcntl'),
352
            'extension_curl' => extension_loaded('curl'),
353
            '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))),
354
            'exception' => null,
355
        ];
356
357
        if ($commandEvent->getException() !== null) {
358
            $stats['status'] = 'error';
359
            $stats['exception'] = get_class($commandEvent->getException());
360
        }
361
362
        if ($stats['command_name'] === 'init') {
363
            $stats['allow_anonymous_stats'] = $GLOBALS['allow_anonymous_stats'] ?? false;
364
        }
365
366
        if (in_array($stats['command_name'], ['worker', 'list', 'help'], true)) {
367
            return;
368
        }
369
370
        Reporter::report($stats);
371
    }
372
373
    /**
374
     * Load recipe file
375
     *
376
     * @param string $deployFile
377
     *
378
     * @return void
379
     * @codeCoverageIgnore
380
     */
381
    public static function loadRecipe($deployFile)
382
    {
383
        if (is_readable($deployFile)) {
384
            // Prevent variable leak into deploy.php file
385
            call_user_func(function () use ($deployFile) {
386
                // reorder autoload stack.
387
                $originStack = spl_autoload_functions();
388
                require $deployFile;
389
                $newStack = spl_autoload_functions();
390
                if ($originStack[0] !== $newStack[0]) {
391
                    foreach (array_reverse($originStack) as $loader) {
392
                        spl_autoload_unregister($loader);
393
                        spl_autoload_register($loader, true, true);
394
                    }
395
                }
396
            });
397
        }
398
    }
399
}
400