Completed
Push — develop ( 43816c...8775b9 )
by Narcotic
10s
created

ImportCommand::importResource()   D

Complexity

Conditions 10
Paths 64

Size

Total Lines 103
Code Lines 67

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 53
CRAP Score 11.4331

Importance

Changes 0
Metric Value
dl 0
loc 103
ccs 53
cts 70
cp 0.7571
rs 4.8196
c 0
b 0
f 0
cc 10
eloc 67
nc 64
nop 7
crap 11.4331

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
/**
3
 * import json data into graviton
4
 *
5
 * Supports importing json data from either a single file or a complete folder of files.
6
 *
7
 * The data needs to contain frontmatter to hint where the bits and pieces should go.
8
 */
9
10
namespace Graviton\ImportExport\Command;
11
12
use Graviton\ImportExport\Exception\MissingTargetException;
13
use Graviton\ImportExport\Exception\JsonParseException;
14
use Graviton\ImportExport\Exception\UnknownFileTypeException;
15
use Graviton\ImportExport\Service\HttpClient;
16
use GuzzleHttp\Exception\ClientException;
17
use Symfony\Component\Console\Input\InputArgument;
18
use Symfony\Component\Console\Input\InputOption;
19
use Symfony\Component\Console\Input\InputInterface;
20
use Symfony\Component\Console\Output\OutputInterface;
21
use Symfony\Component\Finder\Finder;
22
use Symfony\Component\Yaml\Parser;
23
use Symfony\Component\VarDumper\Cloner\VarCloner;
24
use Symfony\Component\VarDumper\Dumper\CliDumper as Dumper;
25
use GuzzleHttp\Promise;
26
use GuzzleHttp\Exception\RequestException;
27
use Symfony\Component\Finder\SplFileInfo;
28
use Webuni\FrontMatter\FrontMatter;
29
use Webuni\FrontMatter\Document;
30
use Psr\Http\Message\ResponseInterface;
31
32
/**
33
 * @author   List of contributors <https://github.com/libgraviton/import-export/graphs/contributors>
34
 * @license  http://opensource.org/licenses/gpl-license.php GNU Public License
35
 * @link     http://swisscom.ch
36
 */
37
class ImportCommand extends ImportCommandAbstract
38
{
39
    /**
40
     * @var HttpClient
41
     */
42
    private $client;
43
44
    /**
45
     * @var FrontMatter
46
     */
47
    private $frontMatter;
48
49
    /**
50
     * @var Parser
51
     */
52
    private $parser;
53
54
    /**
55
     * @var VarCloner
56
     */
57
    private $cloner;
58
59
    /**
60
     * @var Dumper
61
     */
62
    private $dumper;
63
64
    /**
65
     * Count of errors
66
     * @var array
67
     */
68
    private $errors = [];
69
70
    /**
71
     * Header basic auth
72
     * @var string
73
     */
74
    private $headerBasicAuth;
75
76
    /**
77
     * Header for custom variables
78
     * @var array
79
     */
80
    private $customHeaders;
81
82
    /**
83
     * @param HttpClient  $client      Grv HttpClient guzzle http client
84
     * @param Finder      $finder      symfony/finder instance
85
     * @param FrontMatter $frontMatter frontmatter parser
86
     * @param Parser      $parser      yaml/json parser
87
     * @param VarCloner   $cloner      var cloner for dumping reponses
88
     * @param Dumper      $dumper      dumper for outputing responses
89
     */
90 5
    public function __construct(
91
        HttpClient $client,
92
        Finder $finder,
93
        FrontMatter $frontMatter,
94
        Parser $parser,
95
        VarCloner $cloner,
96
        Dumper $dumper
97
    ) {
98 5
        parent::__construct(
99
            $finder
100 5
        );
101 5
        $this->client = $client;
102 5
        $this->frontMatter = $frontMatter;
103 5
        $this->parser = $parser;
104 5
        $this->cloner = $cloner;
105 5
        $this->dumper = $dumper;
106 5
    }
107
108
    /**
109
     * Configures the current command.
110
     *
111
     * @return void
112
     */
113 5
    protected function configure()
114
    {
115 5
        $this
116 5
            ->setName('graviton:import')
117 5
            ->setDescription('Import files from a folder or file.')
118 5
            ->addOption(
119 5
                'rewrite-host',
120 5
                'r',
121 5
                InputOption::VALUE_OPTIONAL,
122 5
                'Replace the value of this option with the <host> value before importing.',
123
                'http://localhost'
124 5
            )
125 5
            ->addOption(
126 5
                'rewrite-to',
127 5
                't',
128 5
                InputOption::VALUE_OPTIONAL,
129 5
                'String to use as the replacement value for the [REWRITE-HOST] string.',
130
                '<host>'
131 5
            )
132 5
            ->addOption(
133 5
                'sync-requests',
134 5
                's',
135 5
                InputOption::VALUE_NONE,
136
                'Send requests synchronously'
137 5
            )
138 5
            ->addOption(
139 5
                'headers-basic-auth',
140 5
                'a',
141 5
                InputOption::VALUE_OPTIONAL,
142
                'Header user:password for Basic auth'
143 5
            )
144 5
            ->addOption(
145 5
                'custom-headers',
146 5
                'c',
147 5
                InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
148
                'Custom Header variable(s), -c{key:value} and multiple is optional.'
149 5
            )
150 5
            ->addOption(
151 5
                'input-file',
152 5
                'i',
153 5
                InputOption::VALUE_REQUIRED,
154
                'If provided, the list of files to load will be loaded from this file, one file per line.'
155 5
            )
156 5
            ->addArgument(
157 5
                'host',
158 5
                InputArgument::REQUIRED,
159
                'Protocol and host to load data into (ie. https://graviton.nova.scapp.io)'
160 5
            )
161 5
            ->addArgument(
162 5
                'file',
163 5
                InputArgument::IS_ARRAY,
164
                'Directories or files to load'
165 5
            );
166 5
    }
167
168
    /**
169
     * Executes the current command.
170
     *
171
     * @param Finder          $finder Finder
172
     * @param InputInterface  $input  User input on console
173
     * @param OutputInterface $output Output of the command
174
     *
175
     * @return integer
176
     */
177 5
    protected function doImport(Finder $finder, InputInterface $input, OutputInterface $output)
178
    {
179 5
        $exitCode = 0;
180 5
        $host = $input->getArgument('host');
181 5
        $rewriteHost = $input->getOption('rewrite-host');
182 5
        $rewriteTo = $input->getOption('rewrite-to');
183 5
        $this->headerBasicAuth = $input->getOption('headers-basic-auth');
184 5
        $this->customHeaders = $input->getOption('custom-headers');
0 ignored issues
show
Documentation Bug introduced by
It seems like $input->getOption('custom-headers') of type * is incompatible with the declared type array of property $customHeaders.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
185 5
        if ($rewriteTo === $this->getDefinition()->getOption('rewrite-to')->getDefault()) {
186 5
            $rewriteTo = $host;
187 5
        }
188 5
        $sync = $input->getOption('sync-requests');
189
190 5
        $this->importPaths($finder, $output, $host, $rewriteHost, $rewriteTo, $sync);
191
192
        // Error exit
193 4
        if (empty($this->errors)) {
194
            // No errors
195 3
            $output->writeln("\n".'<info>No errors</info>');
196 3
        } else {
197
            // Yes, there was errors
198 1
            $output->writeln("\n".'<info>There was errors: '.count($this->errors).'</info>');
199 1
            foreach ($this->errors as $file => $error) {
200 1
                $output->writeln("<error>{$file}: {$error}</error>");
201 1
            }
202 1
            $exitCode = 1;
203
        }
204 4
        return $exitCode;
205
    }
206
207
    /**
208
     * @param Finder          $finder      finder primmed with files to import
209
     * @param OutputInterface $output      output interfac
210
     * @param string          $host        host to import into
211
     * @param string          $rewriteHost string to replace with value from $rewriteTo during loading
212
     * @param string          $rewriteTo   string to replace value from $rewriteHost with during loading
213
     * @param boolean         $sync        send requests syncronously
214
     *
215
     * @return void
216
     *
217
     * @throws MissingTargetException
218
     */
219 5
    protected function importPaths(
220
        Finder $finder,
221
        OutputInterface $output,
222
        $host,
223
        $rewriteHost,
224
        $rewriteTo,
225
        $sync = false
226
    ) {
227 5
        $promises = [];
228
        /** @var SplFileInfo $file */
229 5
        foreach ($finder as $file) {
230 5
            $doc = $this->frontMatter->parse($file->getContents());
231
232 5
            $output->writeln("<info>Loading data from ${file}</info>");
233
234 5
            if (!array_key_exists('target', $doc->getData())) {
235 1
                throw new MissingTargetException('Missing target in \'' . $file . '\'');
236
            }
237
238 4
            $targetUrl = sprintf('%s%s', $host, $doc->getData()['target']);
239
240 4
            $promises[] = $this->importResource(
241 4
                $targetUrl,
242 4
                (string) $file,
243 4
                $output,
244 4
                $doc,
245 4
                $rewriteHost,
246 4
                $rewriteTo,
247
                $sync
248 4
            );
249 4
        }
250
251
        try {
252
            Promise\unwrap($promises);
253
        } catch (ClientException $e) {
254
            // silently ignored since we already output an error when the promise fails
255
        }
256
    }
257
258
    /**
259
     * @param string          $targetUrl   target url to import resource into
260
     * @param string          $file        path to file being loaded
261
     * @param OutputInterface $output      output of the command
262
     * @param Document        $doc         document to load
263
     * @param string          $rewriteHost string to replace with value from $host during loading
264
     * @param string          $rewriteTo   string to replace value from $rewriteHost with during loading
265
     * @param boolean         $sync        send requests syncronously
266
     *
267
     * @return Promise\PromiseInterface|null
268
     */
269
    protected function importResource(
270
        $targetUrl,
271
        $file,
272
        OutputInterface $output,
273
        Document $doc,
274
        $rewriteHost,
275
        $rewriteTo,
276
        $sync = false
277
    ) {
278 4
        $content = str_replace($rewriteHost, $rewriteTo, $doc->getContent());
279 4
        $uploadFile = $this->validateUploadFile($doc, $file);
280
281
        $successFunc = function (ResponseInterface $response) use ($output) {
282 3
            $output->writeln(
283 3
                '<comment>Wrote ' . $response->getHeader('Link')[0] . '</comment>'
284 3
            );
285 4
        };
286
287
        $errFunc = function (RequestException $e) use ($output, $file) {
288 1
            $this->errors[$file] = $e->getMessage();
289 1
            $output->writeln(
290 1
                '<error>' . str_pad(
291 1
                    sprintf(
292 1
                        'Failed to write <%s> from \'%s\' with message \'%s\'',
293 1
                        $e->getRequest()->getUri(),
294 1
                        $file,
295 1
                        $e->getMessage()
296 1
                    ),
297 1
                    140,
298
                    ' '
299 1
                ) . '</error>'
300 1
            );
301 1
            if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
302 1
                $this->dumper->dump(
303 1
                    $this->cloner->cloneVar(
304 1
                        $this->parser->parse($e->getResponse()->getBody(), false, false, true)
305 1
                    ),
306
                    function ($line, $depth) use ($output) {
307 1
                        if ($depth > 0) {
308 1
                            $output->writeln(
309 1
                                '<error>' . str_pad(str_repeat('  ', $depth) . $line, 140, ' ') . '</error>'
310 1
                            );
311 1
                        }
312 1
                    }
313 1
                );
314 1
            }
315 4
        };
316
317
        $data = [
318 4
            'json'   => $this->parseContent($content, $file),
319 4
            'upload' => $uploadFile,
320 4
            'headers'=> []
321 4
        ];
322
323
        // Authentication or custom headers.
324 4
        if ($this->headerBasicAuth) {
325
            $data['headers']['Authorization'] = 'Basic '. base64_encode($this->headerBasicAuth);
326
        }
327 4
        if ($this->customHeaders) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->customHeaders of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
328
            foreach ($this->customHeaders as $headers) {
329
                list($key, $value) = explode(':', $headers);
330
                $data['headers'][$key] = $value;
331
            }
332
        }
333 4
        if (empty($data['headers'])) {
334 4
            unset($data['headers']);
335 4
        }
336
337 4
        $promise = $this->client->requestAsync(
338 4
            'PUT',
339 4
            $targetUrl,
340
            $data
341 4
        );
342
343
        // If there is a file to be uploaded, and it exists in remote, we delete it first.
344
        // TODO This part, $uploadFile, promise should be removed once Graviton/File service is resolved in new Story.
345 4
        $fileRepeatFunc = false;
346 4
        if ($uploadFile) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $uploadFile of type false|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
347
            $fileRepeatFunc = function () use ($targetUrl, $successFunc, $errFunc, $output, $file, $data) {
348
                unset($this->errors[$file]);
349
                $output->writeln('<info>File deleting: '.$targetUrl.'</info>');
350
                $deleteRequest = $this->client->requestAsync('DELETE', $targetUrl);
351
                $insert = function () use ($targetUrl, $successFunc, $errFunc, $output, $data) {
352
                    $output->writeln('<info>File inserting: '.$targetUrl.'</info>');
353
                    $promiseInsert = $this->client->requestAsync('PUT', $targetUrl, $data);
354
                    $promiseInsert->then($successFunc, $errFunc);
355
                };
356
                $deleteRequest
357
                    ->then($insert, $errFunc)->wait();
358 1
            };
359 1
        }
360
361 4
        $promiseError = $fileRepeatFunc ? $fileRepeatFunc : $errFunc;
362 4
        if ($sync) {
363
            $promise->then($successFunc, $promiseError)->wait();
364
        } else {
365 4
            $promise->then($successFunc, $promiseError);
366
        }
367
368
369
370 4
        return $promise;
371
    }
372
373
    /**
374
     * parse contents of a file depending on type
375
     *
376
     * @param string $content contents part of file
377
     * @param string $file    full path to file
378
     *
379
     * @return mixed
380
     * @throws UnknownFileTypeException
381
     * @throws JsonParseException
382
     */
383
    protected function parseContent($content, $file)
384
    {
385 4
        if (substr($file, -5) == '.json') {
386 3
            $data = json_decode($content);
387 3
            if (json_last_error() !== JSON_ERROR_NONE) {
388
                throw new JsonParseException(
389
                    sprintf(
390
                        '%s in %s',
391
                        json_last_error_msg(),
392
                        $file
393
                    )
394
                );
395
            }
396 4
        } elseif (substr($file, -4) == '.yml') {
397 1
            $data = $this->parser->parse($content);
398 1
        } else {
399
            throw new UnknownFileTypeException($file);
400
        }
401
402 4
        return $data;
403
    }
404
405
    /**
406
     * Checks if file exists and return qualified fileName location
407
     *
408
     * @param Document $doc        Data source for import data
409
     * @param string   $originFile Original full filename used toimport
410
     * @return bool|mixed
411
     */
412
    private function validateUploadFile(Document $doc, $originFile)
413
    {
414 4
        $documentData = $doc->getData();
415
416 4
        if (!array_key_exists('file', $documentData)) {
417 3
            return false;
418
        }
419
420
        // Find file
421 1
        $fileName = dirname($originFile) . DIRECTORY_SEPARATOR . $documentData['file'];
422 1
        $fileName = str_replace('//', '/', $fileName);
423 1
        if (!file_exists($fileName)) {
424
            return false;
425
        }
426
427 1
        return $fileName;
428
    }
429
}
430