Completed
Push — feature/modernize ( 7c5f32...3c43eb )
by Narcotic
02:12
created

ImportCommand::importResource()   F

Complexity

Conditions 12
Paths 456

Size

Total Lines 110
Code Lines 76

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 156

Importance

Changes 0
Metric Value
dl 0
loc 110
ccs 0
cts 67
cp 0
rs 3.4787
c 0
b 0
f 0
cc 12
eloc 76
nc 456
nop 7
crap 156

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 Symfony\Component\Console\Input\InputArgument;
17
use Symfony\Component\Console\Input\InputOption;
18
use Symfony\Component\Console\Input\InputInterface;
19
use Symfony\Component\Console\Output\OutputInterface;
20
use Symfony\Component\Finder\Finder;
21
use Symfony\Component\Yaml\Parser;
22
use Symfony\Component\VarDumper\Cloner\VarCloner;
23
use Symfony\Component\VarDumper\Dumper\CliDumper as Dumper;
24
use GuzzleHttp\Promise;
25
use Symfony\Component\Finder\SplFileInfo;
26
use Webuni\FrontMatter\FrontMatter;
27
use Webuni\FrontMatter\Document;
28
29
/**
30
 * @author   List of contributors <https://github.com/libgraviton/import-export/graphs/contributors>
31
 * @license  http://opensource.org/licenses/gpl-license.php GNU Public License
32
 * @link     http://swisscom.ch
33
 */
34
class ImportCommand extends ImportCommandAbstract
35
{
36
    /**
37
     * @var HttpClient
38
     */
39
    private $client;
40
41
    /**
42
     * @var FrontMatter
43
     */
44
    private $frontMatter;
45
46
    /**
47
     * @var Parser
48
     */
49
    private $parser;
50
51
    /**
52
     * @var VarCloner
53
     */
54
    private $cloner;
55
56
    /**
57
     * @var Dumper
58
     */
59
    private $dumper;
60
61
    /**
62
     * Count of errors
63
     * @var array
64
     */
65
    private $errors = [];
66
67
    /**
68
     * Header basic auth
69
     * @var string
70
     */
71
    private $headerBasicAuth;
72
73
    /**
74
     * Header for custom variables
75
     * @var array
76
     */
77
    private $customHeaders;
78
79
    /**
80
     * @param HttpClient  $client      Grv HttpClient guzzle http client
81
     * @param Finder      $finder      symfony/finder instance
82
     * @param FrontMatter $frontMatter frontmatter parser
83
     * @param Parser      $parser      yaml/json parser
84
     * @param VarCloner   $cloner      var cloner for dumping reponses
85
     * @param Dumper      $dumper      dumper for outputing responses
86
     */
87
    public function __construct(
88
        HttpClient $client,
89
        Finder $finder,
90 5
        FrontMatter $frontMatter,
91
        Parser $parser,
92
        VarCloner $cloner,
93
        Dumper $dumper
94
    ) {
95
        parent::__construct(
96
            $finder
97
        );
98 5
        $this->client = $client;
99
        $this->frontMatter = $frontMatter;
100 5
        $this->parser = $parser;
101 5
        $this->cloner = $cloner;
102 5
        $this->dumper = $dumper;
103 5
    }
104 5
105 5
    /**
106 5
     * Configures the current command.
107
     *
108
     * @return void
109
     */
110
    protected function configure()
111
    {
112
        $this
113 5
            ->setName('graviton:import')
114
            ->setDescription('Import files from a folder or file.')
115 5
            ->addOption(
116 5
                'rewrite-host',
117 5
                'r',
118 5
                InputOption::VALUE_OPTIONAL,
119 5
                'Replace the value of this option with the <host> value before importing.',
120 5
                'http://localhost'
121 5
            )
122 5
            ->addOption(
123
                'rewrite-to',
124 5
                't',
125 5
                InputOption::VALUE_OPTIONAL,
126 5
                'String to use as the replacement value for the [REWRITE-HOST] string.',
127 5
                '<host>'
128 5
            )
129 5
            ->addOption(
130
                'sync-requests',
131 5
                's',
132 5
                InputOption::VALUE_NONE,
133 5
                'Send requests synchronously'
134 5
            )
135 5
            ->addOption(
136
                'no-overwrite',
137 5
                'o',
138 5
                InputOption::VALUE_NONE,
139 5
                'If set, we will check for record existence and not overwrite existing ones.'
140 5
            )
141 5
            ->addOption(
142
                'headers-basic-auth',
143 5
                'a',
144 5
                InputOption::VALUE_OPTIONAL,
145 5
                'Header user:password for Basic auth'
146 5
            )
147 5
            ->addOption(
148
                'custom-headers',
149 5
                'c',
150 5
                InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
151 5
                'Custom Header variable(s), -c{key:value} and multiple is optional.'
152 5
            )
153 5
            ->addOption(
154
                'input-file',
155 5
                'i',
156 5
                InputOption::VALUE_REQUIRED,
157 5
                'If provided, the list of files to load will be loaded from this file, one file per line.'
158 5
            )
159 5
            ->addArgument(
160
                'host',
161 5
                InputArgument::REQUIRED,
162 5
                'Protocol and host to load data into (ie. https://graviton.nova.scapp.io)'
163 5
            )
164 5
            ->addArgument(
165
                'file',
166 5
                InputArgument::IS_ARRAY,
167 5
                'Directories or files to load'
168 5
            );
169 5
    }
170
171 5
    /**
172 5
     * Executes the current command.
173
     *
174
     * @param Finder          $finder Finder
175
     * @param InputInterface  $input  User input on console
176
     * @param OutputInterface $output Output of the command
177
     *
178
     * @return integer
179
     */
180
    protected function doImport(Finder $finder, InputInterface $input, OutputInterface $output)
181
    {
182
        $exitCode = 0;
183
        $host = $input->getArgument('host');
184
        $rewriteHost = $input->getOption('rewrite-host');
185
        $rewriteTo = $input->getOption('rewrite-to');
186
        $this->headerBasicAuth = $input->getOption('headers-basic-auth');
187
        $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...
188
        if ($rewriteTo === $this->getDefinition()->getOption('rewrite-to')->getDefault()) {
189
            $rewriteTo = $host;
190
        }
191
        $noOverwrite = $input->getOption('no-overwrite');
192
193
        $this->importPaths($finder, $output, $host, $rewriteHost, $rewriteTo, $noOverwrite);
194
195
        // Error exit
196
        if (empty($this->errors)) {
197
            // No errors
198
            $output->writeln("\n".'<info>No errors</info>');
199
        } else {
200
            // Yes, there was errors
201
            $output->writeln("\n".'<info>There was errors: '.count($this->errors).'</info>');
202
            foreach ($this->errors as $file => $error) {
203
                $output->writeln("<error>{$file}: {$error}</error>");
204
            }
205
            $exitCode = 1;
206
        }
207
        return $exitCode;
208
    }
209
210
    /**
211
     * @param Finder          $finder      finder primmed with files to import
212
     * @param OutputInterface $output      output interfac
213
     * @param string          $host        host to import into
214
     * @param string          $rewriteHost string to replace with value from $rewriteTo during loading
215
     * @param string          $rewriteTo   string to replace value from $rewriteHost with during loading
216
     * @param boolean         $sync        send requests syncronously
0 ignored issues
show
Bug introduced by
There is no parameter named $sync. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
217
     * @param boolean         $noOverwrite should we not overwrite existing records?
218
     *
219
     * @return void
220
     *
221
     * @throws MissingTargetException
222
     */
223
    protected function importPaths(
224
        Finder $finder,
225
        OutputInterface $output,
226
        $host,
227
        $rewriteHost,
228
        $rewriteTo,
229
        $noOverwrite = false
230
    ) {
231
        /** @var SplFileInfo $file */
232
        foreach ($finder as $file) {
233
            $doc = $this->frontMatter->parse($file->getContents());
234
235
            $output->writeln("<info>Loading data from ${file}</info>");
236
237
            if (!array_key_exists('target', $doc->getData())) {
238
                throw new MissingTargetException('Missing target in \'' . $file . '\'');
239
            }
240
241
            $targetUrl = sprintf('%s%s', $host, $doc->getData()['target']);
242
243
            $this->importResource(
244
                $targetUrl,
245
                (string) $file,
246
                $output,
247
                $doc,
248
                $rewriteHost,
249
                $rewriteTo,
250
                $noOverwrite
251
            );
252
        }
253
    }
254
255
    /**
256
     * @param string          $targetUrl   target url to import resource into
257
     * @param string          $file        path to file being loaded
258
     * @param OutputInterface $output      output of the command
259
     * @param Document        $doc         document to load
260
     * @param string          $rewriteHost string to replace with value from $host during loading
261
     * @param string          $rewriteTo   string to replace value from $rewriteHost with during loading
262
     * @param boolean         $noOverwrite should we not overwrite existing records?
263
     *
264
     * @return Promise\PromiseInterface|null
265
     */
266
    protected function importResource(
267
        $targetUrl,
268
        $file,
269
        OutputInterface $output,
270
        Document $doc,
271
        $rewriteHost,
272
        $rewriteTo,
273
        $noOverwrite = false
274
    ) {
275
        $content = str_replace($rewriteHost, $rewriteTo, $doc->getContent());
276
        $uploadFile = $this->validateUploadFile($doc, $file);
277
278
        $data = [
279
            'json'   => $this->parseContent($content, $file),
280
            'upload' => $uploadFile,
281
            'headers'=> []
282
        ];
283
284
        // Authentication or custom headers.
285
        if ($this->headerBasicAuth) {
286
            $data['headers']['Authorization'] = 'Basic '. base64_encode($this->headerBasicAuth);
287
        }
288
        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...
289
            foreach ($this->customHeaders as $headers) {
290
                list($key, $value) = explode(':', $headers);
291
                $data['headers'][$key] = $value;
292
            }
293
        }
294
        if (empty($data['headers'])) {
295
            unset($data['headers']);
296
        }
297
298
        // skip if no overwriting has been requested
299
        if ($noOverwrite) {
300
            $response = $this->client->request('GET', $targetUrl, array_merge($data, ['http_errors' => false]));
301
            if ($response->getStatusCode() == 200) {
302
                $output->writeln(
303
                    '<info>' . str_pad(
304
                        sprintf(
305
                            'Skipping <%s> as "no overwrite" is activated and it does exist.',
306
                            $targetUrl
307
                        ),
308
                        140,
309
                        ' '
310
                    ) . '</info>'
311
                );
312
                return;
313
            }
314
        }
315
316
        try {
317
            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...
318
                unset($this->errors[$file]);
319
                try {
320
                    $this->client->request('DELETE', $targetUrl, $data);
321
                    $output->writeln('<info>File deleted: '.$targetUrl.'</info>');
322
                } catch (\Exception $e) {
323
                    $output->writeln(
324
                        '<error>' . str_pad(
325
                            sprintf(
326
                                'Failed to delete <%s> with message \'%s\'',
327
                                $targetUrl,
328
                                $e->getMessage()
329
                            ),
330
                            140,
331
                            ' '
332
                        ) . '</error>'
333
                    );
334
                }
335
            }
336
337
            $response = $this->client->request(
338
                'PUT',
339
                $targetUrl,
340
                $data
341
            );
342
343
            $output->writeln(
344
                '<comment>Wrote ' . $response->getHeader('Link')[0] . '</comment>'
345
            );
346
        } catch (\Exception $e) {
347
            $this->errors[$file] = $e->getMessage();
348
            $output->writeln(
349
                '<error>' . str_pad(
350
                    sprintf(
351
                        'Failed to write <%s> from \'%s\' with message \'%s\'',
352
                        $e->getRequest()->getUri(),
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Exception as the method getRequest() does only exist in the following sub-classes of Exception: GuzzleHttp\Exception\BadResponseException, GuzzleHttp\Exception\ClientException, GuzzleHttp\Exception\ConnectException, GuzzleHttp\Exception\RequestException, GuzzleHttp\Exception\ServerException, GuzzleHttp\Exception\TooManyRedirectsException. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
353
                        $file,
354
                        $e->getMessage()
355
                    ),
356
                    140,
357
                    ' '
358
                ) . '</error>'
359
            );
360
            if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
361
                $this->dumper->dump(
362
                    $this->cloner->cloneVar(
363
                        $this->parser->parse($e->getResponse()->getBody(), false, false, true)
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Exception as the method getResponse() does only exist in the following sub-classes of Exception: GuzzleHttp\Exception\BadResponseException, GuzzleHttp\Exception\ClientException, GuzzleHttp\Exception\ConnectException, GuzzleHttp\Exception\RequestException, GuzzleHttp\Exception\ServerException, GuzzleHttp\Exception\TooManyRedirectsException. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
364
                    ),
365
                    function ($line, $depth) use ($output) {
366
                        if ($depth > 0) {
367
                            $output->writeln(
368
                                '<error>' . str_pad(str_repeat('  ', $depth) . $line, 140, ' ') . '</error>'
369
                            );
370
                        }
371
                    }
372
                );
373
            }
374
        }
375
    }
376
377
    /**
378
     * parse contents of a file depending on type
379
     *
380
     * @param string $content contents part of file
381
     * @param string $file    full path to file
382
     *
383
     * @return mixed
384
     * @throws UnknownFileTypeException
385
     * @throws JsonParseException
386
     */
387
    protected function parseContent($content, $file)
388
    {
389
        if (substr($file, -5) == '.json') {
390
            $data = json_decode($content);
391
            if (json_last_error() !== JSON_ERROR_NONE) {
392
                throw new JsonParseException(
393
                    sprintf(
394
                        '%s in %s',
395
                        json_last_error_msg(),
396
                        $file
397
                    )
398
                );
399
            }
400
        } elseif (substr($file, -4) == '.yml') {
401
            $data = $this->parser->parse($content);
402
        } else {
403
            throw new UnknownFileTypeException($file);
404
        }
405
406
        return $data;
407
    }
408
409
    /**
410
     * Checks if file exists and return qualified fileName location
411
     *
412
     * @param Document $doc        Data source for import data
413
     * @param string   $originFile Original full filename used toimport
414
     * @return bool|mixed
415
     */
416
    private function validateUploadFile(Document $doc, $originFile)
417
    {
418
        $documentData = $doc->getData();
419
420
        if (!array_key_exists('file', $documentData)) {
421
            return false;
422
        }
423
424
        // Find file
425
        $fileName = dirname($originFile) . DIRECTORY_SEPARATOR . $documentData['file'];
426
        $fileName = str_replace('//', '/', $fileName);
427
        if (!file_exists($fileName)) {
428
            return false;
429
        }
430
431
        return $fileName;
432
    }
433
}
434