http = resolve(HttpFactory::class); if (!$this->resolveInput()) { return 1; } $months = (int) $this->argument('months'); $this->info(sprintf('Checking: %s/%s', $this->vendor, $this->package)); $this->info('Months: ' . $months); $payload = $this->fetchPackageMetadata(); if ($payload === null) { return 1; } $this->filter = now()->subMonths($months)->day(1)->toDateString(); try { $pkg = new PackagistApiPackagePayload($payload->json()); $this->info('Found the package. Type: ' . $pkg->type); $versions = collect($pkg->versions ?? []) ->keys() ->filter(fn ($version) => \str_starts_with($version, 'dev-')) ->sort() ->values(); $this->totalBranches = $versions->count(); $this->info( sprintf( 'Package has %d branches. Starting to download statistics.', $this->totalBranches ) ); $responses = $this->http->pool( fn (Pool $pool) => $versions->map( fn ($branch) => $pool->as($branch)->timeout(self::TIMEOUT_SECONDS)->get($this->getStatsUrl($branch)) )->toArray() ); $statistics = $this->collectBranchStats($versions, $responses); $this->info('Downloaded statistics...'); if ($this->outputTable($statistics)) { $this->outputSuggestions($statistics); } } catch (\Throwable $e) { if ($e instanceof \TypeError) { throw $e; } $this->error($e->getMessage()); return 1; } return 0; } /** Parse and validate vendor/package input arguments. */ private function resolveInput(): bool { $vendor = strtolower((string) $this->argument('vendor')); $package = $this->argument('package'); if (str_contains($vendor, '/')) { if ($package !== null) { $this->error( 'Conflicting arguments: vendor/package format' . ' and separate package argument cannot be used together.' ); return false; } [$vendor, $package] = explode('/', $vendor, 2); } if ($package === null || $package === '') { $this->error('Missing package name. Usage: check vendor/package or check vendor package'); return false; } $package = strtolower((string) $package); if (!preg_match(self::NAME_PATTERN, $vendor)) { $this->error("Invalid vendor name: {$vendor}"); return false; } if (!preg_match(self::NAME_PATTERN, $package)) { $this->error("Invalid package name: {$package}"); return false; } $this->vendor = $vendor; $this->package = $package; return true; } /** Fetch package metadata from Packagist. */ private function fetchPackageMetadata(): ?\Illuminate\Http\Client\Response { $payload = $this->http->timeout(self::TIMEOUT_SECONDS)->get( sprintf( self::PACKAGIST_URL . '.json', $this->vendor, $this->package ) ); if ($payload->failed()) { if ($payload->status() === 404) { $this->error("Package not found: {$this->vendor}/{$this->package}"); return null; } $this->error("Failed to fetch package metadata (HTTP {$payload->status()})"); return null; } return $payload; } /** Build the Packagist stats API URL for a branch. */ private function getStatsUrl(string $branch): string { return sprintf( self::PACKAGIST_URL . '/stats/%s.json?average=monthly&from=%s', $this->vendor, $this->package, $branch, $this->filter ); } /** Parse pooled responses into branch download statistics. */ private function collectBranchStats($versions, array $responses): array { $statistics = []; foreach ($versions as $branch) { $response = $responses[$branch]; if ($response instanceof \Throwable) { $this->warn("Failed to fetch stats for {$branch}, skipping."); continue; } if ($response->failed()) { $this->warn("Failed to fetch stats for {$branch} (HTTP {$response->status()}), skipping."); continue; } $data = collect($response->json()); $labels = collect($data->get('labels', []))->toArray(); $values = collect($data->get('values', []))->flatten()->toArray(); $labels[] = 'Total'; $values[] = array_sum($values); if (count($labels) !== count($values)) { $this->warn(sprintf( 'Malformed stats for %s (labels: %d, values: %d), skipping.', $branch, count($labels), count($values) )); continue; } $statistics[$branch] = \array_combine($labels, $values); } return $statistics; } /** Render the download statistics table. */ private function outputTable(array $statistics): bool { if (empty($statistics)) { $this->info('No statistics found... Stopping.'); return false; } $tableHeaders = ['' => 'Branch']; $tableBranches = []; foreach ($statistics as $branch => $stats) { foreach ($stats as $m => $v) { $tableHeaders[$m] = (string) $m; $tableBranches[$branch][$branch] = $branch; $tableBranches[$branch][$m] = (string) $v; } } $this->line(''); $this->table($tableHeaders, $tableBranches); return true; } /** Render suggestions for zero-download branches. */ private function outputSuggestions(array $statistics): void { $deletable = []; foreach ($statistics as $k => $values) { if (!empty($values['Total'])) { continue; } $deletable[] = $k; } if (empty($deletable)) { $this->info('No suggestions available. Good job!'); return; } $branches = collect($deletable)->mapWithKeys(function ($branch) { return [ $branch => [ $branch, sprintf( self::PACKAGIST_URL . '#%s', $this->vendor, $this->package, $branch ), ], ]; }); $this->line(''); $this->info( sprintf( 'Found %d branches (out of %d total) with no downloads since %s', $branches->count(), $this->totalBranches, $this->filter ) ); $this->table(['Branch', 'URL'], $branches); } }