BuildCommand   A
last analyzed

Complexity

Total Complexity 22

Size/Duplication

Total Lines 225
Duplicated Lines 0 %

Test Coverage

Coverage 95.18%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
wmc 22
eloc 75
c 5
b 0
f 0
dl 0
loc 225
ccs 79
cts 83
cp 0.9518
rs 10

11 Methods

Rating   Name   Duplication   Size   Complexity  
B compile() 0 41 6
A prepare() 0 28 2
A build() 0 16 1
A __destruct() 0 4 2
A getTimeout() 0 9 3
A getBinary() 0 3 1
A handle() 0 9 2
A supportsAsyncSignals() 0 3 1
A listenForSignals() 0 10 2
A run() 0 3 1
A clear() 0 11 1
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * This file is part of Laravel Zero.
7
 *
8
 * (c) Nuno Maduro <[email protected]>
9
 *
10
 *  For the full copyright and license information, please view the LICENSE
11
 *  file that was distributed with this source code.
12
 */
13
14
namespace LaravelZero\Framework\Commands;
15
16
use Illuminate\Console\Application as Artisan;
17
use Illuminate\Support\Facades\File;
18
use Symfony\Component\Console\Helper\ProgressBar;
19
use Symfony\Component\Console\Input\InputInterface;
20
use Symfony\Component\Console\Output\NullOutput;
21
use Symfony\Component\Console\Output\OutputInterface;
22
use Symfony\Component\Process\Process;
23
24
final class BuildCommand extends Command
25
{
26
    /**
27
     * {@inheritdoc}
28
     */
29
    protected $signature = 'app:build
30
                            {name? : The build name}
31
                            {--build-version= : The build version, if not provided it will be asked}
32
                            {--timeout=300 : The timeout in seconds or 0 to disable}';
33
34
    /**
35
     * {@inheritdoc}
36
     */
37
    protected $description = 'Build a single file executable';
38
39
    /**
40
     * Holds the configuration on is original state.
41
     *
42
     * @var string|null
43
     */
44
    private static $config;
45
46
    /**
47
     * Holds the box.json on is original state.
48
     *
49
     * @var string|null
50
     */
51
    private static $box;
52
53
    /**
54
     * Holds the command original output.
55
     *
56
     * @var \Symfony\Component\Console\Output\OutputInterface
57
     */
58
    private $originalOutput;
59
60
    /**
61
     * {@inheritdoc}
62
     */
63 2
    public function handle()
64
    {
65 2
        if ($this->supportsAsyncSignals()) {
66 2
            $this->listenForSignals();
67
        }
68
69 2
        $this->title('Building process');
70
71 2
        $this->build($this->input->getArgument('name') ?? $this->getBinary());
0 ignored issues
show
Bug introduced by
It seems like $this->input->getArgumen...) ?? $this->getBinary() can also be of type string[]; however, parameter $name of LaravelZero\Framework\Co...s\BuildCommand::build() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

71
        $this->build(/** @scrutinizer ignore-type */ $this->input->getArgument('name') ?? $this->getBinary());
Loading history...
72 1
    }
73
74
    /**
75
     * {@inheritdoc}
76
     */
77 2
    public function run(InputInterface $input, OutputInterface $output)
78
    {
79 2
        parent::run($input, $this->originalOutput = $output);
80 1
    }
81
82
    /**
83
     * Builds the application into a single file.
84
     */
85 2
    private function build(string $name): BuildCommand
86
    {
87
        /*
88
         * We prepare the application for a build, moving it to production. Then,
89
         * after compile all the code to a single file, we move the built file
90
         * to the builds folder with the correct permissions.
91
         */
92 2
        $this->prepare()
93 2
            ->compile($name)
94 1
            ->clear();
95
96 1
        $this->output->writeln(
97 1
            sprintf('    Compiled successfully: <fg=green>%s</>', $this->app->buildsPath($name))
98
        );
99
100 1
        return $this;
101
    }
102
103 2
    private function compile(string $name): BuildCommand
104
    {
105 2
        if (! File::exists($this->app->buildsPath())) {
106 2
            File::makeDirectory($this->app->buildsPath());
107
        }
108
109 2
        $process = new Process(
110 2
            './box compile --working-dir="'.base_path().'" --config="'.base_path('box.json').'"',
0 ignored issues
show
Bug introduced by
'./box compile --working..._path('box.json') . '"' of type string is incompatible with the type array expected by parameter $command of Symfony\Component\Process\Process::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

110
            /** @scrutinizer ignore-type */ './box compile --working-dir="'.base_path().'" --config="'.base_path('box.json').'"',
Loading history...
111 2
            dirname(dirname(__DIR__)).'/bin',
112 2
            null,
113 2
            null,
114 2
            $this->getTimeout()
115
        );
116
117 2
        $section = tap($this->originalOutput->section())->write('');
0 ignored issues
show
Bug introduced by
The method section() does not exist on Symfony\Component\Console\Output\OutputInterface. It seems like you code against a sub-type of Symfony\Component\Console\Output\OutputInterface such as Symfony\Component\Console\Style\OutputStyle or Symfony\Component\Console\Output\ConsoleOutput or anonymous//tests/BuildCommandTest.php$1 or anonymous//tests/BuildCommandTest.php$3 or Symfony\Component\Console\Output\ConsoleOutput. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

117
        $section = tap($this->originalOutput->/** @scrutinizer ignore-call */ section())->write('');
Loading history...
118
119 2
        $progressBar = tap(
120 2
            new ProgressBar(
121 2
                $this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL ? new NullOutput() : $section, 25
122
            )
123 2
        )->setProgressCharacter("\xF0\x9F\x8D\xBA");
124
125 2
        foreach (tap($process)->start() as $type => $data) {
126 2
            $progressBar->advance();
127
128 2
            if ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL) {
129 2
                $process::OUT === $type ? $this->info("$data") : $this->error("$data");
130
            }
131
        }
132
133 2
        $progressBar->finish();
134
135 2
        $section->clear();
136
137 1
        $this->task('   2. <fg=yellow>Compile</> into a single file');
138
139 1
        $this->output->newLine();
140
141 1
        File::move($this->app->basePath($this->getBinary()).'.phar', $this->app->buildsPath($name));
142
143 1
        return $this;
144
    }
145
146 2
    private function prepare(): BuildCommand
147
    {
148 2
        $configFile = $this->app->configPath('app.php');
149 2
        static::$config = File::get($configFile);
0 ignored issues
show
Bug introduced by
Since $config is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $config to at least protected.
Loading history...
150
151 2
        $config = include $configFile;
152
153 2
        $config['production'] = true;
154 2
        $version = $this->option('build-version') ?: $this->ask('Build version?', $config['version']);
155 2
        $config['version'] = $version;
156
157 2
        $boxFile = $this->app->basePath('box.json');
158 2
        static::$box = File::get($boxFile);
0 ignored issues
show
Bug introduced by
Since $box is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $box to at least protected.
Loading history...
159
160 2
        $this->task(
161 2
            '   1. Moving application to <fg=yellow>production mode</>',
162
            function () use ($configFile, $config) {
163 2
                File::put($configFile, '<?php return '.var_export($config, true).';'.PHP_EOL);
164 2
            }
165
        );
166
167 2
        $boxContents = json_decode(static::$box, true);
168 2
        $boxContents['main'] = $this->getBinary();
169 2
        File::put($boxFile, json_encode($boxContents));
170
171 2
        File::put($configFile, '<?php return '.var_export($config, true).';'.PHP_EOL);
172
173 2
        return $this;
174
    }
175
176 2
    private function clear(): BuildCommand
177
    {
178 2
        File::put($this->app->configPath('app.php'), static::$config);
0 ignored issues
show
Bug introduced by
Since $config is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $config to at least protected.
Loading history...
179
180 2
        File::put($this->app->basePath('box.json'), static::$box);
0 ignored issues
show
Bug introduced by
Since $box is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $box to at least protected.
Loading history...
181
182 2
        static::$config = null;
183
184 2
        static::$box = null;
185
186 2
        return $this;
187
    }
188
189
    /**
190
     * Returns the artisan binary.
191
     */
192 2
    private function getBinary(): string
193
    {
194 2
        return str_replace(["'", '"'], '', Artisan::artisanBinary());
195
    }
196
197
    /**
198
     * Returns a valid timeout value. Non positive values are converted to null,
199
     * meaning no timeout.
200
     *
201
     * @return float|null
202
     * @throws \InvalidArgumentException
203
     */
204 2
    private function getTimeout(): ?float
205
    {
206 2
        if (! is_numeric($this->option('timeout'))) {
0 ignored issues
show
introduced by
The condition is_numeric($this->option('timeout')) is always true.
Loading history...
207
            throw new \InvalidArgumentException('The timeout value must be a number.');
208
        }
209
210 2
        $timeout = (float) $this->option('timeout');
211
212 2
        return $timeout > 0 ? $timeout : null;
213
    }
214
215
    /**
216
     * Enable and listen to async signals for the process.
217
     */
218 2
    private function listenForSignals(): void
219
    {
220 2
        pcntl_async_signals(true);
221
222
        pcntl_signal(SIGINT, function () {
223
            if (static::$config !== null) {
0 ignored issues
show
Bug introduced by
Since $config is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $config to at least protected.
Loading history...
224
                $this->clear();
225
            }
226
227
            exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
228 2
        });
229 2
    }
230
231
    /**
232
     * Determine if "async" signals are supported.
233
     */
234 2
    private function supportsAsyncSignals(): bool
235
    {
236 2
        return extension_loaded('pcntl');
237
    }
238
239
    /**
240
     * Makes sure that the `clear` is performed even
241
     * if the command fails.
242
     *
243
     * @return void
244
     */
245 39
    public function __destruct()
246
    {
247 39
        if (static::$config !== null) {
0 ignored issues
show
Bug introduced by
Since $config is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $config to at least protected.
Loading history...
248 1
            $this->clear();
249
        }
250 39
    }
251
}
252