Completed
Pull Request — master (#148)
by Patrick D
06:52
created

MergePlugin::onInstallUpdateOrDump()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 15
c 0
b 0
f 0
ccs 13
cts 13
cp 1
rs 9.4285
cc 3
eloc 10
nc 3
nop 1
crap 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
     * @var IOInterface $io
117
     */
118
    protected $io;
119
120
    /**
121
     * Files that have already been fully processed
122
     *
123
     * @var string[] $loaded
124
     */
125
    protected $loaded = array();
126
127
    /**
128
     * Files that have already been partially processed
129
     *
130
     * @var string[] $loadedNoDev
131
     */
132
    protected $loadedNoDev = array();
133
134
    /**
135
     * {@inheritdoc}
136
     */
137 199
    public function activate(Composer $composer, IOInterface $io)
138
    {
139 199
        $this->composer = $composer;
140 199
        $this->state = new PluginState($this->composer);
141 199
        $this->io = $io;
142 199
        $this->logger = new Logger('merge-plugin', $io);
143 199
    }
144
145
    /**
146
     * {@inheritdoc}
147
     */
148 5
    public static function getSubscribedEvents()
149
    {
150
        return array(
151
            // Use our own constant to make this event optional. Once
152
            // composer-1.1 is required, this can use PluginEvents::INIT
153
            // instead.
154 5
            self::COMPAT_PLUGINEVENTS_INIT =>
155 5
                array('onInit', self::CALLBACK_PRIORITY),
156 5
            InstallerEvents::PRE_DEPENDENCIES_SOLVING =>
157 5
                array('onDependencySolve', self::CALLBACK_PRIORITY),
158 5
            PackageEvents::POST_PACKAGE_INSTALL =>
159 5
                array('onPostPackageInstall', self::CALLBACK_PRIORITY),
160 5
            ScriptEvents::POST_INSTALL_CMD =>
161 5
                array('onPostInstallOrUpdate', self::CALLBACK_PRIORITY),
162 5
            ScriptEvents::POST_UPDATE_CMD =>
163 5
                array('onPostInstallOrUpdate', self::CALLBACK_PRIORITY),
164 5
            ScriptEvents::PRE_AUTOLOAD_DUMP =>
165 5
                array('onInstallUpdateOrDump', self::CALLBACK_PRIORITY),
166 5
            ScriptEvents::PRE_INSTALL_CMD =>
167 5
                array('onInstallUpdateOrDump', self::CALLBACK_PRIORITY),
168 5
            ScriptEvents::PRE_UPDATE_CMD =>
169 5
                array('onInstallUpdateOrDump', self::CALLBACK_PRIORITY),
170 5
        );
171
    }
172
173
    /**
174
     * Handle an event callback for initialization.
175
     *
176
     * @param \Composer\EventDispatcher\Event $event
177
     */
178 70
    public function onInit(BaseEvent $event)
179
    {
180 70
        $this->state->loadSettings();
181
        // It is not possible to know if the user specified --dev or --no-dev
182
        // so assume it is false. The dev section will be merged later when
183
        // the other events fire.
184 70
        $this->state->setDevMode(false);
185 70
        $this->mergeFiles($this->state->getIncludes(), false);
186 70
        $this->mergeFiles($this->state->getRequires(), true);
187 70
    }
188
189
    /**
190
     * Handle an event callback for an install, update or dump command by
191
     * checking for "merge-plugin" in the "extra" data and merging package
192
     * contents if found.
193
     *
194
     * @param ScriptEvent $event
195
     */
196 179
    public function onInstallUpdateOrDump(ScriptEvent $event)
197
    {
198 179
        $this->state->loadSettings();
199 179
        $this->state->setDevMode($event->isDevMode());
200 179
        $this->mergeFiles($this->state->getIncludes(), false);
201 179
        $this->mergeFiles($this->state->getRequires(), true);
202
203 174
        if ($event->getName() === ScriptEvents::PRE_AUTOLOAD_DUMP) {
204 174
            $this->state->setDumpAutoloader(true);
205 174
            $flags = $event->getFlags();
206 174
            if (isset($flags['optimize'])) {
207 174
                $this->state->setOptimizeAutoloader($flags['optimize']);
208 174
            }
209 174
        }
210 174
    }
211
212
    /**
213
     * Find configuration files matching the configured glob patterns and
214
     * merge their contents with the master package.
215
     *
216
     * @param array $patterns List of files/glob patterns
217
     * @param bool $required Are the patterns required to match files?
218
     * @throws MissingFileException when required and a pattern returns no
219
     *      results
220
     */
221 179
    protected function mergeFiles(array $patterns, $required = false)
222
    {
223 179
        $root = $this->composer->getPackage();
224
225 179
        $files = array_map(
226 179
            function ($files, $pattern) use ($required) {
227 179
                if ($required && !$files) {
228 5
                    throw new MissingFileException(
229 5
                        "merge-plugin: No files matched required '{$pattern}'"
230 5
                    );
231
                }
232 174
                return $files;
233 179
            },
234 179
            array_map(array($this, 'validatePath'), $patterns),
235
            $patterns
236 179
        );
237
238 179
        foreach (array_reduce($files, 'array_merge', array()) as $path) {
239 174
            $this->mergeFile($root, $path);
240 179
        }
241 179
    }
242
243 179
    protected function validatePath($path){
244 179
        if (substr($path, 0, 7) === 'http://' || substr($path, 0, 8) === 'https://'){
245 4
            $context = stream_context_create(array('http' => array('method' => 'HEAD')));
246 4
            $fd = fopen($path, 'rb', false, $context);
247 4
            if (!empty($fd)) {
248 4
                $valid[] = $path;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$valid was never initialized. Although not strictly required by PHP, it is generally a good practice to add $valid = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
249 4
            }
250 4
            return $valid;
0 ignored issues
show
Bug introduced by
The variable $valid does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
251
        } else {
252 175
            return glob($path);
253
        }
254
    }
255
256
    /**
257
     * Read a JSON file and merge its contents
258
     *
259
     * @param RootPackageInterface $root
260
     * @param string $path
261
     */
262 174
    protected function mergeFile(RootPackageInterface $root, $path)
263
    {
264 174
        if (isset($this->loaded[$path]) ||
265 174
            (isset($this->loadedNoDev[$path]) && !$this->state->isDevMode())
266 174
        ) {
267 174
            $this->logger->debug(
268 174
                "Already merged <comment>$path</comment> completely"
269 174
            );
270 174
            return;
271
        }
272
273 174
        $package = new ExtraPackage($path, $this->composer, $this->logger, $this->io);
274
275 174
        if (isset($this->loadedNoDev[$path])) {
276 70
            $this->logger->info(
277 70
                "Loading -dev sections of <comment>{$path}</comment>..."
278 70
            );
279 70
            $package->mergeDevInto($root, $this->state);
280 70
        } else {
281 174
            $this->logger->info("Loading <comment>{$path}</comment>...");
282 174
            $package->mergeInto($root, $this->state);
283
        }
284
285 174
        if ($this->state->isDevMode()) {
286 169
            $this->loaded[$path] = true;
287 169
        } else {
288 75
            $this->loadedNoDev[$path] = true;
289
        }
290
291 174
        if ($this->state->recurseIncludes()) {
292 169
            $this->mergeFiles($package->getIncludes(), false);
293 169
            $this->mergeFiles($package->getRequires(), true);
294 169
        }
295 174
    }
296
297
    /**
298
     * Handle an event callback for pre-dependency solving phase of an install
299
     * or update by adding any duplicate package dependencies found during
300
     * initial merge processing to the request that will be processed by the
301
     * dependency solver.
302
     *
303
     * @param InstallerEvent $event
304
     */
305 174
    public function onDependencySolve(InstallerEvent $event)
306
    {
307 174
        $request = $event->getRequest();
308 174
        foreach ($this->state->getDuplicateLinks('require') as $link) {
309 25
            $this->logger->info(
310 25
                "Adding dependency <comment>{$link}</comment>"
311 25
            );
312 25
            $request->install($link->getTarget(), $link->getConstraint());
313 174
        }
314
315
        // Issue #113: Check devMode of event rather than our global state.
316
        // Composer fires the PRE_DEPENDENCIES_SOLVING event twice for
317
        // `--no-dev` operations to decide which packages are dev only
318
        // requirements.
319 174
        if ($this->state->shouldMergeDev() && $event->isDevMode()) {
320 169
            foreach ($this->state->getDuplicateLinks('require-dev') as $link) {
321 10
                $this->logger->info(
322 10
                    "Adding dev dependency <comment>{$link}</comment>"
323 10
                );
324 10
                $request->install($link->getTarget(), $link->getConstraint());
325 169
            }
326 169
        }
327 174
    }
328
329
    /**
330
     * Handle an event callback following installation of a new package by
331
     * checking to see if the package that was installed was our plugin.
332
     *
333
     * @param PackageEvent $event
334
     */
335 15
    public function onPostPackageInstall(PackageEvent $event)
336
    {
337 15
        $op = $event->getOperation();
338 15
        if ($op instanceof InstallOperation) {
339 15
            $package = $op->getPackage()->getName();
340 15
            if ($package === self::PACKAGE_NAME) {
341 10
                $this->logger->info('composer-merge-plugin installed');
342 10
                $this->state->setFirstInstall(true);
343 10
                $this->state->setLocked(
344 10
                    $event->getComposer()->getLocker()->isLocked()
345 10
                );
346 10
            }
347 15
        }
348 15
    }
349
350
    /**
351
     * Handle an event callback following an install or update command. If our
352
     * plugin was installed during the run then trigger an update command to
353
     * process any merge-patterns in the current config.
354
     *
355
     * @param ScriptEvent $event
356
     */
357 174
    public function onPostInstallOrUpdate(ScriptEvent $event)
358
    {
359
        // @codeCoverageIgnoreStart
360
        if ($this->state->isFirstInstall()) {
361
            $this->state->setFirstInstall(false);
362
            $this->logger->info(
363
                '<comment>' .
364
                'Running additional update to apply merge settings' .
365
                '</comment>'
366
            );
367
368
            $config = $this->composer->getConfig();
369
370
            $preferSource = $config->get('preferred-install') == 'source';
371
            $preferDist = $config->get('preferred-install') == 'dist';
372
373
            $installer = Installer::create(
374
                $event->getIO(),
375
                // Create a new Composer instance to ensure full processing of
376
                // the merged files.
377
                Factory::create($event->getIO(), null, false)
378
            );
379
380
            $installer->setPreferSource($preferSource);
381
            $installer->setPreferDist($preferDist);
382
            $installer->setDevMode($event->isDevMode());
383
            $installer->setDumpAutoloader($this->state->shouldDumpAutoloader());
384
            $installer->setOptimizeAutoloader(
385
                $this->state->shouldOptimizeAutoloader()
386
            );
387
388
            if ($this->state->forceUpdate()) {
389
                // Force update mode so that new packages are processed rather
390
                // than just telling the user that composer.json and
391
                // composer.lock don't match.
392
                $installer->setUpdate(true);
393
            }
394
395
            $installer->run();
396
        }
397
        // @codeCoverageIgnoreEnd
398 174
    }
399
}
400
// vim:sw=4:ts=4:sts=4:et:
401