These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
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
|
|||
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 |
This check looks from parameters that have been defined for a function or method, but which are not used in the method body.