Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save joshuataylor/b48deb6cc76c053164fba6ac6dce31f1 to your computer and use it in GitHub Desktop.
Save joshuataylor/b48deb6cc76c053164fba6ac6dce31f1 to your computer and use it in GitHub Desktop.
From 0b2c3c5507bd7b068cf827cee23bfe14fc4d616f Mon Sep 17 00:00:00 2001
Message-Id: <0b2c3c5507bd7b068cf827cee23bfe14fc4d616f.1461217761.git.joshuataylorx@gmail.com>
From: Josh Taylor <joshuataylorx@gmail.com>
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