*/ class MergePlugin implements PluginInterface, EventSubscriberInterface { /** * @var Composer $composer */ protected $composer; /** * @var IOInterface $inputOutput */ protected $inputOutput; /** * @var ArrayLoader $loader */ protected $loader; /** * @var array $duplicateLinks */ protected $duplicateLinks; /** * @var bool $devMode */ protected $devMode; /** * Whether to recursively include dependencies * * @var bool $recurse */ protected $recurse = true; /** * Files that have already been processed * * @var string[] $loadedFiles */ protected $loadedFiles = array(); /** * {@inheritdoc} */ public function activate(Composer $composer, IOInterface $io) { $this->composer = $composer; $this->inputOutput = $io; } /** * {@inheritdoc} */ public static function getSubscribedEvents() { return array( InstallerEvents::PRE_DEPENDENCIES_SOLVING => 'onDependencySolve', ScriptEvents::PRE_INSTALL_CMD => 'onInstallOrUpdate', ScriptEvents::PRE_UPDATE_CMD => 'onInstallOrUpdate', ); } /** * Handle an event callback for an install or update command by checking * for "merge-patterns" in the "extra" data and merging package contents * if found. * * @param CommandEvent $event */ public function onInstallOrUpdate(CommandEvent $event) { $config = $this->readConfig($this->composer->getPackage()); if (isset($config['recurse'])) { $this->recurse = (bool)$config['recurse']; } if ($config['include']) { $this->loader = new ArrayLoader(); $this->duplicateLinks = array( 'require' => array(), 'require-dev' => array(), ); $this->devMode = $event->isDevMode(); $this->mergePackages($config); } } /** * @param RootPackageInterface $package * @return array */ protected function readConfig(RootPackageInterface $package) { $config = array( 'include' => array(), ); $extra = $package->getExtra(); if (isset($extra['merge-plugin'])) { $config = array_merge($config, $extra['merge-plugin']); if (!is_array($config['include'])) { $config['include'] = array($config['include']); } } return $config; } /** * Find configuration files matching the configured glob patterns and * merge their contents with the master package. * * @param array $config */ protected function mergePackages(array $config) { $root = $this->composer->getPackage(); foreach (array_reduce( array_map('glob', $config['include']), 'array_merge', array() ) as $path) { $this->loadFile($root, $path); } } /** * Read a JSON file and merge its contents * * @param RootPackageInterface $root * @param string $path */ protected function loadFile($root, $path) { if (in_array($path, $this->loadedFiles)) { $this->debug("Skipping duplicate $path..."); return; } else { $this->loadedFiles[] = $path; } $this->debug("Loading {$path}..."); $json = $this->readPackageJson($path); $package = $this->loader->load($json); $this->mergeRequires($root, $package); $this->mergeDevRequires($root, $package); if (isset($json['repositories'])) { $this->addRepositories($json['repositories'], $root); } if ($package->getSuggests()) { $root->setSuggests(array_merge( $root->getSuggests(), $package->getSuggests() )); } if ($this->recurse && isset($json['extra']['merge-plugin'])) { $this->mergePackages($json['extra']['merge-plugin']); } } /** * 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 RootPackageInterface $root * @param CompletePackage $package */ protected function mergeRequires( RootPackageInterface $root, CompletePackage $package ) { $requires = $package->getRequires(); if (!$requires) { return; } $this->mergeStabilityFlags($root, $requires); $root->setRequires($this->mergeLinks( $root->getRequires(), $requires, $this->duplicateLinks['require'] )); } /** * @param RootPackageInterface $root * @param CompletePackage $package */ protected function mergeDevRequires( RootPackageInterface $root, CompletePackage $package ) { $requires = $package->getDevRequires(); if (!$requires) { return; } $this->mergeStabilityFlags($root, $requires); $root->setDevRequires($this->mergeLinks( $root->getDevRequires(), $requires, $this->duplicateLinks['require-dev'] )); } /** * Extract and merge stability flags from the given collection of * requires. * * @param RootPackageInterface $root * @param array $requires */ protected function mergeStabilityFlags( RootPackageInterface $root, array $requires ) { $flags = $root->getStabilityFlags(); foreach ($requires as $name => $link) { $name = strtolower($name); $version = $link->getPrettyConstraint(); $stability = VersionParser::parseStability($version); $flags[$name] = BasePackage::$stabilities[$stability]; } $root->setStabilityFlags($flags); } /** * Add a collection of repositories described by the given configuration * to the given package and the global repository manager. * * @param array $repositories * @param RootPackageInterface $root */ protected function addRepositories( array $repositories, RootPackageInterface $root ) { $repoManager = $this->composer->getRepositoryManager(); $newRepos = array(); foreach ($repositories as $repoJson) { $this->debug("Adding {$repoJson['type']} repository"); $repo = $repoManager->createRepository( $repoJson['type'], $repoJson ); $repoManager->addRepository($repo); $newRepos[] = $repo; } $root->setRepositories(array_merge( $newRepos, $root->getRepositories() )); } /** * Merge two collections of package links and collect duplicates for * subsequent processing. * * @param array $origin Primary collection * @param array $merge Additional collection * @param array &dups Duplicate storage * @return array Merged collection */ protected function mergeLinks(array $origin, array $merge, array &$dups) { foreach ($merge as $name => $link) { if (!isset($origin[$name])) { $this->debug("Merging {$name}"); $origin[$name] = $link; } else { // Defer to solver. $this->debug("Deferring duplicate {$name}"); $dups[] = $link; } } return $origin; } /** * Handle an event callback for pre-dependency solving phase of an install * or update by adding any duplicate package dependencies found during * initial merge processing to the request that will be processed by the * dependency solver. * * @param InstallerEvent $event */ public function onDependencySolve(InstallerEvent $event) { if (!$this->duplicateLinks) { return; } $request = $event->getRequest(); foreach ($this->duplicateLinks['require'] as $link) { $this->debug("Adding dependency {$link}"); $request->install($link->getTarget(), $link->getConstraint()); } if ($this->devMode) { foreach ($this->duplicateLinks['require-dev'] as $link) { $this->debug("Adding dev dependency {$link}"); $request->install($link->getTarget(), $link->getConstraint()); } } } /** * Log a debug message * * Messages will be output at the "verbose" logging level (eg `-v` needed * on the Composer command). * * @param string $message */ protected function debug($message) { if ($this->inputOutput->isVerbose()) { $this->inputOutput->write(" [merge] {$message}"); } } } // vim:sw=4:ts=4:sts=4:et: