*/
class ExtraPackage
{
/**
* @var Composer $composer
*/
protected $composer;
/**
* @var Logger $logger
*/
protected $logger;
/**
* @var string $path
*/
protected $path;
/**
* @var array $json
*/
protected $json;
/**
* @var CompletePackage $package
*/
protected $package;
/**
* @var VersionParser $versionParser
*/
protected $versionParser;
/**
* @param string $path Path to composer.json file
* @param Composer $composer
* @param Logger $logger
*/
public function __construct($path, Composer $composer, Logger $logger)
{
$this->path = $path;
$this->composer = $composer;
$this->logger = $logger;
$this->json = $this->readPackageJson($path);
$this->package = $this->loadPackage($this->json);
$this->versionParser = new VersionParser();
}
/**
* Get list of additional packages to include if precessing recursively.
*
* @return array
*/
public function getIncludes()
{
return isset($this->json['extra']['merge-plugin']['include']) ?
$this->json['extra']['merge-plugin']['include'] : array();
}
/**
* Get list of additional packages to require if precessing recursively.
*
* @return array
*/
public function getRequires()
{
return isset($this->json['extra']['merge-plugin']['require']) ?
$this->json['extra']['merge-plugin']['require'] : array();
}
/**
* Read the contents of a composer.json style file into an array.
*
* The package contents are fixed up to be usable to create a Package
* object by providing dummy "name" and "version" values if they have not
* been provided in the file. This is consistent with the default root
* package loading behavior of Composer.
*
* @param string $path
* @return array
*/
protected function readPackageJson($path)
{
$file = new JsonFile($path);
$json = $file->read();
if (!isset($json['name'])) {
$json['name'] = 'merge-plugin/' .
strtr($path, DIRECTORY_SEPARATOR, '-');
}
if (!isset($json['version'])) {
$json['version'] = '1.0.0';
}
return $json;
}
/**
* @param array $json
* @return CompletePackage
*/
protected function loadPackage(array $json)
{
$loader = new ArrayLoader();
$package = $loader->load($json);
// @codeCoverageIgnoreStart
if (!$package instanceof CompletePackage) {
throw new UnexpectedValueException(
'Expected instance of CompletePackage, got ' .
get_class($package)
);
}
// @codeCoverageIgnoreEnd
return $package;
}
/**
* Merge this package into a RootPackageInterface
*
* @param RootPackageInterface $root
* @param PluginState $state
*/
public function mergeInto(RootPackageInterface $root, PluginState $state)
{
$this->prependRepositories($root);
$this->mergeRequires('require', $root, $state);
$this->mergePackageLinks('conflict', $root);
$this->mergePackageLinks('replace', $root);
$this->mergePackageLinks('provide', $root);
$this->mergeSuggests($root);
$this->mergeAutoload('autoload', $root);
$this->mergeExtra($root, $state);
$this->mergeScripts($root, $state);
if ($state->isDevMode()) {
$this->mergeDevInto($root, $state);
} else {
$this->mergeReferences($root);
}
}
/**
* Merge just the dev portion into a RootPackageInterface
*
* @param RootPackageInterface $root
* @param PluginState $state
*/
public function mergeDevInto(RootPackageInterface $root, PluginState $state)
{
$this->mergeRequires('require-dev', $root, $state);
$this->mergeAutoload('devAutoload', $root);
$this->mergeReferences($root);
}
/**
* Add a collection of repositories described by the given configuration
* to the given package and the global repository manager.
*
* @param RootPackageInterface $root
*/
protected function prependRepositories(RootPackageInterface $root)
{
if (!isset($this->json['repositories'])) {
return;
}
$repoManager = $this->composer->getRepositoryManager();
$newRepos = array();
foreach ($this->json['repositories'] as $repoJson) {
if (!isset($repoJson['type'])) {
continue;
}
$this->logger->info("Prepending {$repoJson['type']} repository");
$repo = $repoManager->createRepository(
$repoJson['type'],
$repoJson
);
$repoManager->prependRepository($repo);
$newRepos[] = $repo;
}
$unwrapped = self::unwrapIfNeeded($root, 'setRepositories');
$unwrapped->setRepositories(array_merge(
$newRepos,
$root->getRepositories()
));
}
/**
* Merge require or require-dev into a RootPackageInterface
*
* @param string $type 'require' or 'require-dev'
* @param RootPackageInterface $root
* @param PluginState $state
*/
protected function mergeRequires(
$type,
RootPackageInterface $root,
PluginState $state
) {
$linkType = BasePackage::$supportedLinkTypes[$type];
$getter = 'get' . ucfirst($linkType['method']);
$setter = 'set' . ucfirst($linkType['method']);
$requires = $this->package->{$getter}();
if (empty($requires)) {
return;
}
$this->mergeStabilityFlags($root, $requires);
$requires = $this->replaceSelfVersionDependencies(
$type,
$requires,
$root
);
$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 PluginState $state
* @return array Merged collection
*/
protected function mergeOrDefer(
$type,
array $origin,
array $merge,
$state
) {
if ($state->ignoreDuplicateLinks() && $state->replaceDuplicateLinks()) {
$this->logger->warning("Both replace and ignore-duplicates are true. These are mutually exclusive.");
$this->logger->warning("Duplicate packages will be ignored.");
}
$dups = array();
foreach ($merge as $name => $link) {
if (isset($origin[$name]) && $state->ignoreDuplicateLinks()) {
$this->logger->info("Ignoring duplicate {$name}");
continue;
} elseif (!isset($origin[$name]) || $state->replaceDuplicateLinks()) {
$this->logger->info("Merging {$name}");
$origin[$name] = $link;
} else {
// Defer to solver.
$this->logger->info(
"Deferring duplicate {$name}"
);
$dups[] = $link;
}
}
$state->addDuplicateLinks($type, $dups);
return $origin;
}
/**
* Merge autoload or autoload-dev into a RootPackageInterface
*
* @param string $type 'autoload' or 'devAutoload'
* @param RootPackageInterface $root
*/
protected function mergeAutoload($type, RootPackageInterface $root)
{
$getter = 'get' . ucfirst($type);
$setter = 'set' . ucfirst($type);
$autoload = $this->package->{$getter}();
if (empty($autoload)) {
return;
}
$unwrapped = self::unwrapIfNeeded($root, $setter);
$unwrapped->{$setter}(array_merge_recursive(
$root->{$getter}(),
$this->fixRelativePaths($autoload)
));
}
/**
* Fix a collection of paths that are relative to this package to be
* relative to the base package.
*
* @param array $paths
* @return array
*/
protected function fixRelativePaths(array $paths)
{
$base = dirname($this->path);
$base = ($base === '.') ? '' : "{$base}/";
array_walk_recursive(
$paths,
function (&$path) use ($base) {
$path = "{$base}{$path}";
}
);
return $paths;
}
/**
* Extract and merge stability flags from the given collection of
* requires and merge them into a RootPackageInterface
*
* @param RootPackageInterface $root
* @param array $requires
*/
protected function mergeStabilityFlags(
RootPackageInterface $root,
array $requires
) {
$flags = $root->getStabilityFlags();
$sf = new StabilityFlags($flags, $root->getMinimumStability());
$unwrapped = self::unwrapIfNeeded($root, 'setStabilityFlags');
$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 RootPackageInterface $root
*/
protected function mergePackageLinks($type, RootPackageInterface $root)
{
$linkType = BasePackage::$supportedLinkTypes[$type];
$getter = 'get' . ucfirst($linkType['method']);
$setter = 'set' . ucfirst($linkType['method']);
$links = $this->package->{$getter}();
if (!empty($links)) {
$unwrapped = self::unwrapIfNeeded($root, $setter);
// @codeCoverageIgnoreStart
if ($root !== $unwrapped) {
$this->logger->warning(
'This Composer version does not support ' .
"'{$type}' merging for aliased packages."
);
}
// @codeCoverageIgnoreEnd
$unwrapped->{$setter}(array_merge(
$root->{$getter}(),
$this->replaceSelfVersionDependencies($type, $links, $root)
));
}
}
/**
* Merge suggested packages into a RootPackageInterface
*
* @param RootPackageInterface $root
*/
protected function mergeSuggests(RootPackageInterface $root)
{
$suggests = $this->package->getSuggests();
if (!empty($suggests)) {
$unwrapped = self::unwrapIfNeeded($root, 'setSuggests');
$unwrapped->setSuggests(array_merge(
$root->getSuggests(),
$suggests
));
}
}
/**
* Merge extra config into a RootPackageInterface
*
* @param RootPackageInterface $root
* @param PluginState $state
*/
public function mergeExtra(RootPackageInterface $root, PluginState $state)
{
$extra = $this->package->getExtra();
unset($extra['merge-plugin']);
if (!$state->shouldMergeExtra() || empty($extra)) {
return;
}
$rootExtra = $root->getExtra();
$unwrapped = self::unwrapIfNeeded($root, 'setExtra');
if ($state->replaceDuplicateLinks()) {
$unwrapped->setExtra(
self::mergeExtraArray($state->shouldMergeExtraDeep(), $rootExtra, $extra)
);
} else {
if (!$state->shouldMergeExtraDeep()) {
foreach (array_intersect(
array_keys($extra),
array_keys($rootExtra)
) as $key) {
$this->logger->info(
"Ignoring duplicate {$key} in ".
"{$this->path} extra config."
);
}
}
$unwrapped->setExtra(
self::mergeExtraArray($state->shouldMergeExtraDeep(), $extra, $rootExtra)
);
}
}
/**
* Merge scripts config into a RootPackageInterface
*
* @param RootPackageInterface $root
* @param PluginState $state
*/
public function mergeScripts(RootPackageInterface $root, PluginState $state)
{
$scripts = $this->package->getScripts();
if (!$state->shouldMergeScripts() || empty($scripts)) {
return;
}
$rootScripts = $root->getScripts();
$unwrapped = self::unwrapIfNeeded($root, 'setScripts');
if ($state->replaceDuplicateLinks()) {
$unwrapped->setScripts(
array_merge($rootScripts, $scripts)
);
} else {
$unwrapped->setScripts(
array_merge($scripts, $rootScripts)
);
}
}
/**
* Merges two arrays either via arrayMergeDeep or via array_merge.
*
* @param bool $mergeDeep
* @param array $array1
* @param array $array2
* @return array
*/
public static function mergeExtraArray($mergeDeep, $array1, $array2)
{
if ($mergeDeep) {
return NestedArray::mergeDeep($array1, $array2);
}
return array_merge($array1, $array2);
}
/**
* Update Links with a 'self.version' constraint with the root package's
* version.
*
* @param string $type Link type
* @param array $links
* @param RootPackageInterface $root
* @return array
*/
protected function replaceSelfVersionDependencies(
$type,
array $links,
RootPackageInterface $root
) {
$linkType = BasePackage::$supportedLinkTypes[$type];
$version = $root->getVersion();
$prettyVersion = $root->getPrettyVersion();
$vp = $this->versionParser;
$method = 'get' . ucfirst($linkType['method']);
$packages = $root->$method();
return array_map(
function ($link) use ($linkType, $version, $prettyVersion, $vp, $packages) {
if ('self.version' === $link->getPrettyConstraint()) {
if (isset($packages[$link->getSource()])) {
/** @var Link $package */
$package = $packages[$link->getSource()];
return new Link(
$link->getSource(),
$link->getTarget(),
$vp->parseConstraints($package->getConstraint()->getPrettyString()),
$linkType['description'],
$package->getPrettyConstraint()
);
}
return new Link(
$link->getSource(),
$link->getTarget(),
$vp->parseConstraints($version),
$linkType['description'],
$prettyVersion
);
}
return $link;
},
$links
);
}
/**
* Get a full featured Package from a RootPackageInterface.
*
* In Composer versions before 599ad77 the RootPackageInterface only
* defines a sub-set of operations needed by composer-merge-plugin and
* RootAliasPackage only implemented those methods defined by the
* interface. Most of the unimplemented methods in RootAliasPackage can be
* worked around because the getter methods that are implemented proxy to
* the aliased package which we can modify by unwrapping. The exception
* being modifying the 'conflicts', 'provides' and 'replaces' collections.
* We have no way to actually modify those collections unfortunately in
* older versions of Composer.
*
* @param RootPackageInterface $root
* @param string $method Method needed
* @return RootPackageInterface|RootPackage
*/
public static function unwrapIfNeeded(
RootPackageInterface $root,
$method = 'setExtra'
) {
// @codeCoverageIgnoreStart
if ($root instanceof RootAliasPackage &&
!method_exists($root, $method)
) {
// Unwrap and return the aliased RootPackage.
$root = $root->getAliasOf();
}
// @codeCoverageIgnoreEnd
return $root;
}
/**
* Update the root packages reference information.
*
* @param RootPackageInterface $root
*/
protected function mergeReferences(RootPackageInterface $root)
{
// Merge source reference information for merged packages.
// @see RootPackageLoader::load
$references = array();
$unwrapped = $this->unwrapIfNeeded($root, 'setReferences');
foreach (array('require', 'require-dev') as $linkType) {
$linkInfo = BasePackage::$supportedLinkTypes[$linkType];
$method = 'get'.ucfirst($linkInfo['method']);
$links = array();
foreach ($unwrapped->$method() as $link) {
$links[$link->getTarget()] = $link->getConstraint()->getPrettyString();
}
$references = $this->extractReferences($links, $references);
}
$unwrapped->setReferences($references);
}
/**
* Extract vcs revision from version constraint (dev-master#abc123.
*
* @param array $requires
* @param array $references
* @return array
* @see RootPackageLoader::extractReferences()
*/
protected function extractReferences(array $requires, array $references)
{
foreach ($requires as $reqName => $reqVersion) {
$reqVersion = preg_replace('{^([^,\s@]+) as .+$}', '$1', $reqVersion);
$stabilityName = VersionParser::parseStability($reqVersion);
if (
preg_match('{^[^,\s@]+?#([a-f0-9]+)$}', $reqVersion, $match) &&
$stabilityName === 'dev'
) {
$name = strtolower($reqName);
$references[$name] = $match[1];
}
}
return $references;
}
}
// vim:sw=4:ts=4:sts=4:et: