Skip to content

Instantly share code, notes, and snippets.

@KayKursawe
Created August 13, 2021 08:04
Show Gist options
  • Save KayKursawe/524036c6c95fe4214a8759b9fc46704f to your computer and use it in GitHub Desktop.
Save KayKursawe/524036c6c95fe4214a8759b9fc46704f to your computer and use it in GitHub Desktop.
Quick'n'dirty Symfony console command to resolve package names and versions
<?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