eliashaeussler/composer-update-check

View on GitHub
src/Command/UpdateCheckCommand.php

Summary

Maintainability
B
5 hrs
Test Coverage
A
91%
<?php

declare(strict_types=1);

namespace EliasHaeussler\ComposerUpdateCheck\Command;

/*
 * This file is part of the Composer package "eliashaeussler/composer-update-check".
 *
 * Copyright (C) 2020 Elias Häußler <elias@haeussler.dev>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */

use Composer\Command\BaseCommand;
use Composer\Factory;
use Composer\IO\BufferIO;
use EliasHaeussler\ComposerUpdateCheck\IO\OutputBehavior;
use EliasHaeussler\ComposerUpdateCheck\IO\Style;
use EliasHaeussler\ComposerUpdateCheck\IO\Verbosity;
use EliasHaeussler\ComposerUpdateCheck\Options;
use EliasHaeussler\ComposerUpdateCheck\Package\UpdateCheckResult;
use EliasHaeussler\ComposerUpdateCheck\UpdateChecker;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

/**
 * UpdateCheckCommand.
 *
 * @author Elias Häußler <elias@haeussler.dev>
 * @license GPL-3.0-or-later
 */
class UpdateCheckCommand extends BaseCommand
{
    /**
     * @var SymfonyStyle
     */
    private $symfonyStyle;

    /**
     * @var OutputBehavior
     */
    private $behavior;

    protected function configure(): void
    {
        $this->setName('update-check');
        $this->setDescription('Checks your root requirements for available updates.');

        $this->addOption(
            'ignore-packages',
            'i',
            InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
            'Packages to ignore when checking for available updates',
            []
        );
        $this->addOption(
            'no-dev',
            null,
            InputOption::VALUE_NONE,
            'Disables update check of require-dev packages.'
        );
        $this->addOption(
            'security-scan',
            's',
            InputOption::VALUE_NONE,
            'Run security scan for all outdated packages'
        );
        $this->addOption(
            'json',
            'j',
            InputOption::VALUE_NONE,
            'Format update check as JSON'
        );
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $this->symfonyStyle = new SymfonyStyle($input, $output);

        // Prepare command options
        $json = $input->getOption('json');
        $securityScan = $input->getOption('security-scan');

        // Initialize IO
        $style = new Style($json ? Style::JSON : Style::NORMAL);
        $verbosity = new Verbosity($style->isJson() ? OutputInterface::VERBOSITY_NORMAL : OutputInterface::VERBOSITY_VERBOSE);
        $this->behavior = new OutputBehavior($style, $verbosity, $this->getIO());
        $output->setVerbosity($verbosity->getLevel());

        // Run update check
        $composer = Factory::create(new BufferIO());
        $updateChecker = new UpdateChecker($composer, $this->behavior, Options::fromInput($input));
        $result = $updateChecker->run();

        // Decorate update check result
        $this->decorateResult($result, $updateChecker->getPackageBlacklist(), $securityScan);

        return 0;
    }

    /**
     * @param string[] $ignoredPackages
     */
    private function decorateResult(UpdateCheckResult $result, array $ignoredPackages, bool $flagInsecurePackages = false): void
    {
        $outdatedPackages = $result->getOutdatedPackages();

        // Print message if no packages are outdated
        if ([] === $outdatedPackages) {
            $countSkipped = count($ignoredPackages);
            $message = sprintf(
                'All packages are up to date%s.',
                $countSkipped > 0 ? sprintf(' (skipped %d package%s)', $countSkipped, 1 !== $countSkipped ? 's' : '') : ''
            );
            if ($this->behavior->style->isJson()) {
                $this->buildJsonReport(['status' => $message], $ignoredPackages);
            } else {
                $this->symfonyStyle->success($message);
            }

            return;
        }

        // Print header
        $statusLabel = 1 === count($outdatedPackages)
            ? '1 package is outdated.'
            : sprintf('%d packages are outdated.', count($outdatedPackages));
        if (!$this->behavior->style->isJson()) {
            $this->symfonyStyle->warning($statusLabel);
        }

        // Parse table rows
        $tableRows = [];
        foreach ($outdatedPackages as $outdatedPackage) {
            $report = [
                $outdatedPackage->getName(),
                $outdatedPackage->getOutdatedVersion(),
                $outdatedPackage->getNewVersion(),
            ];
            if ($flagInsecurePackages) {
                if (!$this->behavior->style->isJson() && $outdatedPackage->isInsecure()) {
                    $report[1] .= ' <fg=red;options=bold>insecure</>';
                } elseif ($this->behavior->style->isJson()) {
                    $report[] = $outdatedPackage->isInsecure();
                }
            }
            $tableRows[] = $report;
        }

        // Print table
        $tableHeader = ['Package', 'Outdated version', 'New version'];
        if (!$this->behavior->style->isJson()) {
            $this->symfonyStyle->table($tableHeader, $tableRows);
        } else {
            $result = [];
            if ($flagInsecurePackages) {
                $tableHeader[] = 'Insecure';
            }
            foreach ($tableRows as $tableRow) {
                $result[] = array_combine($tableHeader, $tableRow);
            }
            $this->buildJsonReport(['status' => $statusLabel, 'result' => $result], $ignoredPackages);
        }
    }

    /**
     * @param array{status: string, result?: array<int, array<string, mixed>>} $report
     * @param string[]                                                         $ignoredPackages
     */
    private function buildJsonReport(array $report, array $ignoredPackages = []): void
    {
        if ([] !== $ignoredPackages) {
            $report['skipped'] = $ignoredPackages;
        }
        $this->symfonyStyle->writeln(json_encode($report));
    }
}