Skip to content
This repository was archived by the owner on Mar 7, 2023. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,18 @@ you'd like to install with your library:
"dependency": [
"package/to-require",
...
]
],
"dependency-or": {
"Question": [
"package/to-choose",
"package/or-this",
...
]
}
},
"require": {
"php": "^5.6 || ^7.0",
"webimpress/composer-extra-dependency": "^0.1 || ^1.0",
"webimpress/composer-extra-dependency": "^0.3 || ^1.0",
...
}
...
Expand Down
184 changes: 157 additions & 27 deletions src/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
use Composer\Repository\CompositeRepository;
use Composer\Repository\PlatformRepository;
use Composer\Repository\RepositoryFactory;
use Composer\Script\Event;
use InvalidArgumentException;
use RuntimeException;

class Plugin implements PluginInterface, EventSubscriberInterface
{
Expand Down Expand Up @@ -48,11 +51,16 @@ class Plugin implements PluginInterface, EventSubscriberInterface
/** @var CompositeRepository */
private $repos;

/** @var string[] */
private $packagesToInstall = [];

public static function getSubscribedEvents()
{
return [
'post-package-install' => 'onPostPackage',
'post-package-update' => 'onPostPackage',
'post-install-cmd' => 'onPostCommand',
'post-update-cmd' => 'onPostCommand',
];
}

Expand All @@ -63,7 +71,27 @@ public function activate(Composer $composer, IOInterface $io)

$installedPackages = $this->composer->getRepositoryManager()->getLocalRepository()->getPackages();
foreach ($installedPackages as $package) {
$this->installedPackages[$package->getName()] = $package->getPrettyVersion();
$this->installedPackages[strtolower($package->getName())] = $package->getPrettyVersion();
}
}

public function onPostCommand(Event $event)
{
if (! $event->isDevMode()) {
// Do nothing in production mode.
return;
}

if (! $this->io->isInteractive()) {
// Do nothing in no-interactive mode
return;
}

if ($this->packagesToInstall) {
$this->updateComposerJson($this->packagesToInstall);

$rootPackage = $this->updateRootPackage($this->composer->getPackage(), $this->packagesToInstall);
$this->runInstaller($rootPackage, array_keys($this->packagesToInstall));
}
}

Expand All @@ -85,36 +113,89 @@ public function onPostPackage(PackageEvent $event)
} else {
$package = $operation->getTargetPackage();
}
$extra = $this->getExtraMetadata($package->getExtra());
if (empty($extra)) {
// Package does not define anything of interest; do nothing.
return;

$extra = $package->getExtra();

$this->packagesToInstall += $this->andDependencies($extra);
$this->packagesToInstall += $this->orDependencies($extra);
}

private function andDependencies(array $extra)
{
$deps = isset($extra['dependency']) && is_array($extra['dependency'])
? $extra['dependency']
: [];

if (! $deps) {
// No defined any packages to install
return [];
}

$packages = array_flip($extra);
$packages = array_flip($deps);

foreach ($packages as $package => &$constraint) {
if ($this->isPackageReadyToInstall($package)) {
unset($packages[$package]);
continue;
}

if ($this->hasPackage($package)) {
unset($packages[$package]);
continue;
}

// Check if package is currently installed and use installed version.
if ($constraint = $this->getInstalledPackageConstraint($package)) {
continue;
}

// Package is not installed, then prompt user for the version.
$constraint = $this->promptForPackageVersion($package);
}

if ($packages) {
$this->updateComposerJson($packages);

$rootPackage = $this->updateRootPackage($this->composer->getPackage(), $packages);
$this->runInstaller($rootPackage, array_keys($packages));
}
return $packages;
}

private function getExtraMetadata(array $extra)
private function orDependencies(array $extra)
{
return isset($extra['dependency']) && is_array($extra['dependency'])
? $extra['dependency']
$deps = isset($extra['dependency-or']) && is_array($extra['dependency-or'])
? $extra['dependency-or']
: [];

if (! $deps) {
// No any dependencies to choose defined in the package.
return [];
}

$packages = [];
foreach ($deps as $question => $options) {
if (! is_array($options) || count($options) < 2) {
throw new RuntimeException('You must provide at least two optional dependencies.');
}

foreach ($options as $package) {
if ($this->isPackageReadyToInstall($package)) {
// Package has been already prepared to be installed, skipping.
continue 2;
}

if ($this->hasPackage($package)) {
// Package from this group has been found in root composer, skipping.
continue 2;
}

// Check if package is currently installed, if so, use installed constraint and skip question.
if ($constraint = $this->getInstalledPackageConstraint($package)) {
$packages[$package] = $constraint;
continue 2;
}
}

$package = $this->promptForPackageSelection($question, $options);
$packages[$package] = $this->promptForPackageVersion($package);
}

return $packages;
}

private function updateRootPackage(RootPackageInterface $rootPackage, array $packages)
Expand Down Expand Up @@ -157,21 +238,55 @@ private function runInstaller(RootPackageInterface $rootPackage, array $packages
return $installer->run();
}

private function promptForPackageVersion($name)
private function getInstalledPackageConstraint($package)
{
$lower = strtolower($package);

// Package is currently installed. Add it to root composer.json
if (isset($this->installedPackages[$name])) {
$this->io->write(sprintf(
'Added package <info>%s</info> to composer.json with constraint <info>%s</info>;'
. ' to upgrade, run <info>composer require %s:VERSION</info>',
$name,
'^' . $this->installedPackages[$name],
$name
));
if (! isset($this->installedPackages[$lower])) {
return null;
}

$constraint = '^' . $this->installedPackages[$lower];
$this->io->write(sprintf(
'Added package <info>%s</info> to composer.json with constraint <info>%s</info>;'
. ' to upgrade, run <info>composer require %s:VERSION</info>',
$package,
$constraint,
$package
));

return $constraint;
}

return '^' . $this->installedPackages[$name];
private function promptForPackageSelection($question, array $packages)
{
$ask = [sprintf('<question>%s</question>' . "\n", $question)];
foreach ($packages as $i => $name) {
$ask[] = sprintf(' [<comment>%d</comment>] %s' . "\n", $i + 1, $name);
}
$ask[] = ' Make your selection: ';

do {
$package = $this->io->askAndValidate(
$ask,
function ($input) use ($packages) {
$input = is_numeric($input) ? (int) trim($input) : 0;

if (isset($packages[$input - 1])) {
return $packages[$input - 1];
}

return null;
}
);
} while (! $package);

return $package;
}

private function promptForPackageVersion($name)
{
$constraint = $this->io->askAndValidate(
sprintf(
'Enter the version of <info>%s</info> to require (or leave blank to use the latest version): ',
Expand Down Expand Up @@ -214,10 +329,25 @@ private function createInstaller(Composer $composer, IOInterface $io, RootPackag

private function hasPackage($package)
{
$lower = strtolower($package);

$rootPackage = $this->composer->getPackage();
$requires = $rootPackage->getRequires() + $rootPackage->getDevRequires();
foreach ($requires as $name => $link) {
if (strtolower($name) === strtolower($package)) {
if (strtolower($name) === $lower) {
return true;
}
}

return false;
}

private function isPackageReadyToInstall($package)
{
$lower = strtolower($package);

foreach ($this->packagesToInstall as $name => $version) {
if (strtolower($name) === $lower) {
return true;
}
}
Expand Down Expand Up @@ -282,7 +412,7 @@ private function findBestVersionForPackage($name)
$package = $versionSelector->findBestCandidate($name, null, null, 'stable');

if (! $package) {
throw new \InvalidArgumentException(sprintf(
throw new InvalidArgumentException(sprintf(
'Could not find package %s at any version for your minimum-stability (%s).'
. ' Check the package spelling or your minimum-stability',
$name,
Expand Down
Loading