*/ class MergePlugin implements PluginInterface, EventSubscriberInterface { /** * Offical package name */ const PACKAGE_NAME = 'wikimedia/composer-merge-plugin'; /** * Name of the composer 1.1 init event. */ const COMPAT_PLUGINEVENTS_INIT = 'init'; /** * Priority that plugin uses to register callbacks. */ const CALLBACK_PRIORITY = 50000; /** * @var Composer $composer */ protected $composer; /** * @var PluginState $state */ protected $state; /** * @var Logger $logger */ protected $logger; /** * Files that have already been fully processed * * @var string[] $loaded */ protected $loaded = array(); /** * Files that have already been partially processed * * @var string[] $loadedNoDev */ protected $loadedNoDev = array(); /** * {@inheritdoc} */ public function activate(Composer $composer, IOInterface $io) { $this->composer = $composer; $this->state = new PluginState($this->composer); $this->logger = new Logger('merge-plugin', $io); } /** * {@inheritdoc} */ public static function getSubscribedEvents() { return array( // Use our own constant to make this event optional. Once // composer-1.1 is required, this can use PluginEvents::INIT // instead. self::COMPAT_PLUGINEVENTS_INIT => array('onInit', self::CALLBACK_PRIORITY), InstallerEvents::PRE_DEPENDENCIES_SOLVING => array('onDependencySolve', self::CALLBACK_PRIORITY), PackageEvents::POST_PACKAGE_INSTALL => array('onPostPackageInstall', self::CALLBACK_PRIORITY), ScriptEvents::POST_INSTALL_CMD => array('onPostInstallOrUpdate', self::CALLBACK_PRIORITY), ScriptEvents::POST_UPDATE_CMD => array('onPostInstallOrUpdate', self::CALLBACK_PRIORITY), ScriptEvents::PRE_AUTOLOAD_DUMP => array('onInstallUpdateOrDump', self::CALLBACK_PRIORITY), ScriptEvents::PRE_INSTALL_CMD => array('onInstallUpdateOrDump', self::CALLBACK_PRIORITY), ScriptEvents::PRE_UPDATE_CMD => array('onInstallUpdateOrDump', self::CALLBACK_PRIORITY), ); } /** * Handle an event callback for initialization. * * @param \Composer\EventDispatcher\Event $event */ public function onInit(BaseEvent $event) { $this->state->loadSettings(); // It is not possible to know if the user specified --dev or --no-dev // so assume it is false. The dev section will be merged later when // the other events fire. $this->state->setDevMode(false); $this->mergeFiles($this->state->getIncludes(), false); $this->mergeFiles($this->state->getRequires(), true); } /** * Handle an event callback for an install, update or dump command by * checking for "merge-plugin" in the "extra" data and merging package * contents if found. * * @param ScriptEvent $event */ public function onInstallUpdateOrDump(ScriptEvent $event) { $this->state->loadSettings(); $this->state->setDevMode($event->isDevMode()); $this->mergeFiles($this->state->getIncludes(), false); $this->mergeFiles($this->state->getRequires(), true); if ($event->getName() === ScriptEvents::PRE_AUTOLOAD_DUMP) { $this->state->setDumpAutoloader(true); $flags = $event->getFlags(); if (isset($flags['optimize'])) { $this->state->setOptimizeAutoloader($flags['optimize']); } } } /** * Find configuration files matching the configured glob patterns and * 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? * @throws MissingFileException when required and a pattern returns no * results */ protected function mergeFiles(array $patterns, $required = false) { $root = $this->composer->getPackage(); $files = array_map( function ($files, $pattern) use ($required) { if ($required && !$files) { throw new MissingFileException( "merge-plugin: No files matched required '{$pattern}'" ); } return $files; }, array_map('glob', $patterns), $patterns ); foreach (array_reduce($files, 'array_merge', array()) as $path) { $this->mergeFile($root, $path); } } /** * Read a JSON file and merge its contents * * @param RootPackageInterface $root * @param string $path */ protected function mergeFile(RootPackageInterface $root, $path) { if (isset($this->loaded[$path]) || (isset($this->loadedNoDev[$path]) && !$this->state->isDevMode()) ) { $this->logger->debug( "Already merged $path completely" ); return; } $package = new ExtraPackage($path, $this->composer, $this->logger); if (isset($this->loadedNoDev[$path])) { $this->logger->info( "Loading -dev sections of {$path}..." ); $package->mergeDevInto($root, $this->state); } else { $this->logger->info("Loading {$path}..."); $package->mergeInto($root, $this->state); } if ($this->state->isDevMode()) { $this->loaded[$path] = true; } else { $this->loadedNoDev[$path] = true; } if ($this->state->recurseIncludes()) { $this->mergeFiles($package->getIncludes(), false); $this->mergeFiles($package->getRequires(), true); } } /** * 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) { $request = $event->getRequest(); foreach ($this->state->getDuplicateLinks('require') as $link) { $this->logger->info( "Adding dependency {$link}" ); $request->install($link->getTarget(), $link->getConstraint()); } // Issue #113: Check devMode of event rather than our global state. // Composer fires the PRE_DEPENDENCIES_SOLVING event twice for // `--no-dev` operations to decide which packages are dev only // requirements. if ($this->state->shouldMergeDev() && $event->isDevMode()) { foreach ($this->state->getDuplicateLinks('require-dev') as $link) { $this->logger->info( "Adding dev dependency {$link}" ); $request->install($link->getTarget(), $link->getConstraint()); } } } /** * Handle an event callback following installation of a new package by * checking to see if the package that was installed was our plugin. * * @param PackageEvent $event */ public function onPostPackageInstall(PackageEvent $event) { $op = $event->getOperation(); if ($op instanceof InstallOperation) { $package = $op->getPackage()->getName(); if ($package === self::PACKAGE_NAME) { $this->logger->info('composer-merge-plugin installed'); $this->state->setFirstInstall(true); $this->state->setLocked( $event->getComposer()->getLocker()->isLocked() ); } } } /** * Handle an event callback following an install or update command. If our * plugin was installed during the run then trigger an update command to * process any merge-patterns in the current config. * * @param ScriptEvent $event */ public function onPostInstallOrUpdate(ScriptEvent $event) { // @codeCoverageIgnoreStart if ($this->state->isFirstInstall()) { $this->state->setFirstInstall(false); $this->logger->info( '' . 'Running additional update to apply merge settings' . '' ); $config = $this->composer->getConfig(); $preferSource = $config->get('preferred-install') == 'source'; $preferDist = $config->get('preferred-install') == 'dist'; $installer = Installer::create( $event->getIO(), // Create a new Composer instance to ensure full processing of // the merged files. Factory::create($event->getIO(), null, false) ); $installer->setPreferSource($preferSource); $installer->setPreferDist($preferDist); $installer->setDevMode($event->isDevMode()); $installer->setDumpAutoloader($this->state->shouldDumpAutoloader()); $installer->setOptimizeAutoloader( $this->state->shouldOptimizeAutoloader() ); if ($this->state->forceUpdate()) { // Force update mode so that new packages are processed rather // than just telling the user that composer.json and // composer.lock don't match. $installer->setUpdate(true); } $installer->run(); } // @codeCoverageIgnoreEnd } } // vim:sw=4:ts=4:sts=4:et: