Completed
Push — master ( bd5921...d9293e )
by Christian
17:54 queued 08:59
created

ScriptCommand::isEnabled()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
3
namespace N98\Magento\Command;
4
5
use InvalidArgumentException;
6
use N98\Magento\Command\AbstractMagentoCommand;
7
use N98\Util\BinaryString;
8
use N98\Util\Exec;
9
use RuntimeException;
10
use Symfony\Component\Console\Helper\DialogHelper;
11
use Symfony\Component\Console\Input\InputArgument;
12
use Symfony\Component\Console\Input\InputOption;
13
use Symfony\Component\Console\Input\StringInput;
14
use Symfony\Component\Console\Input\InputInterface;
15
use Symfony\Component\Console\Output\OutputInterface;
16
17
class ScriptCommand extends AbstractMagentoCommand
18
{
19
    /**
20
     * @var array
21
     */
22
    protected $scriptVars = array();
23
24
    /**
25
     * @var string
26
     */
27
    protected $_scriptFilename = '';
28
29
    /**
30
     * @var bool
31
     */
32
    protected $_stopOnError = false;
33
34 View Code Duplication
    protected function configure()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
35
    {
36
        $this
37
            ->setName('script')
38
            ->addArgument('filename', InputArgument::OPTIONAL, 'Script file')
39
            ->addOption('define', 'd', InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Defines a variable')
40
            ->addOption('stop-on-error', null, InputOption::VALUE_NONE, 'Stops execution of script on error')
41
            ->setDescription('Runs multiple n98-magerun commands')
42
        ;
43
44
        $help = <<<HELP
45
Example:
46
47
   # Set multiple config
48
   config:set "web/cookie/cookie_domain" example.com
49
50
   # Set with multiline values with "\n"
51
   config:set "general/store_information/address" "First line\nSecond line\nThird line"
52
53
   # This is a comment
54
   cache:flush
55
56
57
Optionally you can work with unix pipes.
58
59
   \$ echo "cache:flush" | n98-magerun-dev script
60
61
   \$ n98-magerun.phar script < filename
62
63
It is even possible to create executable scripts:
64
65
Create file `test.magerun` and make it executable (`chmod +x test.magerun`):
66
67
   #!/usr/bin/env n98-magerun.phar script
68
69
   config:set "web/cookie/cookie_domain" example.com
70
   cache:flush
71
72
   # Run a shell script with "!" as first char
73
   ! ls -l
74
75
   # Register your own variable (only key = value currently supported)
76
   \${my.var}=bar
77
78
   # Let magerun ask for variable value - add a question mark
79
   \${my.var}=?
80
81
   ! echo \${my.var}
82
83
   # Use resolved variables from n98-magerun in shell commands
84
   ! ls -l \${magento.root}/code/local
85
86
Pre-defined variables:
87
88
* \${magento.root}    -> Magento Root-Folder
89
* \${magento.version} -> Magento Version i.e. 1.7.0.2
90
* \${magento.edition} -> Magento Edition -> Community or Enterprise
91
* \${magerun.version} -> Magerun version i.e. 1.66.0
92
* \${php.version}     -> PHP Version
93
* \${script.file}     -> Current script file path
94
* \${script.dir}      -> Current script file dir
95
96
Variables can be passed to a script with "--define (-d)" option.
97
98
Example:
99
100
   $ n98-magerun.phar script -d foo=bar filename
101
102
   # This will register the variable \${foo} with value bar.
103
104
It's possible to define multiple values by passing more than one option.
105
HELP;
106
        $this->setHelp($help);
107
    }
108
109
    /**
110
     * @return bool
111
     */
112
    public function isEnabled()
113
    {
114
        return Exec::allowed();
115
    }
116
117
    protected function execute(InputInterface $input, OutputInterface $output)
118
    {
119
        $this->_scriptFilename = $input->getArgument('filename');
120
        $this->_stopOnError = $input->getOption('stop-on-error');
121
        $this->_initDefines($input);
122
        $script = $this->_getContent($this->_scriptFilename);
123
        $commands = explode("\n", $script);
124
        $this->initScriptVars();
125
126
        foreach ($commands as $commandString) {
127
            $commandString = trim($commandString);
128
            if (empty($commandString)) {
129
                continue;
130
            }
131
            $firstChar = substr($commandString, 0, 1);
132
133
            switch ($firstChar) {
134
135
                // comment
136
                case '#':
137
                    continue;
138
                    break;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
139
140
                // set var
141
                case '$':
142
                    $this->registerVariable($output, $commandString);
143
                    break;
144
145
                // run shell script
146
                case '!':
147
                    $this->runShellCommand($output, $commandString);
148
                    break;
149
150
                default:
151
                    $this->runMagerunCommand($input, $output, $commandString);
152
            }
153
        }
154
    }
155
156
    /**
157
     * @param InputInterface $input
158
     * @throws InvalidArgumentException
159
     */
160
    protected function _initDefines(InputInterface $input)
161
    {
162
        $defines = $input->getOption('define');
163
        if (is_string($defines)) {
164
            $defines = array($defines);
165
        }
166
        if (count($defines) > 0) {
167
            foreach ($defines as $define) {
0 ignored issues
show
Bug introduced by
The expression $defines of type object|integer|double|null|array|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
168
                if (!strstr($define, '=')) {
169
                    throw new InvalidArgumentException('Invalid define');
170
                }
171
                $parts = BinaryString::trimExplodeEmpty('=', $define);
172
                $variable = $parts[0];
173
                $value = null;
174
                if (isset($parts[1])) {
175
                    $value = $parts[1];
176
                }
177
                $this->scriptVars['${' . $variable . '}'] = $value;
178
            }
179
        }
180
    }
181
182
    /**
183
     * @param string $filename
184
     * @throws RuntimeException
185
     * @internal param string $input
186
     * @return string
187
     */
188
    protected function _getContent($filename)
189
    {
190
        if ($filename == '-' || empty($filename)) {
191
            $script = @\file_get_contents('php://stdin', 'r');
192
        } else {
193
            $script = @\file_get_contents($filename);
194
        }
195
196
        if (!$script) {
197
            throw new RuntimeException('Script file was not found');
198
        }
199
200
        return $script;
201
    }
202
203
    /**
204
     * @param OutputInterface $output
205
     * @param string $commandString
206
     * @throws RuntimeException
207
     * @return void
208
     */
209
    protected function registerVariable(OutputInterface $output, $commandString)
210
    {
211
        if (preg_match('/^(\$\{[a-zA-Z0-9-_.]+\})=(.+)/', $commandString, $matches)) {
212
            if (isset($matches[2]) && $matches[2][0] == '?') {
213
214
                // Variable is already defined
215
                if (isset($this->scriptVars[$matches[1]])) {
216
                    return $this->scriptVars[$matches[1]];
217
                }
218
219
                /* @var $dialog DialogHelper */
220
                $dialog = $this->getHelperSet()->get('dialog');
221
222
                /**
223
                 * Check for select "?["
224
                 */
225
                if (isset($matches[2][1]) && $matches[2][1] == '[') {
226
                    if (preg_match('/\[(.+)\]/', $matches[2], $choiceMatches)) {
227
                        $choices = BinaryString::trimExplodeEmpty(',', $choiceMatches[1]);
228
                        $selectedIndex = $dialog->select(
229
                            $output,
230
                            '<info>Please enter a value for <comment>' . $matches[1] . '</comment>:</info> ',
231
                            $choices
232
                        );
233
                        $this->scriptVars[$matches[1]] = $choices[$selectedIndex];
234
235
                    } else {
236
                        throw new RuntimeException('Invalid choices');
237
                    }
238
                } else {
239
                    // normal input
240
                    $this->scriptVars[$matches[1]] = $dialog->askAndValidate(
241
                        $output,
242
                        '<info>Please enter a value for <comment>' . $matches[1] . '</comment>:</info> ',
243
                        function($value) {
244
                            if ($value == '') {
245
                                throw new RuntimeException('Please enter a value');
246
                            }
247
248
                            return $value;
249
                        }
250
                    );
251
                }
252
            } else {
253
                $this->scriptVars[$matches[1]] = $this->_replaceScriptVars($matches[2]);
254
            }
255
        }
256
    }
257
258
    /**
259
     * @param InputInterface $input
260
     * @param OutputInterface $output
261
     * @param string $commandString
262
     * @throws RuntimeException
263
     */
264
    protected function runMagerunCommand(InputInterface $input, OutputInterface $output, $commandString)
0 ignored issues
show
Unused Code introduced by
The parameter $input 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...
265
    {
266
        $this->getApplication()->setAutoExit(false);
267
        $commandString = $this->_replaceScriptVars($commandString);
268
        $input = new StringInput($commandString);
269
        $exitCode = $this->getApplication()->run($input, $output);
270
        if ($exitCode !== 0 && $this->_stopOnError) {
271
            throw new RuntimeException('Script stopped with errors');
272
        }
273
    }
274
275
    /**
276
     * @param string $commandString
277
     * @return string
278
     */
279
    protected function _prepareShellCommand($commandString)
280
    {
281
        $commandString = ltrim($commandString, '!');
282
283
        // @TODO find a better place
284
        if (strstr($commandString, '${magento.root}')
285
            || strstr($commandString, '${magento.version}')
286
            || strstr($commandString, '${magento.edition}')
287
        ) {
288
            $this->initMagento();
289
        }
290
        $this->initScriptVars();
291
        $commandString = $this->_replaceScriptVars($commandString);
292
293
        return $commandString;
294
    }
295
296
    protected function initScriptVars()
297
    {
298
        if (class_exists('\Mage')) {
299
            $this->scriptVars['${magento.root}'] = $this->getApplication()->getMagentoRootFolder();
300
            $this->scriptVars['${magento.version}'] = \Mage::getVersion();
301
            $this->scriptVars['${magento.edition}'] = is_callable(array('\Mage', 'getEdition')) ? \Mage::getEdition() : 'Community';
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 132 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
302
        }
303
304
        $this->scriptVars['${php.version}']     = substr(phpversion(), 0, strpos(phpversion(), '-'));
305
        $this->scriptVars['${magerun.version}'] = $this->getApplication()->getVersion();
306
        $this->scriptVars['${script.file}'] = $this->_scriptFilename;
307
        $this->scriptVars['${script.dir}'] = dirname($this->_scriptFilename);
308
    }
309
310
    /**
311
     * @param OutputInterface $output
312
     * @param string          $commandString
313
     * @internal param $returnValue
314
     */
315
    protected function runShellCommand(OutputInterface $output, $commandString)
316
    {
317
        $commandString = $this->_prepareShellCommand($commandString);
318
        $returnValue = shell_exec($commandString);
319
        if (!empty($returnValue)) {
320
            $output->writeln($returnValue);
321
        }
322
    }
323
324
    /**
325
     * @param string $commandString
326
     *
327
     * @return string
328
     */
329
    protected function _replaceScriptVars($commandString)
330
    {
331
        $commandString = str_replace(array_keys($this->scriptVars), $this->scriptVars, $commandString);
332
333
        return $commandString;
334
    }
335
}
336