Completed
Push — master ( 9645a9...5dfc66 )
by Bryan
03:08
created

MergePlugin::mergeFile()   C

Complexity

Conditions 7
Paths 9

Size

Total Lines 34
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 7.3484

Importance

Changes 5
Bugs 0 Features 1
Metric Value
c 5
b 0
f 1
dl 0
loc 34
ccs 21
cts 26
cp 0.8077
rs 6.7272
cc 7
eloc 21
nc 9
nop 2
crap 7.3484
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
     * @var Composer $composer
97
     */
98
    protected $composer;
99
100
    /**
101
     * @var PluginState $state
102
     */
103
    protected $state;
104
105
    /**
106
     * @var Logger $logger
107
     */
108
    protected $logger;
109
110
    /**
111
     * Files that have already been fully processed
112
     *
113
     * @var string[] $loaded
114
     */
115
    protected $loaded = array();
116
117
    /**
118
     * Files that have already been partially processed
119
     *
120
     * @var string[] $loadedNoDev
121
     */
122
    protected $loadedNoDev = array();
123
124
    /**
125
     * {@inheritdoc}
126
     */
127 125
    public function activate(Composer $composer, IOInterface $io)
128
    {
129 125
        $this->composer = $composer;
130 125
        $this->state = new PluginState($this->composer);
131 125
        $this->logger = new Logger('merge-plugin', $io);
132 125
    }
133
134
    /**
135
     * {@inheritdoc}
136
     */
137 5
    public static function getSubscribedEvents()
138
    {
139
        return array(
140
            // Use our own constant to make this event optional. Once
141
            // composer-1.1 is required, this can use PluginEvents::INIT
142
            // instead.
143 5
            self::COMPAT_PLUGINEVENTS_INIT => 'onInit',
144 5
            InstallerEvents::PRE_DEPENDENCIES_SOLVING => 'onDependencySolve',
145 5
            PackageEvents::POST_PACKAGE_INSTALL => 'onPostPackageInstall',
146 5
            ScriptEvents::POST_INSTALL_CMD => 'onPostInstallOrUpdate',
147 5
            ScriptEvents::POST_UPDATE_CMD => 'onPostInstallOrUpdate',
148 5
            ScriptEvents::PRE_AUTOLOAD_DUMP => 'onInstallUpdateOrDump',
149 5
            ScriptEvents::PRE_INSTALL_CMD => 'onInstallUpdateOrDump',
150 5
            ScriptEvents::PRE_UPDATE_CMD => 'onInstallUpdateOrDump',
151 5
        );
152
    }
153
154
    /**
155
     * Handle an event callback for initialization.
156
     *
157
     * @param \Composer\EventDispatcher\Event $event
158
     */
159 30
    public function onInit(BaseEvent $event)
1 ignored issue
show
Unused Code introduced by Fabian Franz
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...
160
    {
161 30
        $this->state->loadSettings();
162
        // It is not possible to know if the user specified --dev or --no-dev
163
        // so assume it is false. The dev section will be merged later when
164
        // the other events fire.
165 30
        $this->state->setDevMode(false);
166 30
        $this->mergeFiles($this->state->getIncludes(), false);
167 30
        $this->mergeFiles($this->state->getRequires(), true);
168 30
    }
169
170
    /**
171
     * Handle an event callback for an install, update or dump command by
172
     * checking for "merge-plugin" in the "extra" data and merging package
173
     * contents if found.
174
     *
175
     * @param ScriptEvent $event
176
     */
177 105
    public function onInstallUpdateOrDump(ScriptEvent $event)
178
    {
179 105
        $this->state->loadSettings();
180 105
        $this->state->setDevMode($event->isDevMode());
181 105
        $this->mergeFiles($this->state->getIncludes(), false);
182 105
        $this->mergeFiles($this->state->getRequires(), true);
183
184 100
        if ($event->getName() === ScriptEvents::PRE_AUTOLOAD_DUMP) {
185 100
            $this->state->setDumpAutoloader(true);
186 100
            $flags = $event->getFlags();
187 100
            if (isset($flags['optimize'])) {
188 100
                $this->state->setOptimizeAutoloader($flags['optimize']);
189 100
            }
190 100
        }
191 100
    }
192
193
    /**
194
     * Find configuration files matching the configured glob patterns and
195
     * merge their contents with the master package.
196
     *
197
     * @param array $patterns List of files/glob patterns
198
     * @param bool $required Are the patterns required to match files?
199
     * @throws MissingFileException when required and a pattern returns no
200
     *      results
201
     */
202 105
    protected function mergeFiles(array $patterns, $required = false)
203
    {
204 105
        $root = $this->composer->getPackage();
205
206 105
        $files = array_map(
207 105
            function ($files, $pattern) use ($required) {
208 105
                if ($required && !$files) {
209 5
                    throw new MissingFileException(
210 5
                        "merge-plugin: No files matched required '{$pattern}'"
211 5
                    );
212
                }
213 100
                return $files;
214 105
            },
215 105
            array_map('glob', $patterns),
216
            $patterns
217 105
        );
218
219 105
        foreach (array_reduce($files, 'array_merge', array()) as $path) {
220 100
            $this->mergeFile($root, $path);
221 105
        }
222 105
    }
223
224
    /**
225
     * Read a JSON file and merge its contents
226
     *
227
     * @param RootPackageInterface $root
228
     * @param string $path
229
     */
230 100
    protected function mergeFile(RootPackageInterface $root, $path)
231
    {
232 100
        if (isset($this->loaded[$path]) ||
233 100
            (isset($this->loadedNoDev[$path]) && !$this->state->isDevMode())
234 100
        ) {
235 100
            $this->logger->debug(
236 100
                "Already merged <comment>$path</comment> completely"
237 100
            );
238 100
            return;
239
        }
240
241 100
        $package = new ExtraPackage($path, $this->composer, $this->logger);
242
243 100
        if (isset($this->loadedNoDev[$path])) {
244
            $this->logger->info(
245
                "Loading -dev sections of <comment>{$path}</comment>..."
246
            );
247
            $package->mergeDevInto($root, $this->state);
248
        } else {
249 100
            $this->logger->info("Loading <comment>{$path}</comment>...");
250 100
            $package->mergeInto($root, $this->state);
251
        }
252
253 100
        if ($this->state->isDevMode()) {
254 95
            $this->loaded[$path] = true;
255 95
        } else {
256 5
            $this->loadedNoDev[$path] = true;
257
        }
258
259 100
        if ($this->state->recurseIncludes()) {
260 95
            $this->mergeFiles($package->getIncludes(), false);
261 95
            $this->mergeFiles($package->getRequires(), true);
262 95
        }
263 100
    }
264
265
    /**
266
     * Handle an event callback for pre-dependency solving phase of an install
267
     * or update by adding any duplicate package dependencies found during
268
     * initial merge processing to the request that will be processed by the
269
     * dependency solver.
270
     *
271
     * @param InstallerEvent $event
272
     */
273 100
    public function onDependencySolve(InstallerEvent $event)
274
    {
275 100
        $request = $event->getRequest();
276 100
        foreach ($this->state->getDuplicateLinks('require') as $link) {
277 15
            $this->logger->info(
278 15
                "Adding dependency <comment>{$link}</comment>"
279 15
            );
280 15
            $request->install($link->getTarget(), $link->getConstraint());
281 100
        }
282 100
        if ($this->state->isDevMode()) {
283 95
            foreach ($this->state->getDuplicateLinks('require-dev') as $link) {
284 5
                $this->logger->info(
285 5
                    "Adding dev dependency <comment>{$link}</comment>"
286 5
                );
287 5
                $request->install($link->getTarget(), $link->getConstraint());
288 95
            }
289 95
        }
290 100
    }
291
292
    /**
293
     * Handle an event callback following installation of a new package by
294
     * checking to see if the package that was installed was our plugin.
295
     *
296
     * @param PackageEvent $event
297
     */
298 15
    public function onPostPackageInstall(PackageEvent $event)
299
    {
300 15
        $op = $event->getOperation();
301 15
        if ($op instanceof InstallOperation) {
302 15
            $package = $op->getPackage()->getName();
303 15
            if ($package === self::PACKAGE_NAME) {
304 10
                $this->logger->info('composer-merge-plugin installed');
305 10
                $this->state->setFirstInstall(true);
306 10
                $this->state->setLocked(
307 10
                    $event->getComposer()->getLocker()->isLocked()
308 10
                );
309 10
            }
310 15
        }
311 15
    }
312
313
    /**
314
     * Handle an event callback following an install or update command. If our
315
     * plugin was installed during the run then trigger an update command to
316
     * process any merge-patterns in the current config.
317
     *
318
     * @param ScriptEvent $event
319
     */
320 100
    public function onPostInstallOrUpdate(ScriptEvent $event)
321
    {
322
        // @codeCoverageIgnoreStart
323
        if ($this->state->isFirstInstall()) {
324
            $this->state->setFirstInstall(false);
325
            $this->logger->info(
326
                '<comment>' .
327
                'Running additional update to apply merge settings' .
328
                '</comment>'
329
            );
330
331
            $config = $this->composer->getConfig();
332
333
            $preferSource = $config->get('preferred-install') == 'source';
334
            $preferDist = $config->get('preferred-install') == 'dist';
335
336
            $installer = Installer::create(
337
                $event->getIO(),
338
                // Create a new Composer instance to ensure full processing of
339
                // the merged files.
340
                Factory::create($event->getIO(), null, false)
341
            );
342
343
            $installer->setPreferSource($preferSource);
344
            $installer->setPreferDist($preferDist);
345
            $installer->setDevMode($event->isDevMode());
346
            $installer->setDumpAutoloader($this->state->shouldDumpAutoloader());
347
            $installer->setOptimizeAutoloader(
348
                $this->state->shouldOptimizeAutoloader()
349
            );
350
351
            if ($this->state->forceUpdate()) {
352
                // Force update mode so that new packages are processed rather
353
                // than just telling the user that composer.json and
354
                // composer.lock don't match.
355
                $installer->setUpdate(true);
356
            }
357
358
            $installer->run();
359
        }
360
        // @codeCoverageIgnoreEnd
361 100
    }
362
}
363
// vim:sw=4:ts=4:sts=4:et:
364