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