Test Failed
Branch feature/decoupled (5e8293)
by Webysther
02:49
created

Create::getPackagesGenerator()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 8
nc 4
nop 0
dl 0
loc 14
rs 9.2
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the Packagist Mirror.
7
 *
8
 * For the full license information, please view the LICENSE.md
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Webs\Mirror\Command;
13
14
use Symfony\Component\Console\Input\InputInterface;
15
use Symfony\Component\Console\Output\OutputInterface;
16
use Webs\Mirror\ShortName;
17
use Webs\Mirror\Provider;
18
use stdClass;
19
use Generator;
20
use Closure;
21
22
/**
23
 * Create a mirror.
24
 *
25
 * @author Webysther Nunes <[email protected]>
26
 */
27
class Create extends Base
28
{
29
    use ShortName;
30
31
    /**
32
     * @var stdClass
33
     */
34
    protected $providers;
35
36
    /**
37
     * @var array
38
     */
39
    protected $providerIncludes;
40
41
    /**
42
     * @var string
43
     */
44
    protected $currentProvider;
45
46
    /**
47
     * @var Clean
48
     */
49
    protected $clean;
50
51
    /**
52
     * {@inheritdoc}
53
     */
54
    public function __construct($name = '')
55
    {
56
        parent::__construct('create');
57
        $this->setDescription(
58
            'Create/update packagist mirror'
59
        );
60
    }
61
62
    /**
63
     * {@inheritdoc}
64
     */
65
    public function execute(InputInterface $input, OutputInterface $output):int
66
    {
67
        $this->progressBar->setConsole($input, $output);
68
        $this->package->setConsole($input, $output);
69
        $this->package->setHttp($this->http);
70
        $this->provider->setConsole($input, $output);
71
        $this->provider->setHttp($this->http);
72
73
        // Download providers, with repository, is incremental
74
        if ($this->downloadProviders()->stop()) {
75
            return $this->getExitCode();
76
        }
77
78
        // Download packages
79
        if ($this->downloadPackages()->stop()) {
80
            return $this->getExitCode();
81
        }
82
83
        // Switch .packagist.json to packagist.json
84
        if ($this->switch()->stop()) {
85
            return $this->getExitCode();
86
        }
87
88
        $this->setExitCode($this->clean->execute($input, $output));
89
90
        if ($this->initialized) {
91
            $this->filesystem->remove(self::INIT);
0 ignored issues
show
Bug introduced by
The method remove() does not exist on Webs\Mirror\Filesystem. ( Ignorable by Annotation )

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

91
            $this->filesystem->/** @scrutinizer ignore-call */ 
92
                               remove(self::INIT);

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...
92
        }
93
94
        return $this->getExitCode();
95
    }
96
97
    /**
98
     * @param Clean $clean
99
     */
100
    public function setClean(Clean $clean):Create
101
    {
102
        $this->clean = $clean;
103
104
        return $this;
105
    }
106
107
    /**
108
     * @return int
109
     */
110
    protected function getExitCode():int
111
    {
112
        $this->generateHtml();
113
114
        return $this->exitCode;
115
    }
116
117
    /**
118
     * Load main packages.json.
119
     *
120
     * @return Create
121
     */
122
    protected function loadPackagesJson():Create
123
    {
124
        $this->output->writeln(
125
            'Loading providers from <info>'.$this->http->getBaseUri().'</>'
126
        );
127
128
        $this->providers = $this->provider->addFullPath(
129
            $this->package->loadMainJson()
130
        );
131
132
        return $this;
133
    }
134
135
    /**
136
     * Check if packages.json was changed.
137
     *
138
     * @return bool
139
     */
140
    protected function isEqual():bool
141
    {
142
        // if 'p/...' folder not found
143
        if (!$this->filesystem->has(self::TO)) {
144
            $this->filesystem->touch(self::INIT);
145
        }
146
147
        $this->initialized = $this->filesystem->has(self::INIT);
148
149
        $newPackages = json_encode($this->providers, JSON_PRETTY_PRINT);
150
151
        // No provider changed? Just relax...
152
        if ($this->canSkip(self::MAIN)) {
153
            $old = $this->filesystem->getHashFile(self::MAIN);
154
            $new = $this->filesystem->getHash($newPackages);
155
156
            if ($old == $new) {
157
                $this->output->writeln(self::MAIN.' <info>updated</>');
158
                $this->setExitCode(0);
159
160
                return true;
161
            }
162
        }
163
164
        $this->filesystem->write(self::DOT, $newPackages);
165
166
        return false;
167
    }
168
169
    /**
170
     * Switch current packagist.json to space and .packagist to packagist.json.
171
     *
172
     * @return bool True if work, false otherside
173
     */
174
    protected function switch():Create
175
    {
176
        // If .packages.json dont exists
177
        if (!$this->filesystem->has(self::DOT)) {
178
            return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type Webs\Mirror\Command\Create which is incompatible with the documented return type boolean.
Loading history...
179
        }
180
181
        // Move to new location
182
        $this->filesystem->move(self::DOT, self::MAIN);
183
184
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type Webs\Mirror\Command\Create which is incompatible with the documented return type boolean.
Loading history...
185
    }
186
187
    /**
188
     * Download packages.json & provider-xxx$xxx.json.
189
     *
190
     * @return bool True if work, false otherside
191
     */
192
    protected function downloadProviders():Create
193
    {
194
        if ($this->loadPackagesJson()->isEqual()) {
195
            return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type Webs\Mirror\Command\Create which is incompatible with the documented return type boolean.
Loading history...
196
        }
197
198
        $this->providerIncludes = $this->provider->normalize($this->providers);
199
        $generator = $this->getProvidersGenerator();
200
201
        $this->progressBar->start(count($this->providerIncludes));
202
203
        $success = function ($body, $path) {
204
            $this->filesystem->write($path, $body);
205
        };
206
207
        $this->http->pool($generator, $success, $this->getClosureComplete());
208
        $this->progressBar->end();
209
        $this->showErrors();
210
211
        if ($generator->getReturn()) {
1 ignored issue
show
Bug introduced by
The method getReturn() does not exist on Generator. ( Ignorable by Annotation )

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

211
        if ($generator->/** @scrutinizer ignore-call */ getReturn()) {

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...
212
            $this->output->writeln('All providers are <info>updated</>');
213
214
            return $this->setExitCode(0);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->setExitCode(0) returns the type Webs\Mirror\Command\Create which is incompatible with the documented return type boolean.
Loading history...
215
        }
216
217
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type Webs\Mirror\Command\Create which is incompatible with the documented return type boolean.
Loading history...
218
    }
219
220
    /**
221
     * @return Closure
222
     */
223
    protected function getClosureComplete():Closure
224
    {
225
        return function () {
226
            $this->progressBar->progress();
227
        };
228
    }
229
230
    /**
231
     * Download packages.json & provider-xxx$xxx.json.
232
     *
233
     * @return Generator Providers downloaded
234
     */
235
    protected function getProvidersGenerator():Generator
236
    {
237
        $providerIncludes = array_keys($this->providerIncludes);
238
        $updated = true;
239
        foreach ($providerIncludes as $uri) {
240
            if ($this->canSkip($uri)) {
241
                continue;
242
            }
243
244
            $updated = false;
245
            yield $uri => $this->http->getRequest($uri);
246
        }
247
248
        return $updated;
1 ignored issue
show
Bug Best Practice introduced by
The expression return $updated returns the type boolean which is incompatible with the type-hinted return Generator.
Loading history...
249
    }
250
251
    /**
252
     * @param string $path
253
     *
254
     * @return bool
255
     */
256
    protected function canSkip(string $path):bool
257
    {
258
        if ($this->filesystem->has($path) && !$this->initialized) {
259
            return true;
260
        }
261
262
        return false;
263
    }
264
265
    /**
266
     * Show errors.
267
     *
268
     * @return Create
269
     */
270
    protected function showErrors():Create
271
    {
272
        if (!$this->isVerbose()) {
273
            return $this;
274
        }
275
276
        $errors = $this->http->getPoolErrors();
277
        if (count($errors) === 0) {
278
            return $this;
279
        }
280
281
        foreach ($errors as $path => $reason) {
282
            $shortname = $this->shortname($path);
283
            $error = $reason->getCode();
284
            $host = $reason->getRequest()->getUri()->getHost();
285
286
            $this->output->write(
287
                "<comment>$shortname</> failed from ".
288
                "<info>$host</> with HTTP error"
289
            );
290
291
            if (!$error) {
292
                $this->output->writeln(
293
                    ':'.PHP_EOL.'<error>'.$reason->getMessage().'</>'
294
                );
295
                continue;
296
            }
297
298
            $this->output->writeln(" <error>$error</>");
299
        }
300
301
        $this->output->write(PHP_EOL);
302
303
        return $this;
304
    }
305
306
    /**
307
     * Disable mirror when due lots of errors.
308
     */
309
    protected function disableDueErrors()
310
    {
311
        $errors = $this->http->getPoolErrors();
312
        if (count($errors) === 0) {
313
            return $this;
314
        }
315
316
        $counter = [];
317
318
        foreach ($errors as $reason) {
319
            $uri = $reason->getRequest()->getUri();
320
            $host = $uri->getScheme().'://'.$uri->getHost();
321
322
            if (!isset($counter[$host])) {
323
                $counter[$host] = 0;
324
            }
325
326
            ++$counter[$host];
327
        }
328
329
        $mirrors = $this->http->getMirror()->toArray();
0 ignored issues
show
Bug introduced by
The method toArray() does not exist on Webs\Mirror\Mirror. ( Ignorable by Annotation )

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

329
        $mirrors = $this->http->getMirror()->/** @scrutinizer ignore-call */ toArray();

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...
330
331
        foreach ($mirrors as $mirror) {
332
            $total = $counter[$mirror];
333
            if ($total < 1000) {
334
                continue;
335
            }
336
337
            $this->output->write(PHP_EOL);
338
            $this->output->writeln(
339
                '<error>Due to '.
340
                $total.' errors mirror '.
341
                $mirror.' will be disabled</>'
342
            );
343
            $this->output->write(PHP_EOL);
344
            $this->http->getMirror()->remove($mirror);
345
        }
346
347
        return $this;
348
    }
349
350
    /**
351
     * @param string $uri
352
     *
353
     * @return Create
354
     */
355
    protected function loadProviderPackages(string $uri):Create
356
    {
357
        $providers = json_decode($this->filesystem->read($uri))->providers;
358
        $this->providerPackages = $this->package->normalize($providers);
0 ignored issues
show
Bug Best Practice introduced by
The property providerPackages does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
359
        $this->currentProvider = $uri;
360
361
        return $this;
362
    }
363
364
    /**
365
     * Download packages listed on provider-*.json on public/p dir.
366
     *
367
     * @return Create
368
     */
369
    protected function downloadPackages():Create
370
    {
371
        $totalProviders = count($this->providerIncludes);
372
        $currentProvider = 0;
373
374
        $providerIncludes = array_keys($this->providerIncludes);
375
        foreach ($providerIncludes as $uri) {
376
            $shortname = $this->shortname($uri);
377
378
            $this->output->writeln(
379
                '['.++$currentProvider.'/'.$totalProviders.']'.
380
                ' Loading packages from <info>'.$shortname.'</> provider'
381
            );
382
383
            $this->http->useMirrors();
384
            $generator = $this->loadProviderPackages($uri)->getPackagesGenerator();
385
            if (empty(iterator_to_array($generator))) {
386
                continue;
387
            }
388
389
            $this->progressBar->start(count($this->providerPackages));
390
            $this->poolPackages($generator);
391
            $this->progressBar->end();
392
            $this->showErrors()->disableDueErrors()->fallback();
393
        }
394
395
        return $this;
396
    }
397
398
    /**
399
     * Download only a package.
400
     *
401
     * @return Generator Providers downloaded
402
     */
403
    protected function getPackagesGenerator():Generator
404
    {
405
        $providerPackages = array_keys($this->providerPackages);
406
        foreach ($providerPackages as $uri) {
407
            if ($this->filesystem->has($uri)) {
408
                $this->progressBar->progress();
409
                continue;
410
            }
411
412
            if ($this->initialized) {
413
                $uri = $this->http->getMirror()->getNext().'/'.$uri;
414
            }
415
416
            yield $uri => $this->http->getRequest($uri);
417
        }
418
    }
419
420
    /**
421
     * @param Generator $generator
422
     * @param bool|bool $useMirrors
423
     *
424
     * @return Create
425
     */
426
    protected function poolPackages(Generator $generator):Create
427
    {
428
        $this->http->pool(
429
            $generator,
430
            // Success
431
            function ($body, $path) {
432
                $this->filesystem->write($path, $body);
433
                $this->package->setDownloaded($path);
434
            },
435
            // If complete, even failed and success
436
            $this->getCompleteClosure()
0 ignored issues
show
Bug introduced by
The method getCompleteClosure() does not exist on Webs\Mirror\Command\Create. Did you maybe mean getClosureComplete()? ( Ignorable by Annotation )

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

436
            $this->/** @scrutinizer ignore-call */ 
437
                   getCompleteClosure()

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...
437
        );
438
439
        return $this;
440
    }
441
442
    protected function fallback():Create
443
    {
444
        $total = count($this->http->getPoolErrors());
445
446
        if (!$total) {
447
            return $this;
448
        }
449
450
        $shortname = $this->shortname($this->currentProvider);
451
452
        $this->output->writeln(
453
            'Fallback packages from <info>'.$shortname.
454
            '</> provider to main mirror <info>'.$this->http->getBaseUri().'</>'
455
        );
456
457
        $this->providerPackages = $this->http->getPoolErrors();
0 ignored issues
show
Bug Best Practice introduced by
The property providerPackages does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
458
        $generator = $this->getPackagesGenerator();
459
        $this->progressBar->start($total);
460
        $this->poolPackages($generator);
461
        $this->progressBar->end();
462
        $this->showErrors();
0 ignored issues
show
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return Webs\Mirror\Command\Create. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
463
    }
464
465
    /**
466
     * Generate HTML of index.html.
467
     */
468
    protected function generateHtml():Create
469
    {
470
        ob_start();
471
        include getcwd().'/resources/index.html.php';
472
        $this->filesystem->write('index.html', ob_get_clean());
473
474
        return $this;
475
    }
476
}
477