Completed
Pull Request — master (#163)
by
unknown
20:55
created

MergePlugin   B

Complexity

Total Complexity 29

Size/Duplication

Total Lines 313
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 17

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 29
lcom 1
cbo 17
dl 0
loc 313
ccs 116
cts 116
cp 1
rs 7.8571
c 0
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
A activate() 0 6 1
B getSubscribedEvents() 0 24 1
A onInit() 0 10 1
A onInstallUpdateOrDump() 0 15 3
A mergeFiles() 0 21 4
C mergeFile() 0 48 8
B onDependencySolve() 0 23 5
A onPostPackageInstall() 0 14 3
B onPostInstallOrUpdate() 0 42 3
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\Event as BaseEvent;
20
use Composer\EventDispatcher\EventSubscriberInterface;
21
use Composer\Factory;
22
use Composer\Installer;
23
use Composer\Installer\InstallerEvent;
24
use Composer\Installer\InstallerEvents;
25
use Composer\Installer\PackageEvent;
26
use Composer\Installer\PackageEvents;
27
use Composer\IO\IOInterface;
28
use Composer\Package\RootPackageInterface;
29
use Composer\Plugin\PluginInterface;
30
use Composer\Script\Event as ScriptEvent;
31
use Composer\Script\ScriptEvents;
32
33
/**
34
 * Composer plugin that allows merging multiple composer.json files.
35
 *
36
 * When installed, this plugin will look for a "merge-plugin" key in the
37
 * composer configuration's "extra" section. The value for this key is
38
 * a set of options configuring the plugin.
39
 *
40
 * An "include" setting is required. The value of this setting can be either
41
 * a single value or an array of values. Each value is treated as a glob()
42
 * pattern identifying additional composer.json style configuration files to
43
 * merge into the configuration for the current compser execution.
44
 *
45
 * The "autoload", "autoload-dev", "conflict", "provide", "replace",
46
 * "repositories", "require", "require-dev", and "suggest" sections of the
47
 * found configuration files will be merged into the root package
48
 * configuration as though they were directly included in the top-level
49
 * composer.json file.
50
 *
51
 * If included files specify conflicting package versions for "require" or
52
 * "require-dev", the normal Composer dependency solver process will be used
53
 * to attempt to resolve the conflict. Specifying the 'replace' key as true will
54
 * change this default behaviour so that the last-defined version of a package
55
 * will win, allowing for force-overrides of package defines.
56
 *
57
 * By default the "extra" section is not merged. This can be enabled by
58
 * setitng the 'merge-extra' key to true. In normal mode, when the same key is
59
 * found in both the original and the imported extra section, the version in
60
 * the original config is used and the imported version is skipped. If
61
 * 'replace' mode is active, this behaviour changes so the imported version of
62
 * the key is used, replacing the version in the original config.
63
 *
64
 *
65
 * @code
66
 * {
67
 *     "require": {
68
 *         "wikimedia/composer-merge-plugin": "dev-master"
69
 *     },
70
 *     "extra": {
71
 *         "merge-plugin": {
72
 *             "include": [
73
 *                 "composer.local.json"
74
 *             ]
75
 *         }
76
 *     }
77
 * }
78
 * @endcode
79
 *
80
 * @author Bryan Davis <[email protected]>
81
 */
82
class MergePlugin implements PluginInterface, EventSubscriberInterface
83
{
84
85
    /**
86
     * Offical package name
87
     */
88
    const PACKAGE_NAME = 'wikimedia/composer-merge-plugin';
89
90
    /**
91
     * Name of the composer 1.1 init event.
92
     */
93
    const COMPAT_PLUGINEVENTS_INIT = 'init';
94
95
    /**
96
     * Priority that plugin uses to register callbacks.
97
     */
98
    const CALLBACK_PRIORITY = 50000;
99
100
    /**
101
     * @var Composer $composer
102
     */
103
    protected $composer;
104
105
    /**
106
     * @var PluginState $state
107
     */
108
    protected $state;
109
110
    /**
111
     * @var Logger $logger
112
     */
113
    protected $logger;
114
115
    /**
116
     * Files that have already been fully processed
117
     *
118
     * @var string[] $loaded
119
     */
120
    protected $loaded = array();
121
122
    /**
123
     * Files that have already been partially processed
124
     *
125
     * @var string[] $loadedNoDev
126
     */
127
    protected $loadedNoDev = array();
128
129
    /**
130
     * {@inheritdoc}
131
     */
132 195
    public function activate(Composer $composer, IOInterface $io)
133
    {
134 195
        $this->composer = $composer;
135 195
        $this->state = new PluginState($this->composer);
136 195
        $this->logger = new Logger('merge-plugin', $io);
137 195
    }
138
139
    /**
140
     * {@inheritdoc}
141
     */
142 5
    public static function getSubscribedEvents()
143
    {
144
        return array(
145
            // Use our own constant to make this event optional. Once
146
            // composer-1.1 is required, this can use PluginEvents::INIT
147
            // instead.
148 5
            self::COMPAT_PLUGINEVENTS_INIT =>
149 5
                array('onInit', self::CALLBACK_PRIORITY),
150 5
            InstallerEvents::PRE_DEPENDENCIES_SOLVING =>
151 5
                array('onDependencySolve', self::CALLBACK_PRIORITY),
152 5
            PackageEvents::POST_PACKAGE_INSTALL =>
153 5
                array('onPostPackageInstall', self::CALLBACK_PRIORITY),
154 5
            ScriptEvents::POST_INSTALL_CMD =>
155 5
                array('onPostInstallOrUpdate', self::CALLBACK_PRIORITY),
156 5
            ScriptEvents::POST_UPDATE_CMD =>
157 5
                array('onPostInstallOrUpdate', self::CALLBACK_PRIORITY),
158 5
            ScriptEvents::PRE_AUTOLOAD_DUMP =>
159 5
                array('onInstallUpdateOrDump', self::CALLBACK_PRIORITY),
160 5
            ScriptEvents::PRE_INSTALL_CMD =>
161 5
                array('onInstallUpdateOrDump', self::CALLBACK_PRIORITY),
162 5
            ScriptEvents::PRE_UPDATE_CMD =>
163 5
                array('onInstallUpdateOrDump', self::CALLBACK_PRIORITY),
164 5
        );
165
    }
166
167
    /**
168
     * Handle an event callback for initialization.
169
     *
170
     * @param \Composer\EventDispatcher\Event $event
171
     */
172 70
    public function onInit(BaseEvent $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...
173
    {
174 70
        $this->state->loadSettings();
175
        // It is not possible to know if the user specified --dev or --no-dev
176
        // so assume it is false. The dev section will be merged later when
177
        // the other events fire.
178 70
        $this->state->setDevMode(false);
179 70
        $this->mergeFiles($this->state->getIncludes(), false);
180 70
        $this->mergeFiles($this->state->getRequires(), true);
181 70
    }
182
183
    /**
184
     * Handle an event callback for an install, update or dump command by
185
     * checking for "merge-plugin" in the "extra" data and merging package
186
     * contents if found.
187
     *
188
     * @param ScriptEvent $event
189
     */
190 175
    public function onInstallUpdateOrDump(ScriptEvent $event)
191
    {
192 175
        $this->state->loadSettings();
193 175
        $this->state->setDevMode($event->isDevMode());
194 175
        $this->mergeFiles($this->state->getIncludes(), false);
195 175
        $this->mergeFiles($this->state->getRequires(), true);
196
197 170
        if ($event->getName() === ScriptEvents::PRE_AUTOLOAD_DUMP) {
198 170
            $this->state->setDumpAutoloader(true);
199 170
            $flags = $event->getFlags();
200 170
            if (isset($flags['optimize'])) {
201 170
                $this->state->setOptimizeAutoloader($flags['optimize']);
202 170
            }
203 170
        }
204 170
    }
205
206
    /**
207
     * Find configuration files matching the configured glob patterns and
208
     * merge their contents with the master package.
209
     *
210
     * @param array $patterns List of files/glob patterns
211
     * @param bool $required Are the patterns required to match files?
212
     * @throws MissingFileException when required and a pattern returns no
213
     *      results
214
     */
215 175
    protected function mergeFiles(array $patterns, $required = false)
216
    {
217 175
        $root = $this->composer->getPackage();
218
219 175
        $files = array_map(
220 175
            function ($files, $pattern) use ($required) {
221 175
                if ($required && !$files) {
222 5
                    throw new MissingFileException(
223 5
                        "merge-plugin: No files matched required '{$pattern}'"
224 5
                    );
225
                }
226 170
                return $files;
227 175
            },
228 175
            array_map('glob', $patterns),
229
            $patterns
230 175
        );
231
232 175
        foreach (array_reduce($files, 'array_merge', array()) as $path) {
233 170
            $this->mergeFile($root, $path);
234 175
        }
235 175
    }
236
237
    /**
238
     * Read a JSON file and merge its contents
239
     *
240
     * @param RootPackageInterface $root
241
     * @param string $path
242
     */
243 170
    protected function mergeFile(RootPackageInterface $root, $path)
244
    {
245 170
        if (isset($this->loaded[$path]) ||
246 170
            (isset($this->loadedNoDev[$path]) && !$this->state->isDevMode())
247 170
        ) {
248 170
            $this->logger->debug(
249 170
                "Already merged <comment>$path</comment> completely"
250 170
            );
251 170
            return;
252
        }
253
254 170
        $package = new ExtraPackage($path, $this->composer, $this->logger);
255
256 170
        if (isset($this->loadedNoDev[$path])) {
257 70
            $this->logger->info(
258 70
                "Loading -dev sections of <comment>{$path}</comment>..."
259 70
            );
260 70
            $package->mergeDevInto($root, $this->state);
261 70
        } else {
262 170
            $this->logger->info("Loading <comment>{$path}</comment>...");
263 170
            $package->mergeInto($root, $this->state);
264
        }
265
266 170
        //should replaced
267 165
        if ($this->state->shouldCreateReplace()) {
268 165
            $replaces = $root->getReplaces();
269 75
            $replaces[$package->getName()] = new \Composer\Package\Link(
270
                $root->getName(),
271
                $package->getName(),
272 170
                new \Composer\Semver\Constraint\Constraint('=', $root->getVersion()),
273 165
                'replaces',
274 165
                'self.version'
275 165
            );
276 170
            $root->setReplaces($replaces);
277
            $this->logger->info("Added <comment>{$package->getName()}</comment> to replace section of {$root->getName()}");
278
        }
279
280
        if ($this->state->isDevMode()) {
281
            $this->loaded[$path] = true;
282
        } else {
283
            $this->loadedNoDev[$path] = true;
284
        }
285
286 170
        if ($this->state->recurseIncludes()) {
287
            $this->mergeFiles($package->getIncludes(), false);
288 170
            $this->mergeFiles($package->getRequires(), true);
289 170
        }
290 25
    }
291 25
292 25
    /**
293 25
     * Handle an event callback for pre-dependency solving phase of an install
294 170
     * or update by adding any duplicate package dependencies found during
295
     * initial merge processing to the request that will be processed by the
296
     * dependency solver.
297
     *
298
     * @param InstallerEvent $event
299
     */
300 170
    public function onDependencySolve(InstallerEvent $event)
301 165
    {
302 10
        $request = $event->getRequest();
303 10
        foreach ($this->state->getDuplicateLinks('require') as $link) {
304 10
            $this->logger->info(
305 10
                "Adding dependency <comment>{$link}</comment>"
306 165
            );
307 165
            $request->install($link->getTarget(), $link->getConstraint());
308 170
        }
309
310
        // Issue #113: Check devMode of event rather than our global state.
311
        // Composer fires the PRE_DEPENDENCIES_SOLVING event twice for
312
        // `--no-dev` operations to decide which packages are dev only
313
        // requirements.
314
        if ($this->state->shouldMergeDev() && $event->isDevMode()) {
315
            foreach ($this->state->getDuplicateLinks('require-dev') as $link) {
316 15
                $this->logger->info(
317
                    "Adding dev dependency <comment>{$link}</comment>"
318 15
                );
319 15
                $request->install($link->getTarget(), $link->getConstraint());
320 15
            }
321 15
        }
322 10
    }
323 10
324 10
    /**
325 10
     * Handle an event callback following installation of a new package by
326 10
     * checking to see if the package that was installed was our plugin.
327 10
     *
328 15
     * @param PackageEvent $event
329 15
     */
330
    public function onPostPackageInstall(PackageEvent $event)
331
    {
332
        $op = $event->getOperation();
333
        if ($op instanceof InstallOperation) {
334
            $package = $op->getPackage()->getName();
335
            if ($package === self::PACKAGE_NAME) {
336
                $this->logger->info('composer-merge-plugin installed');
337
                $this->state->setFirstInstall(true);
338 170
                $this->state->setLocked(
339
                    $event->getComposer()->getLocker()->isLocked()
340
                );
341
            }
342
        }
343
    }
344
345
    /**
346
     * Handle an event callback following an install or update command. If our
347
     * plugin was installed during the run then trigger an update command to
348
     * process any merge-patterns in the current config.
349
     *
350
     * @param ScriptEvent $event
351
     */
352
    public function onPostInstallOrUpdate(ScriptEvent $event)
353
    {
354
        // @codeCoverageIgnoreStart
355
        if ($this->state->isFirstInstall()) {
356
            $this->state->setFirstInstall(false);
357
            $this->logger->info(
358
                '<comment>' .
359
                'Running additional update to apply merge settings' .
360
                '</comment>'
361
            );
362
363
            $config = $this->composer->getConfig();
364
365
            $preferSource = $config->get('preferred-install') == 'source';
366
            $preferDist = $config->get('preferred-install') == 'dist';
367
368
            $installer = Installer::create(
369
                $event->getIO(),
370
                // Create a new Composer instance to ensure full processing of
371
                // the merged files.
372
                Factory::create($event->getIO(), null, false)
373
            );
374
375
            $installer->setPreferSource($preferSource);
376
            $installer->setPreferDist($preferDist);
377
            $installer->setDevMode($event->isDevMode());
378
            $installer->setDumpAutoloader($this->state->shouldDumpAutoloader());
379 170
            $installer->setOptimizeAutoloader(
380
                $this->state->shouldOptimizeAutoloader()
381
            );
382
383
            if ($this->state->forceUpdate()) {
384
                // Force update mode so that new packages are processed rather
385
                // than just telling the user that composer.json and
386
                // composer.lock don't match.
387
                $installer->setUpdate(true);
388
            }
389
390
            $installer->run();
391
        }
392
        // @codeCoverageIgnoreEnd
393
    }
394
}
395
// vim:sw=4:ts=4:sts=4:et:
396