Completed
Push — master ( 41bf94...7c1797 )
by Narcotic
04:34 queued 02:16
created

ImportCommand::doImport()   B

Complexity

Conditions 4
Paths 6

Size

Total Lines 28
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 0
loc 28
ccs 7
cts 7
cp 1
rs 8.5806
cc 4
eloc 18
nc 6
nop 3
crap 4
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 5
     * @param HttpClient  $client      Grv HttpClient guzzle http client
72
     * @param Finder      $finder      symfony/finder instance
73
     * @param FrontMatter $frontMatter frontmatter parser
74
     * @param Parser      $parser      yaml/json parser
75
     * @param VarCloner   $cloner      var cloner for dumping reponses
76
     * @param Dumper      $dumper      dumper for outputing responses
77
     */
78
    public function __construct(
79 5
        HttpClient $client,
80
        Finder $finder,
81 5
        FrontMatter $frontMatter,
82 5
        Parser $parser,
83 5
        VarCloner $cloner,
84 5
        Dumper $dumper
85 5
    ) {
86 5
        parent::__construct(
87 5
            $finder
88
        );
89
        $this->client = $client;
90
        $this->frontMatter = $frontMatter;
91
        $this->parser = $parser;
92
        $this->cloner = $cloner;
93
        $this->dumper = $dumper;
94 5
    }
95
96 5
    /**
97 5
     * Configures the current command.
98 5
     *
99 5
     * @return void
100 5
     */
101 5
    protected function configure()
102 5
    {
103 5
        $this
104
            ->setName('graviton:import')
105 5
            ->setDescription('Import files from a folder or file.')
106 5
            ->addOption(
107 5
                'rewrite-host',
108 5
                'r',
109 5
                InputOption::VALUE_OPTIONAL,
110 5
                'Replace the value of this option with the <host> value before importing.',
111
                'http://localhost'
112 5
            )
113 5
            ->addOption(
114 5
                'rewrite-to',
115 5
                't',
116 5
                InputOption::VALUE_OPTIONAL,
117
                'String to use as the replacement value for the [REWRITE-HOST] string.',
118 5
                '<host>'
119 5
            )
120 5
            ->addOption(
121 5
                'sync-requests',
122
                's',
123 5
                InputOption::VALUE_NONE,
124 5
                'Send requests synchronously'
125 5
            )
126 5
            ->addArgument(
127
                'host',
128 5
                InputArgument::REQUIRED,
129 5
                'Protocol and host to load data into (ie. https://graviton.nova.scapp.io)'
130
            )
131
            ->addArgument(
132
                'file',
133
                InputArgument::REQUIRED + InputArgument::IS_ARRAY,
134
                'Directories or files to load'
135
            );
136
    }
137
138
    /**
139
     * Executes the current command.
140 5
     *
141
     * @param Finder          $finder Finder
142 5
     * @param InputInterface  $input  User input on console
143 5
     * @param OutputInterface $output Output of the command
144 5
     *
145 5
     * @return void
146 5
     */
147 5
    protected function doImport(Finder $finder, InputInterface $input, OutputInterface $output)
148 5
    {
149
        $host = $input->getArgument('host');
150 5
        $rewriteHost = $input->getOption('rewrite-host');
151 4
        $rewriteTo = $input->getOption('rewrite-to');
152
        if ($rewriteTo === $this->getDefinition()->getOption('rewrite-to')->getDefault()) {
153
            $rewriteTo = $host;
154
        }
155
        $sync = $input->getOption('sync-requests');
156
157
        $this->importPaths($finder, $output, $host, $rewriteHost, $rewriteTo, $sync);
158
159
        // Error exit
160
        if (empty($this->errors)) {
161
            // No errors
162
            $output->writeln("\n".'<info>No errors</info>');
163
            $output->writeln('0');
164
            exit(0);
165 5
        } else {
166
            // Yes, there was errors
167
            $output->writeln("\n".'<info>There was errors: '.count($this->errors).'</info>');
168
            foreach ($this->errors as $file => $error) {
169
                $output->writeln("<error>{$file}: {$error}</error>");
170
            }
171
            $output->writeln('1');
172
            exit(1);
173 5
        }
174 5
    }
175 5
176
    /**
177 5
     * @param Finder          $finder      finder primmed with files to import
178
     * @param OutputInterface $output      output interfac
179 5
     * @param string          $host        host to import into
180 1
     * @param string          $rewriteHost string to replace with value from $rewriteTo during loading
181
     * @param string          $rewriteTo   string to replace value from $rewriteHost with during loading
182
     * @param boolean         $sync        send requests syncronously
183 4
     *
184
     * @return void
185 4
     *
186 4
     * @throws MissingTargetException
187 4
     */
188 4
    protected function importPaths(
189 4
        Finder $finder,
190 4
        OutputInterface $output,
191 4
        $host,
192 4
        $rewriteHost,
193
        $rewriteTo,
194 4
        $sync = false
195 4
    ) {
196
        $promises = [];
197
        /** @var SplFileInfo $file */
198
        foreach ($finder as $file) {
199
            $doc = $this->frontMatter->parse($file->getContents());
200
201
            $output->writeln("<info>Loading data from ${file}</info>");
202
203
            if (!array_key_exists('target', $doc->getData())) {
204
                throw new MissingTargetException('Missing target in \'' . $file . '\'');
205
            }
206
207
            $targetUrl = sprintf('%s%s', $host, $doc->getData()['target']);
208
209
            $promises[] = $this->importResource(
210
                $targetUrl,
211
                (string) $file,
212
                $output,
213
                $doc,
214
                $rewriteHost,
215
                $rewriteTo,
216
                $sync
217
            );
218
        }
219
220
        try {
221
            Promise\unwrap($promises);
222
        } catch (ClientException $e) {
223
            // silently ignored since we already output an error when the promise fails
224
        }
225
    }
226 4
227 4
    /**
228
     * @param string          $targetUrl   target url to import resource into
229
     * @param string          $file        path to file being loaded
230 3
     * @param OutputInterface $output      output of the command
231 3
     * @param Document        $doc         document to load
232 3
     * @param string          $rewriteHost string to replace with value from $host during loading
233 4
     * @param string          $rewriteTo   string to replace value from $rewriteHost with during loading
234
     * @param boolean         $sync        send requests syncronously
235
     *
236 1
     * @return Promise\Promise|null
237 1
     */
238 1
    protected function importResource(
239 1
        $targetUrl,
240 1
        $file,
241 1
        OutputInterface $output,
242 1
        Document $doc,
243 1
        $rewriteHost,
244 1
        $rewriteTo,
245
        $sync = false
246 1
    ) {
247 1
        $content = str_replace($rewriteHost, $rewriteTo, $doc->getContent());
248 1
        $uploadFile = $this->validateUploadFile($doc, $file);
249 1
250 1
        $successFunc = function (ResponseInterface $response) use ($output) {
251 1
            $output->writeln(
252 1
                '<comment>Wrote ' . $response->getHeader('Link')[0] . '</comment>'
253
            );
254 1
        };
255 1
256 1
        $errFunc = function (RequestException $e) use ($output, $file) {
257 1
            $this->errors[$file] = $e->getMessage();
258 1
            $output->writeln(
259 1
                '<error>' . str_pad(
260 1
                    sprintf(
261 1
                        'Failed to write <%s> from \'%s\' with message \'%s\'',
262 4
                        $e->getRequest()->getUri(),
263
                        $file,
264 4
                        $e->getMessage()
265 4
                    ),
266 4
                    140,
267 4
                    ' '
268
                ) . '</error>'
269 4
            );
270
            if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
271 4
                $this->dumper->dump(
272 4
                    $this->cloner->cloneVar(
273 4
                        $this->parser->parse($e->getResponse()->getBody(), false, false, true)
274 4
                    ),
275
                    function ($line, $depth) use ($output) {
276
                        if ($depth > 0) {
277
                            $output->writeln(
278
                                '<error>' . str_pad(str_repeat('  ', $depth) . $line, 140, ' ') . '</error>'
279
                            );
280
                        }
281
                    }
282
                );
283
            }
284
        };
285
286
        $data = [
287
            'json'   => $this->parseContent($content, $file),
288
            'upload' => $uploadFile
289
        ];
290
        $promise = $this->client->requestAsync(
291
            'PUT',
292
            $targetUrl,
293
            $data
294
        );
295 4
296
        // If there is a file to be uploaded, and it exists in remote, we delete it first.
297
        // TODO This part, $uploadFile, promise should be removed once Graviton/File service is resolved in new Story.
298
        $fileRepeatFunc = false;
299
        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...
300
            $fileRepeatFunc = function () use ($targetUrl, $successFunc, $errFunc, $output, $file, $data) {
301
                unset($this->errors[$file]);
302
                $output->writeln('<info>File deleting: '.$targetUrl.'</info>');
303
                $deleteRequest = $this->client->requestAsync('DELETE', $targetUrl);
304
                $insert = function () use ($targetUrl, $successFunc, $errFunc, $output, $data) {
305
                    $output->writeln('<info>File inserting: '.$targetUrl.'</info>');
306
                    $promiseInsert = $this->client->requestAsync('PUT', $targetUrl, $data);
307
                    $promiseInsert->then($successFunc, $errFunc);
308 4
                };
309 3
                $deleteRequest
310 3
                    ->then($insert, $errFunc)->wait();
311
            };
312
        }
313
314
        $promiseError = $fileRepeatFunc ? $fileRepeatFunc : $errFunc;
315
        if ($sync) {
316
            $promise->then($successFunc, $promiseError)->wait();
317
        } else {
318
            $promise->then($successFunc, $promiseError);
319 4
        }
320 1
321 1
322
323
        return $promise;
324
    }
325 4
326
    /**
327
     * parse contents of a file depending on type
328
     *
329
     * @param string $content contents part of file
330
     * @param string $file    full path to file
331
     *
332
     * @return mixed
333
     * @throws UnknownFileTypeException
334
     * @throws JsonParseException
335
     */
336
    protected function parseContent($content, $file)
337 4
    {
338
        if (substr($file, -5) == '.json') {
339 4
            $data = json_decode($content);
340 3
            if (json_last_error() !== JSON_ERROR_NONE) {
341
                throw new JsonParseException(
342
                    sprintf(
343
                        '%s in %s',
344 1
                        json_last_error_msg(),
345 1
                        $file
346 1
                    )
347
                );
348
            }
349
        } elseif (substr($file, -4) == '.yml') {
350 1
            $data = $this->parser->parse($content);
351
        } else {
352
            throw new UnknownFileTypeException($file);
353
        }
354
355
        return $data;
356
    }
357
358
    /**
359
     * Checks if file exists and return qualified fileName location
360
     *
361
     * @param Document $doc        Data source for import data
362
     * @param string   $originFile Original full filename used toimport
363
     * @return bool|mixed
364
     */
365
    private function validateUploadFile(Document $doc, $originFile)
366
    {
367
        $documentData = $doc->getData();
368
369
        if (!array_key_exists('file', $documentData)) {
370
            return false;
371
        }
372
373
        // Find file
374
        $fileName = dirname($originFile) . DIRECTORY_SEPARATOR . $documentData['file'];
375
        $fileName = str_replace('//', '/', $fileName);
376
        if (!file_exists($fileName)) {
377
            return false;
378
        }
379
380
        return $fileName;
381
    }
382
}
383