ComposerPlugin::mergeFiles()   A
last analyzed

Complexity

Conditions 4
Paths 2

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 17
ccs 10
cts 10
cp 1
rs 9.7
c 0
b 0
f 0
cc 4
nc 2
nop 2
crap 4
1
<?php namespace Arcanedev\Composer;
2
3
use Arcanedev\Composer\Entities\Package;
4
use Arcanedev\Composer\Entities\PluginState;
5
use Arcanedev\Composer\Exceptions\MissingFileException;
6
use Arcanedev\Composer\Utilities\Logger;
7
use Composer\Composer;
8
use Composer\DependencyResolver\Operation\InstallOperation;
9
use Composer\DependencyResolver\Request;
10
use Composer\EventDispatcher\Event as BaseEvent;
11
use Composer\EventDispatcher\EventSubscriberInterface;
12
use Composer\Factory;
13
use Composer\Installer;
14
use Composer\Installer\InstallerEvent;
15
use Composer\Installer\InstallerEvents;
16
use Composer\Installer\PackageEvent;
17
use Composer\Installer\PackageEvents;
18
use Composer\IO\IOInterface;
19
use Composer\Package\RootPackageInterface;
20
use Composer\Plugin\PluginInterface;
21
use Composer\Script\Event as ScriptEvent;
22
use Composer\Script\ScriptEvents;
23
24
/**
25
 * Class     ComposerPlugin
26
 *
27
 * @package  Arcanedev\Composer
28
 * @author   ARCANEDEV <[email protected]>
29
 */
30
class ComposerPlugin implements PluginInterface, EventSubscriberInterface
31
{
32
    /* -----------------------------------------------------------------
33
     |  Constants
34
     | -----------------------------------------------------------------
35
     */
36
37
    /**
38
     * Package name
39
     */
40
    const PACKAGE_NAME = 'arcanedev/composer';
41
42
    /**
43
     * Name of the composer 1.1 init event.
44
     */
45
    const COMPAT_PLUGINEVENTS_INIT = 'init';
46
47
    /**
48
     * Plugin key
49
     */
50
    const PLUGIN_KEY = 'merge-plugin';
51
52
    /**
53
     * Priority that plugin uses to register callbacks.
54
     */
55
    const CALLBACK_PRIORITY = 50000;
56
57
    /* -----------------------------------------------------------------
58
     |  Properties
59
     | -----------------------------------------------------------------
60
     */
61
62
    /** @var \Composer\Composer */
63
    protected $composer;
64
65
    /** @var \Arcanedev\Composer\Entities\PluginState */
66
    protected $state;
67
68
    /** @var \Arcanedev\Composer\Utilities\Logger */
69
    protected $logger;
70
71
    /**
72
     * Files that have already been fully processed.
73
     *
74
     * @var array
75
     */
76
    protected $loaded = [];
77
78
    /**
79
     * Files that have already been partially processed.
80
     *
81
     * @var array
82
     */
83
    protected $loadedNoDev = [];
84
85
    /* -----------------------------------------------------------------
86
     |  Main Methods
87
     | -----------------------------------------------------------------
88
     */
89
90
    /**
91
     * Apply plugin modifications to composer
92
     *
93
     * @param  \Composer\Composer        $composer
94
     * @param  \Composer\IO\IOInterface  $io
95
     */
96 114
    public function activate(Composer $composer, IOInterface $io)
97
    {
98 114
        $this->composer = $composer;
99 114
        $this->state    = new PluginState($composer);
100 114
        $this->logger   = new Logger('merge-plugin', $io);
101 114
}
102
103
    /**
104
     * Returns an array of event names this subscriber wants to listen to.
105
     *
106
     * @return array
107
     */
108 3
    public static function getSubscribedEvents()
109
    {
110
        return [
111
            // Use our own constant to make this event optional.
112
            // Once composer-1.1 is required, this can use PluginEvents::INIT instead.
113 3
            self::COMPAT_PLUGINEVENTS_INIT            => ['onInit', self::CALLBACK_PRIORITY],
114 3
            InstallerEvents::PRE_DEPENDENCIES_SOLVING => ['onDependencySolve', self::CALLBACK_PRIORITY],
115 3
            PackageEvents::POST_PACKAGE_INSTALL       => ['onPostPackageInstall', self::CALLBACK_PRIORITY],
116 3
            ScriptEvents::POST_INSTALL_CMD            => ['onPostInstallOrUpdate', self::CALLBACK_PRIORITY],
117 3
            ScriptEvents::POST_UPDATE_CMD             => ['onPostInstallOrUpdate', self::CALLBACK_PRIORITY],
118 3
            ScriptEvents::PRE_AUTOLOAD_DUMP           => ['onInstallUpdateOrDump', self::CALLBACK_PRIORITY],
119 3
            ScriptEvents::PRE_INSTALL_CMD             => ['onInstallUpdateOrDump', self::CALLBACK_PRIORITY],
120 3
            ScriptEvents::PRE_UPDATE_CMD              => ['onInstallUpdateOrDump', self::CALLBACK_PRIORITY],
121
        ];
122
    }
123
124
    /**
125
     * Handle an event callback for initialization.
126
     *
127
     * @param  \Composer\EventDispatcher\Event  $event
128
     */
129 21
    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...
130
    {
131 21
        $this->state->loadSettings();
132
        // It is not possible to know if the user specified --dev or --no-dev so assume it is false.
133
        // The dev section will be merged later when the other events fire.
134 21
        $this->state->setDevMode(false);
135 21
        $this->mergeFiles($this->state->getIncludes(), false);
136 21
        $this->mergeFiles($this->state->getRequires(), true);
137 21
    }
138
139
    /**
140
     * Handle an event callback for pre-dependency solving phase of an install
141
     * or update by adding any duplicate package dependencies found during
142
     * initial merge processing to the request that will be processed by the
143
     * dependency solver.
144
     *
145
     * @param  \Composer\Installer\InstallerEvent  $event
146
     */
147 99
    public function onDependencySolve(InstallerEvent $event)
148
    {
149 99
        $request = $event->getRequest();
150
151 99
        $this->installRequires(
152 99
            $request, $this->state->getDuplicateLinks('require')
153
        );
154
155
        // Check devMode of event rather than our global state.
156
        // Composer fires the PRE_DEPENDENCIES_SOLVING event twice for `--no-dev`
157
        // operations to decide which packages are dev only requirements.
158 99
        if ($this->state->shouldMergeDev() && $event->isDevMode()) {
159 99
            $this->installRequires(
160 99
                $request, $this->state->getDuplicateLinks('require-dev'), true
161
            );
162
        }
163 99
    }
164
165
    /**
166
     * Install requirements.
167
     *
168
     * @param  \Composer\DependencyResolver\Request  $request
169
     * @param  \Composer\Package\Link[]              $links
170
     * @param  bool                                  $dev
171
     */
172 99
    private function installRequires(Request $request, array $links, $dev = false)
173
    {
174 99
        foreach ($links as $link) {
175 9
            $this->logger->info($dev
176 6
                ? "Adding dev dependency <comment>{$link}</comment>"
177 9
                : "Adding dependency <comment>{$link}</comment>"
178
            );
179 9
            $request->install($link->getTarget(), $link->getConstraint());
180
        }
181 99
    }
182
183
    /**
184
     * Handle an event callback for an install or update or dump-autoload command by checking
185
     * for "merge-patterns" in the "extra" data and merging package contents if found.
186
     *
187
     * @param  \Composer\Script\Event  $event
188
     */
189 102
    public function onInstallUpdateOrDump(ScriptEvent $event)
190
    {
191 102
        $this->state->loadSettings();
192 102
        $this->state->setDevMode($event->isDevMode());
193 102
        $this->mergeFiles($this->state->getIncludes());
194 102
        $this->mergeFiles($this->state->getRequires(), true);
195
196 99
        if ($event->getName() === ScriptEvents::PRE_AUTOLOAD_DUMP) {
197 99
            $this->state->setDumpAutoloader(true);
198 99
            $flags = $event->getFlags();
199
200 99
            if (isset($flags['optimize'])) {
201 99
                $this->state->setOptimizeAutoloader($flags['optimize']);
202
            }
203
        }
204 99
    }
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
     *
213
     * @throws \Arcanedev\Composer\Exceptions\MissingFileException
214
     */
215 102
    protected function mergeFiles(array $patterns, $required = false)
216
    {
217 102
        $root  = $this->composer->getPackage();
218
        $files = array_map(function ($files, $pattern) use ($required) {
219 102
            if ($required && ! $files) {
220 3
                throw new MissingFileException(
221 3
                    "merge-plugin: No files matched required '{$pattern}'"
222
                );
223
            }
224
225 99
            return $files;
226 102
        }, array_map('glob', $patterns), $patterns);
227
228 102
        foreach (array_reduce($files, 'array_merge', []) as $path) {
229 99
            $this->mergeFile($root, $path);
230
        }
231 102
    }
232
233
    /**
234
     * Read a JSON file and merge its contents
235
     *
236
     * @param  \Composer\Package\RootPackageInterface  $root
237
     * @param  string                                  $path
238
     */
239 99
    private function mergeFile(RootPackageInterface $root, $path)
240
    {
241
        if (
242 99
            isset($this->loaded[$path]) ||
243 99
            (isset($this->loadedNoDev[$path]) && ! $this->state->isDevMode())
244
        ) {
245 99
            $this->logger->debug("Already merged <comment>$path</comment> completely");
246 99
            return;
247
        }
248
249 99
        $package = new Package($path, $this->composer, $this->logger);
250
251
        // If something was already loaded, merge just the dev section.
252 99
        if (isset($this->loadedNoDev[$path])) {
253 21
            $this->logger->info("Loading -dev sections of <comment>{$path}</comment>...");
254 21
            $package->mergeDevInto($root, $this->state);
255
        }
256
        else {
257 99
            $this->logger->info("Loading <comment>{$path}</comment>...");
258 99
            $package->mergeInto($root, $this->state);
259
        }
260
261 99
        if ($this->state->isDevMode())
262 99
            $this->loaded[$path] = true;
263
        else
264 21
            $this->loadedNoDev[$path] = true;
265
266 99
        if ($this->state->recurseIncludes()) {
267 96
            $this->mergeFiles($package->getIncludes());
268 96
            $this->mergeFiles($package->getRequires(), true);
269
        }
270 99
    }
271
272
    /**
273
     * Handle an event callback following installation of a new package by
274
     * checking to see if the package that was installed was our plugin.
275
     *
276
     * @param  \Composer\Installer\PackageEvent  $event
277
     */
278 9
    public function onPostPackageInstall(PackageEvent $event)
279
    {
280 9
        $op = $event->getOperation();
281
282 9
        if ($op instanceof InstallOperation) {
283 9
            $package = $op->getPackage()->getName();
284
285 9
            if ($package === self::PACKAGE_NAME) {
286 6
                $this->logger->debug('Composer merge-plugin installed');
287 6
                $this->state->setFirstInstall(true);
288 6
                $this->state->setLocked(
289 6
                    $event->getComposer()->getLocker()->isLocked()
290
                );
291
            }
292
        }
293 9
    }
294
295
    /**
296
     * Handle an event callback following an install or update command. If our
297
     * plugin was installed during the run then trigger an update command to
298
     * process any merge-patterns in the current config.
299
     *
300
     * @param  \Composer\Script\Event  $event
301
     *
302
     * @codeCoverageIgnore
303
     */
304
    public function onPostInstallOrUpdate(ScriptEvent $event)
305
    {
306
        if ($this->state->isFirstInstall()) {
307
            $this->state->setFirstInstall(false);
308
            $this->logger->info(
309
                '<comment>Running additional update to apply merge settings</comment>'
310
            );
311
            $this->runFirstInstall($event);
312
        }
313
    }
314
315
    /**
316
     * Run first install.
317
     *
318
     * @param  \Composer\Script\Event  $event
319
     *
320
     * @throws \Exception
321
     *
322
     * @codeCoverageIgnore
323
     */
324
    private function runFirstInstall(ScriptEvent $event)
325
    {
326
        $installer    = Installer::create(
327
            $event->getIO(),
328
            // Create a new Composer instance to ensure full processing of the merged files.
329
            Factory::create($event->getIO(), null, false)
330
        );
331
332
        $installer->setPreferSource($this->isPreferredInstall('source'));
333
        $installer->setPreferDist($this->isPreferredInstall('dist'));
334
        $installer->setDevMode($event->isDevMode());
335
        $installer->setDumpAutoloader($this->state->shouldDumpAutoloader());
336
        $installer->setOptimizeAutoloader($this->state->shouldOptimizeAutoloader());
337
338
        if ($this->state->forceUpdate()) {
339
            // Force update mode so that new packages are processed rather than just telling
340
            // the user that composer.json and composer.lock don't match.
341
            $installer->setUpdate(true);
342
        }
343
344
        $installer->run();
345
    }
346
347
    /* -----------------------------------------------------------------
348
     |  Check Methods
349
     | -----------------------------------------------------------------
350
     */
351
352
    /**
353
     * Check the preferred install (source or dist).
354
     *
355
     * @param  string  $preferred
356
     *
357
     * @return bool
358
     *
359
     * @codeCoverageIgnore
360
     */
361
    private function isPreferredInstall($preferred)
362
    {
363
        return $this->composer->getConfig()->get('preferred-install') === $preferred;
364
    }
365
}
366