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: