Completed
Pull Request — master (#117)
by Fabian
20:33 queued 18:08
created

MergePlugin::onDependencySolve()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 4

Importance

Changes 8
Bugs 0 Features 2
Metric Value
c 8
b 0
f 2
dl 0
loc 18
ccs 17
cts 17
cp 1
rs 9.2
cc 4
eloc 11
nc 6
nop 1
crap 4
1
<?php
2
/**
3
 * This file is part of the Composer Merge plugin.
4
 *
5
 * Copyright (C) 2015 Bryan Davis, Wikimedia Foundation, and contributors
6
 *
7
 * This software may be modified and distributed under the terms of the MIT
8
 * license. See the LICENSE file for details.
9
 */
10
11
namespace Wikimedia\Composer;
12
13
use Wikimedia\Composer\Merge\ExtraPackage;
14
use Wikimedia\Composer\Merge\MissingFileException;
15
use Wikimedia\Composer\Merge\PluginState;
16
17
use Composer\Composer;
18
use Composer\DependencyResolver\Operation\InstallOperation;
19
use Composer\EventDispatcher\EventSubscriberInterface;
20
use Composer\Factory;
21
use Composer\Installer;
22
use Composer\Installer\InstallerEvent;
23
use Composer\Installer\InstallerEvents;
24
use Composer\Installer\PackageEvent;
25
use Composer\Installer\PackageEvents;
26
use Composer\IO\IOInterface;
27
use Composer\Package\RootPackageInterface;
28
use Composer\Plugin\PluginInterface;
29
use Composer\Script\Event;
30
use Composer\Script\ScriptEvents;
31
32
/**
33
 * Composer plugin that allows merging multiple composer.json files.
34
 *
35
 * When installed, this plugin will look for a "merge-plugin" key in the
36
 * composer configuration's "extra" section. The value for this key is
37
 * a set of options configuring the plugin.
38
 *
39
 * An "include" setting is required. The value of this setting can be either
40
 * a single value or an array of values. Each value is treated as a glob()
41
 * pattern identifying additional composer.json style configuration files to
42
 * merge into the configuration for the current compser execution.
43
 *
44
 * The "autoload", "autoload-dev", "conflict", "provide", "replace",
45
 * "repositories", "require", "require-dev", and "suggest" sections of the
46
 * found configuration files will be merged into the root package
47
 * configuration as though they were directly included in the top-level
48
 * composer.json file.
49
 *
50
 * If included files specify conflicting package versions for "require" or
51
 * "require-dev", the normal Composer dependency solver process will be used
52
 * to attempt to resolve the conflict. Specifying the 'replace' key as true will
53
 * change this default behaviour so that the last-defined version of a package
54
 * will win, allowing for force-overrides of package defines.
55
 *
56
 * By default the "extra" section is not merged. This can be enabled by
57
 * setitng the 'merge-extra' key to true. In normal mode, when the same key is
58
 * found in both the original and the imported extra section, the version in
59
 * the original config is used and the imported version is skipped. If
60
 * 'replace' mode is active, this behaviour changes so the imported version of
61
 * the key is used, replacing the version in the original config.
62
 *
63
 *
64
 * @code
65
 * {
66
 *     "require": {
67
 *         "wikimedia/composer-merge-plugin": "dev-master"
68
 *     },
69
 *     "extra": {
70
 *         "merge-plugin": {
71
 *             "include": [
72
 *                 "composer.local.json"
73
 *             ]
74
 *         }
75
 *     }
76
 * }
77
 * @endcode
78
 *
79
 * @author Bryan Davis <[email protected]>
80
 */
81
class MergePlugin implements PluginInterface, EventSubscriberInterface
82
{
83
84
    /**
85
     * Offical package name
86
     */
87
    const PACKAGE_NAME = 'wikimedia/composer-merge-plugin';
88
89
    /**
90
     * Name of the composer 1.1 init event.
91
     */
92
    const INIT = 'init';
93
94
    /**
95
     * @var Composer $composer
96
     */
97
    protected $composer;
98
99
    /**
100
     * @var PluginState $state
101
     */
102
    protected $state;
103
104
    /**
105
     * @var Logger $logger
106
     */
107
    protected $logger;
108
109
    /**
110
     * Files that have already been processed
111
     *
112
     * @var string[] $loadedFiles
113
     */
114
    protected $loadedFiles = array();
115
116
    /**
117
     * {@inheritdoc}
118
     */
119 120
    public function activate(Composer $composer, IOInterface $io)
120
    {
121 120
        $this->composer = $composer;
122 120
        $this->state = new PluginState($this->composer);
123 120
        $this->logger = new Logger('merge-plugin', $io);
124 120
    }
125
126
    /**
127
     * {@inheritdoc}
128
     */
129 5
    public static function getSubscribedEvents()
130
    {
131
        return array(
132
            // Use our own constant to make this event optional. Once
133
            // composer-1.1 is required, this can use PluginEvents::INIT
134
            // instead.
135 5
            self::INIT => 'onInit',
136 5
            InstallerEvents::PRE_DEPENDENCIES_SOLVING => 'onDependencySolve',
137 5
            PackageEvents::POST_PACKAGE_INSTALL => 'onPostPackageInstall',
138 5
            ScriptEvents::POST_INSTALL_CMD => 'onPostInstallOrUpdate',
139 5
            ScriptEvents::POST_UPDATE_CMD => 'onPostInstallOrUpdate',
140 5
            ScriptEvents::PRE_AUTOLOAD_DUMP => 'onInstallUpdateOrDump',
141 5
            ScriptEvents::PRE_INSTALL_CMD => 'onInstallUpdateOrDump',
142 5
            ScriptEvents::PRE_UPDATE_CMD => 'onInstallUpdateOrDump',
143 5
        );
144
    }
145
146
    /**
147
     * Handle an event callback for initialization.
148
     *
149
     * @param \Composer\EventDispatcher\Event $event
150
     */
151 100
    public function onInit(\Composer\EventDispatcher\Event $event)
0 ignored issues
show
Unused Code introduced by
The parameter $event is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
152
    {
153 100
        $this->state->loadSettings();
154
        // @todo devMode cannot be properly detected at this stage, but
155
        //       merging it should not hurt.
156 100
        $this->state->setDevMode(true);
157 100
        $this->mergeFiles($this->state->getIncludes(), false);
158 100
        $this->mergeFiles($this->state->getRequires(), true);
159 95
    }
160
161
    /**
162
     * Handle an event callback for an install, update or dump command by
163
     * checking for "merge-plugin" in the "extra" data and merging package
164
     * contents if found.
165
     *
166
     * @param Event $event
167
     */
168 95
    public function onInstallUpdateOrDump(Event $event)
169
    {
170 95
        $this->state->loadSettings();
171 95
        $this->state->setDevMode($event->isDevMode());
172 95
        $this->mergeFiles($this->state->getIncludes(), false);
173 95
        $this->mergeFiles($this->state->getRequires(), true);
174
175 95
        if ($event->getName() === ScriptEvents::PRE_AUTOLOAD_DUMP) {
176 95
            $this->state->setDumpAutoloader(true);
177 95
            $flags = $event->getFlags();
178 95
            if (isset($flags['optimize'])) {
179 95
                $this->state->setOptimizeAutoloader($flags['optimize']);
180 95
            }
181 95
        }
182 95
    }
183
184
    /**
185
     * Find configuration files matching the configured glob patterns and
186
     * merge their contents with the master package.
187
     *
188
     * @param array $patterns List of files/glob patterns
189
     * @param bool $required Are the patterns required to match files?
190
     * @throws MissingFileException when required and a pattern returns no
191
     *      results
192
     */
193 100
    protected function mergeFiles(array $patterns, $required = false)
194
    {
195 100
        $root = $this->composer->getPackage();
196
197 100
        $files = array_map(
198 100
            function ($files, $pattern) use ($required) {
199 100
                if ($required && !$files) {
200 5
                    throw new MissingFileException(
201 5
                        "merge-plugin: No files matched required '{$pattern}'"
202 5
                    );
203
                }
204 95
                return $files;
205 100
            },
206 100
            array_map('glob', $patterns),
207
            $patterns
208 100
        );
209
210 100
        foreach (array_reduce($files, 'array_merge', array()) as $path) {
211 95
            $this->mergeFile($root, $path);
212 100
        }
213 100
    }
214
215
    /**
216
     * Read a JSON file and merge its contents
217
     *
218
     * @param RootPackageInterface $root
219
     * @param string $path
220
     */
221 95
    protected function mergeFile(RootPackageInterface $root, $path)
222
    {
223 95
        if (isset($this->loadedFiles[$path])) {
224 95
            $this->logger->debug("Already merged <comment>$path</comment>");
225 95
            return;
226
        } else {
227 95
            $this->loadedFiles[$path] = true;
228
        }
229 95
        $this->logger->info("Loading <comment>{$path}</comment>...");
230
231 95
        $package = new ExtraPackage($path, $this->composer, $this->logger);
232 95
        $package->mergeInto($root, $this->state);
233
234 95
        if ($this->state->recurseIncludes()) {
235 90
            $this->mergeFiles($package->getIncludes(), false);
236 90
            $this->mergeFiles($package->getRequires(), true);
237 90
        }
238 95
    }
239
240
    /**
241
     * Handle an event callback for pre-dependency solving phase of an install
242
     * or update by adding any duplicate package dependencies found during
243
     * initial merge processing to the request that will be processed by the
244
     * dependency solver.
245
     *
246
     * @param InstallerEvent $event
247
     */
248 95
    public function onDependencySolve(InstallerEvent $event)
249
    {
250 95
        $request = $event->getRequest();
251 95
        foreach ($this->state->getDuplicateLinks('require') as $link) {
252 15
            $this->logger->info(
253 15
                "Adding dependency <comment>{$link}</comment>"
254 15
            );
255 15
            $request->install($link->getTarget(), $link->getConstraint());
256 95
        }
257 95
        if ($this->state->isDevMode()) {
258 90
            foreach ($this->state->getDuplicateLinks('require-dev') as $link) {
259 5
                $this->logger->info(
260 5
                    "Adding dev dependency <comment>{$link}</comment>"
261 5
                );
262 5
                $request->install($link->getTarget(), $link->getConstraint());
263 90
            }
264 90
        }
265 95
    }
266
267
    /**
268
     * Handle an event callback following installation of a new package by
269
     * checking to see if the package that was installed was our plugin.
270
     *
271
     * @param PackageEvent $event
272
     */
273 15
    public function onPostPackageInstall(PackageEvent $event)
274
    {
275 15
        $op = $event->getOperation();
276 15
        if ($op instanceof InstallOperation) {
277 15
            $package = $op->getPackage()->getName();
278 15
            if ($package === self::PACKAGE_NAME) {
279 10
                $this->logger->info('composer-merge-plugin installed');
280 10
                $this->state->setFirstInstall(true);
281 10
                $this->state->setLocked(
282 10
                    $event->getComposer()->getLocker()->isLocked()
283 10
                );
284 10
            }
285 15
        }
286 15
    }
287
288
    /**
289
     * Handle an event callback following an install or update command. If our
290
     * plugin was installed during the run then trigger an update command to
291
     * process any merge-patterns in the current config.
292
     *
293
     * @param Event $event
294
     */
295 95
    public function onPostInstallOrUpdate(Event $event)
296
    {
297
        // @codeCoverageIgnoreStart
298
        if ($this->state->isFirstInstall()) {
299
            $this->state->setFirstInstall(false);
300
            $this->logger->info(
301
                '<comment>' .
302
                'Running additional update to apply merge settings' .
303
                '</comment>'
304
            );
305
306
            $config = $this->composer->getConfig();
307
308
            $preferSource = $config->get('preferred-install') == 'source';
309
            $preferDist = $config->get('preferred-install') == 'dist';
310
311
            $installer = Installer::create(
312
                $event->getIO(),
313
                // Create a new Composer instance to ensure full processing of
314
                // the merged files.
315
                Factory::create($event->getIO(), null, false)
316
            );
317
318
            $installer->setPreferSource($preferSource);
319
            $installer->setPreferDist($preferDist);
320
            $installer->setDevMode($event->isDevMode());
321
            $installer->setDumpAutoloader($this->state->shouldDumpAutoloader());
322
            $installer->setOptimizeAutoloader(
323
                $this->state->shouldOptimizeAutoloader()
324
            );
325
326
            if ($this->state->forceUpdate()) {
327
                // Force update mode so that new packages are processed rather
328
                // than just telling the user that composer.json and
329
                // composer.lock don't match.
330
                $installer->setUpdate(true);
331
            }
332
333
            $installer->run();
334
        }
335
        // @codeCoverageIgnoreEnd
336 95
    }
337
}
338
// vim:sw=4:ts=4:sts=4:et:
339