Completed
Branch master (c7fd68)
by Gaetano
06:59
created

SQLExecutingCommand::executeSqlAction()   F

Complexity

Conditions 18
Paths 1918

Size

Total Lines 124
Code Lines 78

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 78
dl 0
loc 124
rs 0.7
c 0
b 0
f 0
cc 18
nc 1918
nop 5

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
0 ignored issues
show
Coding Style introduced by
Missing file doc comment
Loading history...
3
namespace Db3v4l\Command;
4
5
use Db3v4l\API\Interfaces\SqlAction\CommandAction;
6
use Db3v4l\API\Interfaces\SqlAction\FileAction;
7
use Db3v4l\Core\DatabaseSchemaManager;
8
use Db3v4l\Service\DatabaseConfigurationManager;
9
use Db3v4l\Service\ProcessManager;
10
use Db3v4l\Service\SqlExecutorFactory;
11
use Db3v4l\Util\Process;
12
use Symfony\Component\Console\Input\InputInterface;
0 ignored issues
show
Bug introduced by
The type Symfony\Component\Console\Input\InputInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
13
use Symfony\Component\Console\Input\InputOption;
0 ignored issues
show
Bug introduced by
The type Symfony\Component\Console\Input\InputOption was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
14
use Symfony\Component\Console\Output\OutputInterface;
0 ignored issues
show
Bug introduced by
The type Symfony\Component\Console\Output\OutputInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
15
use Symfony\Component\Yaml\Yaml;
0 ignored issues
show
Bug introduced by
The type Symfony\Component\Yaml\Yaml was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
16
17
abstract class SQLExecutingCommand extends BaseCommand
0 ignored issues
show
Coding Style introduced by
Missing doc comment for class SQLExecutingCommand
Loading history...
18
{
19
    /** @var DatabaseConfigurationManager $dbConfigurationManager */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
Missing short description in doc comment
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
20
    protected $dbConfigurationManager;
21
    /** @var SqlExecutorFactory $executorFactory */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
Missing short description in doc comment
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
22
    protected $executorFactory;
23
    protected $processManager;
24
25
    const DEFAULT_OUTPUT_FORMAT = 'text';
26
    const DEFAULT_PARALLEL_PROCESSES = 16;
27
    const DEFAULT_PROCESS_TIMEOUT = 600;
28
29
    protected $outputFormat;
30
    protected $maxParallelProcesses;
31
    protected $processTimeout;
32
33
    public function __construct(
0 ignored issues
show
Coding Style introduced by
Missing doc comment for function __construct()
Loading history...
34
        DatabaseConfigurationManager $dbConfigurationManager,
35
        SqlExecutorFactory $executorFactory,
36
        ProcessManager $processManager)
0 ignored issues
show
Coding Style introduced by
The closing parenthesis of a multi-line function declaration must be on a new line
Loading history...
37
    {
0 ignored issues
show
Coding Style introduced by
The closing parenthesis and the opening brace of a multi-line function declaration must be on the same line
Loading history...
38
        $this->dbConfigurationManager = $dbConfigurationManager;
39
        $this->executorFactory = $executorFactory;
40
        $this->processManager = $processManager;
41
42
        parent::__construct();
43
    }
44
45
    protected function addCommonOptions()
0 ignored issues
show
Coding Style introduced by
Missing doc comment for function addCommonOptions()
Loading history...
46
    {
47
        $this
48
            ->addOption('only-instances', null, InputOption::VALUE_REQUIRED, 'Filter the database servers to run this command against. Usage of * and ? wildcards is allowed. To see all instances available, use `instance:list`', null)
49
            ->addOption('except-instances', null, InputOption::VALUE_REQUIRED, 'Filter the database servers to run this command against', null)
50
            ->addOption('output-type', null, InputOption::VALUE_REQUIRED, 'The format for the output: json, php, text or yml', self::DEFAULT_OUTPUT_FORMAT)
51
            ->addOption('timeout', null, InputOption::VALUE_REQUIRED, 'The maximum time to wait for subprocess execution (secs)', self::DEFAULT_PROCESS_TIMEOUT)
52
            ->addOption('max-parallel', null, InputOption::VALUE_REQUIRED, 'The maximum number of subprocesses to run in parallel', self::DEFAULT_PARALLEL_PROCESSES)
53
            ->addOption('dont-force-enabled-sigchild', null, InputOption::VALUE_NONE, "When using a separate process to run each sql command, do not force Symfony to believe that php was compiled with --enable-sigchild option")
0 ignored issues
show
Coding Style introduced by
Space after closing parenthesis of function call prohibited
Loading history...
54
        ;
55
    }
56
57
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
58
     * @param InputInterface $input
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
59
     * @return string[][] the list of instances to use. key: name, value: connection spec
0 ignored issues
show
Coding Style introduced by
Tag @return cannot be grouped with parameter tags in a doc comment
Loading history...
60
     */
61
    protected function parseCommonOptions(InputInterface $input)
62
    {
63
        $this->outputFormat = $input->getOption('output-type');
64
        $this->processTimeout = $input->getOption('timeout');
65
        $this->maxParallelProcesses = $input->getOption('max-parallel');
66
67
        // On Debian, which we use by default, SF has troubles understanding that php was compiled with --enable-sigchild
68
        // We thus force it, but give end users an option to disable this
69
        // For more details, see comment 12 at https://bugs.launchpad.net/ubuntu/+source/php5/+bug/516061
70
        if (! $input->getOption('dont-force-enabled-sigchild')) {
71
            Process::forceSigchildEnabled(true);
72
        }
73
74
        return $this->dbConfigurationManager->getInstancesConfiguration(
75
            $this->dbConfigurationManager->listInstances($input->getOption('only-instances'), $input->getOption('except-instances'))
76
        );
77
    }
78
79
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
80
     * @param string[][] $instanceList list of instances to execute the action on. Key: instance name, value: connection spec
0 ignored issues
show
Coding Style introduced by
Expected 10 spaces after parameter name; 1 found
Loading history...
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
81
     * @param string $actionName used to build error messages
0 ignored issues
show
Coding Style introduced by
Expected 5 spaces after parameter type; 1 found
Loading history...
Coding Style introduced by
Expected 12 spaces after parameter name; 1 found
Loading history...
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
82
     * @param callable $getSqlActionCallable the method used to retrieve the desired SqlAction.
0 ignored issues
show
Coding Style introduced by
Expected 3 spaces after parameter type; 1 found
Loading history...
Coding Style introduced by
Expected 2 spaces after parameter name; 1 found
Loading history...
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
83
     *                                       It will be passed as arguments the SchemaManager and instance name, and should return a CommandAction or FileAction
84
     * @param bool $timed whether to use a timed executor
0 ignored issues
show
Coding Style introduced by
Expected 7 spaces after parameter type; 1 found
Loading history...
Coding Style introduced by
Expected 17 spaces after parameter name; 1 found
Loading history...
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
85
     * @param callable $onForkedProcessOutput a callback invoked when forked processes produce output
0 ignored issues
show
Coding Style introduced by
Expected 3 spaces after parameter type; 1 found
Loading history...
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
86
     * @return array 'succeeded': int, 'failed': int, 'data': mixed[]
0 ignored issues
show
Coding Style introduced by
Tag @return cannot be grouped with parameter tags in a doc comment
Loading history...
87
     * @throws \Exception
0 ignored issues
show
Coding Style introduced by
Tag @throws cannot be grouped with parameter tags in a doc comment
Loading history...
88
     */
89
    protected function executeSqlAction($instanceList, $actionName, $getSqlActionCallable, $timed = false, $onForkedProcessOutput = null)
90
    {
91
        $processes = [];
92
        $callables = [];
93
        $outputFilters = [];
94
        $tempSQLFileNames = [];
95
        $executors = [];
96
97
        try {
98
99
            foreach ($instanceList as $instanceName => $dbConnectionSpec) {
100
101
                $schemaManager = new DatabaseSchemaManager($dbConnectionSpec);
102
103
                /** @var CommandAction|FileAction $sqlAction */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
Missing short description in doc comment
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
104
                $sqlAction = call_user_func_array($getSqlActionCallable, [$schemaManager, $instanceName]);
105
106
                if ($sqlAction instanceof CommandAction) {
107
                    $filename = null;
108
                    $sql = $sqlAction->getCommand();
109
                } else if ($sqlAction instanceof FileAction) {
110
                    $filename = $sqlAction->getFilename();
111
                    $sql = null;
112
                } else {
113
                    // this is a coding error, not a sql execution error
114
                    throw new \Exception("Unsupported action type: " . get_class($sqlAction));
115
                }
116
                $filterCallable = $sqlAction->getResultsFilterCallable();
117
118
                if ($filename === null && $sql === null) {
119
                    // no sql to execute as forked process - we run the 'filter' functions in a separate loop
120
                    $callables[$instanceName] = $filterCallable;
121
                } else {
122
                    $outputFilters[$instanceName] = $filterCallable;
123
124
                    $executor = $this->executorFactory->createForkedExecutor($dbConnectionSpec, 'NativeClient', $timed);
125
                    $executors[$instanceName] = $executor;
126
127
                    if ($filename === null) {
128
                        if (!$sqlAction->isSingleStatement()) {
0 ignored issues
show
Bug introduced by
The method isSingleStatement() does not exist on Db3v4l\API\Interfaces\SqlAction\FileAction. ( Ignorable by Annotation )

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

128
                        if (!$sqlAction->/** @scrutinizer ignore-call */ isSingleStatement()) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
129
                            $tempSQLFileName = tempnam(sys_get_temp_dir(), 'db3v4l_') . '.sql';
130
                            file_put_contents($tempSQLFileName, $sql);
131
                            $tempSQLFileNames[] = $tempSQLFileName;
132
133
                            $process = $executor->getExecuteFileProcess($tempSQLFileName);
134
                        } else {
135
                            $process = $executor->getExecuteStatementProcess($sql);
136
                        }
137
                    } else {
138
                        $process = $executor->getExecuteFileProcess($filename);
139
                    }
140
141
                    if ($this->outputFormat === 'text') {
142
                        $this->writeln('Command line: ' . $process->getCommandLine(), OutputInterface::VERBOSITY_VERY_VERBOSE);
143
                    }
144
145
                    $process->setTimeout($this->processTimeout);
146
147
                    $processes[$instanceName] = $process;
148
                }
149
            }
150
151
            $succeeded = 0;
152
            $failed = 0;
153
            $results = [];
154
155
            foreach ($callables as $instanceName => $callable) {
156
                try {
157
                    $results[$instanceName] = call_user_func($callable);
158
                    $succeeded++;
159
                } catch (\Throwable $t) {
160
                    $failed++;
161
                    $this->writeErrorln("\n<error>$actionName in instance '$instanceName' failed! Reason: " . $t->getMessage() . "</error>\n", OutputInterface::VERBOSITY_NORMAL);
162
                }
163
            }
164
165
            if (count($processes)) {
166
                if ($this->outputFormat === 'text') {
167
                    $this->writeln('<info>Starting parallel execution...</info>', OutputInterface::VERBOSITY_VERY_VERBOSE);
168
                }
169
                $this->processManager->runParallel($processes, $this->maxParallelProcesses, 100, $onForkedProcessOutput);
170
171
                foreach ($processes as $instanceName => $process) {
172
                    if ($process->isSuccessful()) {
173
                        /// @todo is it necessary to have rtrim here ? shall we maybe move it to the executor ?
174
                        $output = rtrim($process->getOutput());
175
                        if (isset($outputFilters[$instanceName])) {
176
                            try {
177
                                $output = call_user_func_array($outputFilters[$instanceName], [$output, $executors[$instanceName]]);
178
                            } catch (\Throwable $t) {
179
                                /// @todo shall we reset $result to null or not?
180
                                //$result = null;
181
                                $failed++;
182
                                $succeeded--;
183
                                $this->writeErrorln("\n<error>$actionName in instance '$instanceName' failed! Reason: " . $t->getMessage() . "</error>\n", OutputInterface::VERBOSITY_NORMAL);
184
                            }
185
                        }
186
                        $results[$instanceName] = $output;
187
                        $succeeded++;
188
                    } else {
189
                        $results[$instanceName] = [
190
                            'stderr' => trim($process->getErrorOutput()),
191
                            'exitcode' => $process->getExitCode()
192
                        ];
193
                        $failed++;
194
                        $this->writeErrorln("\n<error>$actionName in instance '$instanceName' failed! Reason: " . $process->getErrorOutput() . "</error>\n", OutputInterface::VERBOSITY_NORMAL);
195
                    }
196
                }
197
            }
198
199
        } finally {
200
            // make sure that we clean up temp files, as they might contain sensitive data
201
            foreach($tempSQLFileNames as $tempSQLFileName) {
0 ignored issues
show
Coding Style introduced by
Expected "foreach (...) {\n"; found "foreach(...) {\n"
Loading history...
202
                unlink($tempSQLFileName);
203
            }
204
        }
205
206
        /// @todo implement proper sorting based on vendor name + version
207
        ksort($results);
208
209
        return [
210
            'succeeded' => $succeeded,
211
            'failed' => $failed,
212
            'data' => $results
213
        ];
214
    }
215
216
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
217
     * @param array $results
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
218
     * @return string
0 ignored issues
show
Coding Style introduced by
Tag @return cannot be grouped with parameter tags in a doc comment
Loading history...
219
     * @throws \OutOfBoundsException for unsupported output formats
0 ignored issues
show
Coding Style introduced by
Tag @throws cannot be grouped with parameter tags in a doc comment
Loading history...
220
     */
221
    protected function formatResults(array $results)
222
    {
223
        switch ($this->outputFormat) {
224
            case 'json':
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 8 spaces, found 12
Loading history...
225
                return json_encode($results['data'], JSON_PRETTY_PRINT);
226
            case 'php':
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 8 spaces, found 12
Loading history...
227
                return var_export($results['data'], true);
228
            case 'text':
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 8 spaces, found 12
Loading history...
229
            case 'yml':
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 8 spaces, found 12
Loading history...
230
            case 'yaml':
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 8 spaces, found 12
Loading history...
231
                return Yaml::dump($results['data'], 2, 4, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK);
232
            default:
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 8 spaces, found 12
Loading history...
233
                throw new \OutOfBoundsException("Unsupported output format: '{$this->outputFormat}'");
234
                break;
235
        }
236
    }
237
238
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
239
     * @param array $results should contain elements: succeeded(int) failed(int), data(mixed)
0 ignored issues
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
240
     * @param float $time execution time in seconds
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces after parameter name; 1 found
Loading history...
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
241
     * @throws \Exception for unsupported formats
0 ignored issues
show
Coding Style introduced by
Tag @throws cannot be grouped with parameter tags in a doc comment
Loading history...
242
     * @todo since we use could be using forked processes, we can not measure total memory used... is it worth measuring just ours?
0 ignored issues
show
Coding Style introduced by
Tag @todo cannot be grouped with parameter tags in a doc comment
Loading history...
Coding Style introduced by
Tag value for @todo tag indented incorrectly; expected 3 spaces but found 1
Loading history...
243
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
244
    protected function writeResults(array $results, $time = null)
245
    {
246
        $this->writeln($this->formatResults($results), OutputInterface::VERBOSITY_QUIET,  OutputInterface::OUTPUT_RAW);
247
248
        if ($this->outputFormat === 'text') {
249
            $this->writeln($results['succeeded'] . ' succeeded, ' . $results['failed'] . ' failed');
250
251
            if ($time !== null) {
252
                $this->writeln("<info>Time taken: ".sprintf('%.2f', $time)." secs</info>");
253
            }
254
        }
255
    }
256
}
257