Created
April 21, 2016 05:50
-
-
Save joshuataylor/b48deb6cc76c053164fba6ac6dce31f1 to your computer and use it in GitHub Desktop.
This file contains hidden or 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
From 0b2c3c5507bd7b068cf827cee23bfe14fc4d616f Mon Sep 17 00:00:00 2001 | |
Message-Id: <0b2c3c5507bd7b068cf827cee23bfe14fc4d616f.1461217761.git.joshuataylorx@gmail.com> | |
From: Josh Taylor <[email protected]> | |
Date: Thu, 21 Apr 2016 15:49:15 +1000 | |
Subject: [PATCH] Add ability to merge remote composer configurations via http | |
--- | |
README.md | 5 +- | |
src/Merge/ExtraPackage.php | 134 +++++++++++++-------- | |
src/MergePlugin.php | 56 +++++++-- | |
tests/phpunit/MergePluginTest.php | 20 +++ | |
.../fixtures/testRequireRemote/composer.json | 7 ++ | |
5 files changed, 161 insertions(+), 61 deletions(-) | |
diff --git a/README.md b/README.md | |
index f020d40..3b14fa7 100644 | |
--- a/README.md | |
+++ b/README.md | |
@@ -44,7 +44,8 @@ Usage | |
"extensions/*/composer.json" | |
], | |
"require": [ | |
- "submodule/composer.json" | |
+ "submodule/composer.json", | |
+ "https://example.com/composer.json" | |
], | |
"recurse": true, | |
"replace": false, | |
@@ -71,6 +72,8 @@ Each value is treated as a PHP `glob()` pattern identifying additional | |
composer.json style configuration files to merge into the root package | |
configuration for the current Composer execution. | |
+You can also use a remote HTTP source, such as `http://example.com/composer.json` | |
+ | |
The following sections of the found configuration files will be merged into | |
the Composer root package configuration as though they were directly included | |
in the top-level composer.json file: | |
diff --git a/src/Merge/ExtraPackage.php b/src/Merge/ExtraPackage.php | |
index b32131f..392d1fc 100644 | |
--- a/src/Merge/ExtraPackage.php | |
+++ b/src/Merge/ExtraPackage.php | |
@@ -22,6 +22,8 @@ use Composer\Package\RootAliasPackage; | |
use Composer\Package\RootPackage; | |
use Composer\Package\RootPackageInterface; | |
use Composer\Package\Version\VersionParser; | |
+use Composer\IO\IOInterface; | |
+use Composer\Util\RemoteFilesystem; | |
use UnexpectedValueException; | |
/** | |
@@ -44,6 +46,11 @@ class ExtraPackage | |
protected $logger; | |
/** | |
+ * @var IOInterface $io | |
+ */ | |
+ protected $io; | |
+ | |
+ /** | |
* @var string $path | |
*/ | |
protected $path; | |
@@ -59,15 +66,16 @@ class ExtraPackage | |
protected $package; | |
/** | |
- * @param string $path Path to composer.json file | |
+ * @param string $path Path to composer.json file | |
* @param Composer $composer | |
- * @param Logger $logger | |
+ * @param Logger $logger | |
*/ | |
- public function __construct($path, Composer $composer, Logger $logger) | |
+ public function __construct($path, Composer $composer, Logger $logger, IOInterface $io) | |
{ | |
$this->path = $path; | |
$this->composer = $composer; | |
$this->logger = $logger; | |
+ $this->io = $io; | |
$this->json = $this->readPackageJson($path); | |
$this->package = $this->loadPackage($this->json); | |
} | |
@@ -80,7 +88,7 @@ class ExtraPackage | |
public function getIncludes() | |
{ | |
return isset($this->json['extra']['merge-plugin']['include']) ? | |
- $this->json['extra']['merge-plugin']['include'] : array(); | |
+ $this->json['extra']['merge-plugin']['include'] : []; | |
} | |
/** | |
@@ -91,7 +99,7 @@ class ExtraPackage | |
public function getRequires() | |
{ | |
return isset($this->json['extra']['merge-plugin']['require']) ? | |
- $this->json['extra']['merge-plugin']['require'] : array(); | |
+ $this->json['extra']['merge-plugin']['require'] : []; | |
} | |
/** | |
@@ -107,7 +115,12 @@ class ExtraPackage | |
*/ | |
protected function readPackageJson($path) | |
{ | |
- $file = new JsonFile($path); | |
+ if (substr($path, 0, 4) === '^https?://') { | |
+ $file = new JsonFile($path, new RemoteFilesystem($this->io)); | |
+ } else { | |
+ $file = new JsonFile($path); | |
+ } | |
+ | |
$json = $file->read(); | |
if (!isset($json['name'])) { | |
$json['name'] = 'merge-plugin/' . | |
@@ -116,6 +129,7 @@ class ExtraPackage | |
if (!isset($json['version'])) { | |
$json['version'] = '1.0.0'; | |
} | |
+ | |
return $json; | |
} | |
@@ -134,6 +148,7 @@ class ExtraPackage | |
get_class($package) | |
); | |
} | |
+ | |
// @codeCoverageIgnoreEnd | |
return $package; | |
} | |
@@ -142,7 +157,7 @@ class ExtraPackage | |
* Merge this package into a RootPackageInterface | |
* | |
* @param RootPackageInterface $root | |
- * @param PluginState $state | |
+ * @param PluginState $state | |
*/ | |
public function mergeInto(RootPackageInterface $root, PluginState $state) | |
{ | |
@@ -179,7 +194,7 @@ class ExtraPackage | |
return; | |
} | |
$repoManager = $this->composer->getRepositoryManager(); | |
- $newRepos = array(); | |
+ $newRepos = []; | |
foreach ($this->json['repositories'] as $repoJson) { | |
if (!isset($repoJson['type'])) { | |
@@ -195,18 +210,20 @@ class ExtraPackage | |
} | |
$unwrapped = self::unwrapIfNeeded($root, 'setRepositories'); | |
- $unwrapped->setRepositories(array_merge( | |
- $newRepos, | |
- $root->getRepositories() | |
- )); | |
+ $unwrapped->setRepositories( | |
+ array_merge( | |
+ $newRepos, | |
+ $root->getRepositories() | |
+ ) | |
+ ); | |
} | |
/** | |
* Merge require or require-dev into a RootPackageInterface | |
* | |
- * @param string $type 'require' or 'require-dev' | |
+ * @param string $type 'require' or 'require-dev' | |
* @param RootPackageInterface $root | |
- * @param PluginState $state | |
+ * @param PluginState $state | |
*/ | |
protected function mergeRequires( | |
$type, | |
@@ -230,21 +247,23 @@ class ExtraPackage | |
$root | |
); | |
- $root->{$setter}($this->mergeOrDefer( | |
- $type, | |
- $root->{$getter}(), | |
- $requires, | |
- $state | |
- )); | |
+ $root->{$setter}( | |
+ $this->mergeOrDefer( | |
+ $type, | |
+ $root->{$getter}(), | |
+ $requires, | |
+ $state | |
+ ) | |
+ ); | |
} | |
/** | |
* Merge two collections of package links and collect duplicates for | |
* subsequent processing. | |
* | |
- * @param string $type 'require' or 'require-dev' | |
- * @param array $origin Primary collection | |
- * @param array $merge Additional collection | |
+ * @param string $type 'require' or 'require-dev' | |
+ * @param array $origin Primary collection | |
+ * @param array $merge Additional collection | |
* @param PluginState $state | |
* @return array Merged collection | |
*/ | |
@@ -254,7 +273,7 @@ class ExtraPackage | |
array $merge, | |
$state | |
) { | |
- $dups = array(); | |
+ $dups = []; | |
foreach ($merge as $name => $link) { | |
if (!isset($origin[$name]) || $state->replaceDuplicateLinks()) { | |
$this->logger->info("Merging <comment>{$name}</comment>"); | |
@@ -268,13 +287,14 @@ class ExtraPackage | |
} | |
} | |
$state->addDuplicateLinks($type, $dups); | |
+ | |
return $origin; | |
} | |
/** | |
* Merge autoload or autoload-dev into a RootPackageInterface | |
* | |
- * @param string $type 'autoload' or 'devAutoload' | |
+ * @param string $type 'autoload' or 'devAutoload' | |
* @param RootPackageInterface $root | |
*/ | |
protected function mergeAutoload($type, RootPackageInterface $root) | |
@@ -288,10 +308,12 @@ class ExtraPackage | |
} | |
$unwrapped = self::unwrapIfNeeded($root, $setter); | |
- $unwrapped->{$setter}(array_merge_recursive( | |
- $root->{$getter}(), | |
- $this->fixRelativePaths($autoload) | |
- )); | |
+ $unwrapped->{$setter}( | |
+ array_merge_recursive( | |
+ $root->{$getter}(), | |
+ $this->fixRelativePaths($autoload) | |
+ ) | |
+ ); | |
} | |
/** | |
@@ -312,6 +334,7 @@ class ExtraPackage | |
$path = "{$base}{$path}"; | |
} | |
); | |
+ | |
return $paths; | |
} | |
@@ -320,7 +343,7 @@ class ExtraPackage | |
* requires and merge them into a RootPackageInterface | |
* | |
* @param RootPackageInterface $root | |
- * @param array $requires | |
+ * @param array $requires | |
*/ | |
protected function mergeStabilityFlags( | |
RootPackageInterface $root, | |
@@ -330,16 +353,18 @@ class ExtraPackage | |
$sf = new StabilityFlags($flags, $root->getMinimumStability()); | |
$unwrapped = self::unwrapIfNeeded($root, 'setStabilityFlags'); | |
- $unwrapped->setStabilityFlags(array_merge( | |
- $flags, | |
- $sf->extractAll($requires) | |
- )); | |
+ $unwrapped->setStabilityFlags( | |
+ array_merge( | |
+ $flags, | |
+ $sf->extractAll($requires) | |
+ ) | |
+ ); | |
} | |
/** | |
* Merge package links of the given type into a RootPackageInterface | |
* | |
- * @param string $type 'conflict', 'replace' or 'provide' | |
+ * @param string $type 'conflict', 'replace' or 'provide' | |
* @param RootPackageInterface $root | |
*/ | |
protected function mergePackageLinks($type, RootPackageInterface $root) | |
@@ -359,10 +384,12 @@ class ExtraPackage | |
); | |
} | |
// @codeCoverageIgnoreEnd | |
- $unwrapped->{$setter}(array_merge( | |
- $root->{$getter}(), | |
- $this->replaceSelfVersionDependencies($type, $links, $root) | |
- )); | |
+ $unwrapped->{$setter}( | |
+ array_merge( | |
+ $root->{$getter}(), | |
+ $this->replaceSelfVersionDependencies($type, $links, $root) | |
+ ) | |
+ ); | |
} | |
} | |
@@ -376,10 +403,12 @@ class ExtraPackage | |
$suggests = $this->package->getSuggests(); | |
if (!empty($suggests)) { | |
$unwrapped = self::unwrapIfNeeded($root, 'setSuggests'); | |
- $unwrapped->setSuggests(array_merge( | |
- $root->getSuggests(), | |
- $suggests | |
- )); | |
+ $unwrapped->setSuggests( | |
+ array_merge( | |
+ $root->getSuggests(), | |
+ $suggests | |
+ ) | |
+ ); | |
} | |
} | |
@@ -387,7 +416,7 @@ class ExtraPackage | |
* Merge extra config into a RootPackageInterface | |
* | |
* @param RootPackageInterface $root | |
- * @param PluginState $state | |
+ * @param PluginState $state | |
*/ | |
public function mergeExtra(RootPackageInterface $root, PluginState $state) | |
{ | |
@@ -407,11 +436,11 @@ class ExtraPackage | |
} else { | |
foreach (array_intersect( | |
- array_keys($extra), | |
- array_keys($rootExtra) | |
- ) as $key) { | |
+ array_keys($extra), | |
+ array_keys($rootExtra) | |
+ ) as $key) { | |
$this->logger->info( | |
- "Ignoring duplicate <comment>{$key}</comment> in ". | |
+ "Ignoring duplicate <comment>{$key}</comment> in " . | |
"<comment>{$this->path}</comment> extra config." | |
); | |
} | |
@@ -425,8 +454,8 @@ class ExtraPackage | |
* Update Links with a 'self.version' constraint with the root package's | |
* version. | |
* | |
- * @param string $type Link type | |
- * @param array $links | |
+ * @param string $type Link type | |
+ * @param array $links | |
* @param RootPackageInterface $root | |
* @return array | |
*/ | |
@@ -449,6 +478,7 @@ class ExtraPackage | |
if (isset($packages[$link->getSource()])) { | |
/** @var Link $package */ | |
$package = $packages[$link->getSource()]; | |
+ | |
return new Link( | |
$link->getSource(), | |
$link->getTarget(), | |
@@ -466,6 +496,7 @@ class ExtraPackage | |
$prettyVersion | |
); | |
} | |
+ | |
return $link; | |
}, | |
$links | |
@@ -486,7 +517,7 @@ class ExtraPackage | |
* older versions of Composer. | |
* | |
* @param RootPackageInterface $root | |
- * @param string $method Method needed | |
+ * @param string $method Method needed | |
* @return RootPackageInterface|RootPackage | |
*/ | |
public static function unwrapIfNeeded( | |
@@ -500,6 +531,7 @@ class ExtraPackage | |
// Unwrap and return the aliased RootPackage. | |
$root = $root->getAliasOf(); | |
} | |
+ | |
// @codeCoverageIgnoreEnd | |
return $root; | |
} | |
diff --git a/src/MergePlugin.php b/src/MergePlugin.php | |
index ea41d41..f75d5b6 100644 | |
--- a/src/MergePlugin.php | |
+++ b/src/MergePlugin.php | |
@@ -102,11 +102,16 @@ class MergePlugin implements PluginInterface, EventSubscriberInterface | |
protected $logger; | |
/** | |
+ * @var IOInterface $io | |
+ */ | |
+ protected $io; | |
+ | |
+ /** | |
* Files that have already been processed | |
* | |
* @var string[] $loadedFiles | |
*/ | |
- protected $loadedFiles = array(); | |
+ protected $loadedFiles = []; | |
/** | |
* {@inheritdoc} | |
@@ -115,6 +120,7 @@ class MergePlugin implements PluginInterface, EventSubscriberInterface | |
{ | |
$this->composer = $composer; | |
$this->state = new PluginState($this->composer); | |
+ $this->io = $io; | |
$this->logger = new Logger('merge-plugin', $io); | |
} | |
@@ -123,7 +129,7 @@ class MergePlugin implements PluginInterface, EventSubscriberInterface | |
*/ | |
public static function getSubscribedEvents() | |
{ | |
- return array( | |
+ return [ | |
InstallerEvents::PRE_DEPENDENCIES_SOLVING => 'onDependencySolve', | |
PackageEvents::POST_PACKAGE_INSTALL => 'onPostPackageInstall', | |
ScriptEvents::POST_INSTALL_CMD => 'onPostInstallOrUpdate', | |
@@ -131,7 +137,7 @@ class MergePlugin implements PluginInterface, EventSubscriberInterface | |
ScriptEvents::PRE_AUTOLOAD_DUMP => 'onInstallUpdateOrDump', | |
ScriptEvents::PRE_INSTALL_CMD => 'onInstallUpdateOrDump', | |
ScriptEvents::PRE_UPDATE_CMD => 'onInstallUpdateOrDump', | |
- ); | |
+ ]; | |
} | |
/** | |
@@ -162,9 +168,9 @@ class MergePlugin implements PluginInterface, EventSubscriberInterface | |
* merge their contents with the master package. | |
* | |
* @param array $patterns List of files/glob patterns | |
- * @param bool $required Are the patterns required to match files? | |
+ * @param bool $required Are the patterns required to match files? | |
* @throws MissingFileException when required and a pattern returns no | |
- * results | |
+ * results | |
*/ | |
protected function mergeFiles(array $patterns, $required = false) | |
{ | |
@@ -177,34 +183,66 @@ class MergePlugin implements PluginInterface, EventSubscriberInterface | |
"merge-plugin: No files matched required '{$pattern}'" | |
); | |
} | |
+ | |
return $files; | |
}, | |
- array_map('glob', $patterns), | |
+ array_map([$this, 'validatePath'], $patterns), | |
$patterns | |
); | |
- foreach (array_reduce($files, 'array_merge', array()) as $path) { | |
+ foreach (array_reduce($files, 'array_merge', []) as $path) { | |
$this->mergeFile($root, $path); | |
} | |
} | |
/** | |
+ * Validates a URL is correct, using a HEAD request. | |
+ * If a 200/301/302/307/308 response code is returned, it is valid. | |
+ * | |
+ * @param $path | |
+ * @return array | |
+ */ | |
+ protected function validatePath($path) | |
+ { | |
+ if (substr($path, 0, 4) === '^https?://') { | |
+ stream_context_set_default( | |
+ [ | |
+ 'http' => [ | |
+ 'method' => 'HEAD', | |
+ ], | |
+ ] | |
+ ); | |
+ | |
+ $headers = get_headers($path); | |
+ $valid = []; | |
+ if (preg_match('#^HTTP/.*\s+[(200|301|302|307|308)]+\s#i', $headers[0])) { | |
+ $valid[] = $path; | |
+ } | |
+ | |
+ return $valid; | |
+ } else { | |
+ return glob($path); | |
+ } | |
+ } | |
+ | |
+ /** | |
* Read a JSON file and merge its contents | |
* | |
* @param RootPackageInterface $root | |
- * @param string $path | |
+ * @param string $path | |
*/ | |
protected function mergeFile(RootPackageInterface $root, $path) | |
{ | |
if (isset($this->loadedFiles[$path])) { | |
$this->logger->debug("Already merged <comment>$path</comment>"); | |
+ | |
return; | |
} else { | |
$this->loadedFiles[$path] = true; | |
} | |
$this->logger->info("Loading <comment>{$path}</comment>..."); | |
- $package = new ExtraPackage($path, $this->composer, $this->logger); | |
+ $package = new ExtraPackage($path, $this->composer, $this->logger, $this->io); | |
$package->mergeInto($root, $this->state); | |
if ($this->state->recurseIncludes()) { | |
diff --git a/tests/phpunit/MergePluginTest.php b/tests/phpunit/MergePluginTest.php | |
index e2df154..18cc358 100644 | |
--- a/tests/phpunit/MergePluginTest.php | |
+++ b/tests/phpunit/MergePluginTest.php | |
@@ -994,6 +994,26 @@ class MergePluginTest extends \PHPUnit_Framework_TestCase | |
$this->assertEquals(0, count($extraInstalls)); | |
} | |
+ public function testRequireRemote() | |
+ { | |
+ $that = $this; | |
+ $dir = $this->fixtureDir(__FUNCTION__); | |
+ | |
+ $root = $this->rootFromJson("{$dir}/composer.json"); | |
+ | |
+ $root->setRequires(Argument::type('array'))->will( | |
+ function ($args) use ($that) { | |
+ $requires = $args[0]; | |
+ $that->assertEquals(1, count($requires)); | |
+ $that->assertArrayHasKey('monolog/monolog', $requires); | |
+ } | |
+ ); | |
+ | |
+ $extraInstalls = $this->triggerPlugin($root->reveal(), $dir); | |
+ | |
+ $this->assertEquals(0, count($extraInstalls)); | |
+ } | |
+ | |
/** | |
* @param RootPackage $package | |
diff --git a/tests/phpunit/fixtures/testRequireRemote/composer.json b/tests/phpunit/fixtures/testRequireRemote/composer.json | |
new file mode 100644 | |
index 0000000..07e4a59 | |
--- /dev/null | |
+++ b/tests/phpunit/fixtures/testRequireRemote/composer.json | |
@@ -0,0 +1,7 @@ | |
+{ | |
+ "extra": { | |
+ "merge-plugin": { | |
+ "require": "https://raw.githubusercontent.com/wikimedia/composer-merge-plugin/master/example/composer.local.json" | |
+ } | |
+ } | |
+} | |
\ No newline at end of file | |
-- | |
2.8.1 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment