Completed
Push — master ( 1feee7...3f7e70 )
by ARCANEDEV
03:14
created

ComposerPlugin::runFirstInstall()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 22
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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