Completed
Push — master ( 70df7e...516969 )
by ARCANEDEV
9s
created

ComposerPlugin::onInit()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

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