-
-
Save KayKursawe/524036c6c95fe4214a8759b9fc46704f to your computer and use it in GitHub Desktop.
Quick'n'dirty Symfony console command to resolve package names and versions
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Console; | |
use Error; | |
use Exception; | |
use ReflectionClass; | |
use Symfony\Component\Console\Command\Command; | |
use Symfony\Component\Console\Input\InputInterface; | |
use Symfony\Component\Console\Input\InputOption; | |
use Symfony\Component\Console\Output\OutputInterface; | |
class ComposerCheckCommand extends Command | |
{ | |
protected const OPTION_COMPOSER_DIR_NAME = 'composer-dir'; | |
protected const VERSION_CONSTRAINT = 'constraint'; | |
protected const VERSION_INSTALLED = 'installed'; | |
protected const IS_DEV = 'is_dev'; | |
protected function configure(): void | |
{ | |
$this->setName('composer:check') | |
->setDescription('Prints all composer package names and version info (if any) that should be installed as hard dependencies') | |
->addOption(self::OPTION_COMPOSER_DIR_NAME, null, InputOption::VALUE_REQUIRED, | |
'directory path of composer.json and composer.lock') | |
->setHelp('Check for missing composer packages.'); | |
} | |
/** | |
* @param InputInterface $input | |
* @param OutputInterface $output | |
*/ | |
protected function execute(InputInterface $input, OutputInterface $output) | |
{ | |
$composerDir = $input->getOption(self::OPTION_COMPOSER_DIR_NAME); | |
$composerDir = preg_replace('/\/+$/', '', $composerDir); | |
list($packageNamesAndVersions, $unresolvableClassNames) = $this->getDistinctPackageNamesAndVersions($composerDir); | |
list($packagesWithVersionConstraint, $packagesWithVersion, $devPackagesWithVersionConstraint, $devPackagesWithVersion) = $this->splitPackagesByVersionInfo($packageNamesAndVersions); | |
$this->outputItems($output, $packagesWithVersionConstraint, | |
'# Packages with version constraints (can be applied as is to composer.json)', 'green'); | |
$this->outputItems($output, $packagesWithVersion, | |
'# Packages and installed versions (version constraints need to be defined out of it)', 'yellow'); | |
$this->outputItems($output, $devPackagesWithVersionConstraint, | |
'# Dev packages with version constraints (can be applied as is to composer.json)', 'green'); | |
$this->outputItems($output, $devPackagesWithVersion, | |
'# Dev packages and installed versions (version constraints need to be defined out of it)', 'yellow'); | |
$this->outputItems($output, $unresolvableClassNames, | |
'# Classes that couldn\'t be resolved to a package (needs manual checking)', 'red'); | |
} | |
/** | |
* @param string $composerDir | |
* @return array | |
*/ | |
protected function getDistinctPackageNamesAndVersions(string $composerDir): array | |
{ | |
$composerFileContent = json_decode(file_get_contents(sprintf('%s/composer.json', $composerDir)), true); | |
$composerLockFileContent = json_decode(file_get_contents(sprintf('%s/composer.lock', $composerDir)), true); | |
$packageNamesAndVersions = []; | |
$unresolvableClassNames = []; | |
foreach ($this->getClassesToResolve() as $className) { | |
try { | |
$reflector = new ReflectionClass($className); | |
$packageDir = dirname($reflector->getFileName()); | |
$packageDirLevels = explode('/', $packageDir); | |
do { | |
$packageComposerFilePath = sprintf('%s/composer.json', implode('/', $packageDirLevels)); | |
if (file_exists($packageComposerFilePath)) { | |
$packageComposerFileContent = json_decode(file_get_contents($packageComposerFilePath), true); | |
$packageName = $packageComposerFileContent['name']; | |
if (!array_key_exists($packageName, $packageNamesAndVersions)) { | |
$packageVersionConstraint = $composerFileContent['require'][$packageName] ?? null; | |
$devPackageVersionConstraint = $composerFileContent['require-dev'][$packageName] ?? null; | |
$installedPackages = array_values( | |
array_filter($composerLockFileContent['packages'], | |
static function (array $package) use ($packageName) { | |
return $package['name'] === $packageName; | |
}) | |
); | |
$installedPackageVersion = !empty($installedPackages) ? $installedPackages[0]['version'] : null; | |
$installedDevPackages = array_values( | |
array_filter($composerLockFileContent['packages-dev'], | |
static function (array $package) use ($packageName) { | |
return $package['name'] === $packageName; | |
}) | |
); | |
$installedDevPackageVersion = !empty($installedDevPackages) ? $installedDevPackages[0]['version'] : null; | |
$packageNamesAndVersions[$packageName] = [ | |
self::VERSION_CONSTRAINT => $packageVersionConstraint ?? $devPackageVersionConstraint, | |
self::VERSION_INSTALLED => $installedPackageVersion ?? $installedDevPackageVersion, | |
self::IS_DEV => $devPackageVersionConstraint !== null || $installedDevPackageVersion !== null, | |
]; | |
} | |
break; | |
} | |
array_pop($packageDirLevels); | |
} while (!empty($packageDirLevels)); | |
} catch (Exception | Error $e) { | |
$unresolvableClassNames[] = $className; | |
} | |
} | |
return [ | |
$packageNamesAndVersions, | |
$unresolvableClassNames, | |
]; | |
} | |
/** | |
* @param array $packageNamesAndVersions | |
* @return array[] | |
*/ | |
protected function splitPackagesByVersionInfo(array $packageNamesAndVersions): array | |
{ | |
list($packagesHavingConstraints, $packagesMissingConstraintsButHavingVersion) = $this->buildComposerCompliantPackageConstraints($packageNamesAndVersions, | |
false); | |
list($devPackagesHavingConstraints, $devPackagesMissingConstraintsButHavingVersion) = $this->buildComposerCompliantPackageConstraints($packageNamesAndVersions, | |
true); | |
return [ | |
$packagesHavingConstraints, | |
$packagesMissingConstraintsButHavingVersion, | |
$devPackagesHavingConstraints, | |
$devPackagesMissingConstraintsButHavingVersion, | |
]; | |
} | |
/** | |
* @param array $packageNamesAndVersions | |
* @param bool $useDev | |
* @return array | |
*/ | |
protected function buildComposerCompliantPackageConstraints(array $packageNamesAndVersions, bool $useDev): array | |
{ | |
$packagesHavingConstraints = []; | |
$packagesMissingConstraintsButHavingVersion = []; | |
foreach ($packageNamesAndVersions as $packageName => $versionInfo) { | |
if ($versionInfo[self::IS_DEV] !== $useDev) { | |
continue; | |
} | |
if ($versionInfo[self::VERSION_CONSTRAINT] !== null) { | |
$packagesHavingConstraints[] = sprintf('"%s": "%s"', $packageName, | |
$versionInfo[self::VERSION_CONSTRAINT]); | |
} else { | |
$packagesMissingConstraintsButHavingVersion[] = sprintf('"%s": "%s"', $packageName, | |
$versionInfo[self::VERSION_INSTALLED]); | |
} | |
} | |
return [ | |
$packagesHavingConstraints, | |
$packagesMissingConstraintsButHavingVersion, | |
]; | |
} | |
/** | |
* @param OutputInterface $output | |
* @param array $items | |
* @param string $heading | |
* @param string $color | |
*/ | |
protected function outputItems(OutputInterface $output, array $items, string $heading, string $color): void | |
{ | |
if (empty($items)) { | |
return; | |
} | |
$output->writeln(sprintf('%s<fg=white>%s</>', PHP_EOL, $heading)); | |
foreach ($items as $item) { | |
$output->writeln(sprintf('<fg=%s>%s</>', $color, $item)); | |
} | |
} | |
/** | |
* @return string[] | |
*/ | |
protected function getClassesToResolve(): array | |
{ | |
/** | |
* @TODO: read from require checker output as soon as it is machine-readable | |
* https://github.com/maglnet/ComposerRequireChecker/issues/294 | |
*/ | |
return [ | |
'Codeception\Util\HttpCode', | |
'CodeItNow\BarcodeBundle\Utils\BarcodeGenerator', | |
'Composer\Script\Event', | |
'DOMDocument', | |
'DOMElement', | |
'Elastica\Query', | |
'Elastica\Query\AbstractQuery', | |
'Elastica\Query\BoolQuery', | |
'Elastica\Query\FunctionScore', | |
'Elastica\Query\Match', | |
'Elastica\Query\MatchAll', | |
'Elastica\Query\MultiMatch', | |
'Elastica\Query\Term', | |
'Elastica\ResultSet', | |
'Elastica\Script\Script', | |
'Elastica\Suggest', | |
'Everon\Component\Collection\Collection', | |
'GuzzleHttp\Client', | |
'GuzzleHttp\ClientInterface', | |
'GuzzleHttp\Exception\RequestException', | |
'GuzzleHttp\HandlerStack', | |
'GuzzleHttp\Promise\rejection_for', | |
'GuzzleHttp\RequestOptions', | |
]; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment