• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

cweagans / composer-patches / 4116908618

pending completion
4116908618

push

github

GitHub
Merge pull request #447 from cweagans/2.x-wip

457 of 457 new or added lines in 28 files covered. (100.0%)

456 of 570 relevant lines covered (80.0%)

3.29 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.19
/src/Plugin/Patches.php
1
<?php
2

3
/**
4
 * @file
5
 * Provides a way to patch Composer packages after installation.
6
 */
7

8
namespace cweagans\Composer\Plugin;
9

10
use Composer\Composer;
11
use Composer\DependencyResolver\Operation\InstallOperation;
12
use Composer\DependencyResolver\Operation\OperationInterface;
13
use Composer\DependencyResolver\Operation\UpdateOperation;
14
use Composer\EventDispatcher\EventDispatcher;
15
use Composer\EventDispatcher\EventSubscriberInterface;
16
use Composer\Installer\PackageEvent;
17
use Composer\Installer\PackageEvents;
18
use Composer\IO\IOInterface;
19
use Composer\Json\JsonFile;
20
use Composer\Package\PackageInterface;
21
use Composer\Plugin\Capability\CommandProvider as CommandProviderCapability;
22
use Composer\Plugin\Capable;
23
use Composer\Plugin\PluginInterface;
24
use Composer\Util\ProcessExecutor;
25
use cweagans\Composer\Capability\CommandProvider;
26
use cweagans\Composer\Capability\Downloader\CoreDownloaderProvider;
27
use cweagans\Composer\Capability\Downloader\DownloaderProvider;
28
use cweagans\Composer\Capability\Patcher\CorePatcherProvider;
29
use cweagans\Composer\Capability\Patcher\PatcherProvider;
30
use cweagans\Composer\Capability\Resolver\CoreResolverProvider;
31
use cweagans\Composer\Capability\Resolver\ResolverProvider;
32
use cweagans\Composer\ConfigurablePlugin;
33
use cweagans\Composer\Downloader;
34
use cweagans\Composer\Event\PatchEvent;
35
use cweagans\Composer\Event\PatchEvents;
36
use cweagans\Composer\Locker;
37
use cweagans\Composer\Patch;
38
use cweagans\Composer\PatchCollection;
39
use cweagans\Composer\Patcher;
40
use cweagans\Composer\Resolver;
41
use cweagans\Composer\Util;
42
use InvalidArgumentException;
43
use Exception;
44

45
class Patches implements PluginInterface, EventSubscriberInterface, Capable
46
{
47
    use ConfigurablePlugin;
48

49
    /**
50
     * @var Composer $composer
51
     */
52
    protected Composer $composer;
53

54
    /**
55
     * @var IOInterface $io
56
     */
57
    protected IOInterface $io;
58

59
    /**
60
     * @var EventDispatcher $eventDispatcher
61
     */
62
    protected EventDispatcher $eventDispatcher;
63

64
    /**
65
     * @var ProcessExecutor $executor
66
     */
67
    protected ProcessExecutor $executor;
68

69
    /**
70
     * @var array $patches
71
     */
72
    protected array $patches;
73

74
    /**
75
     * @var array $installedPatches
76
     */
77
    protected array $installedPatches;
78

79
    /**
80
     * @var ?PatchCollection $patchCollection
81
     */
82
    protected ?PatchCollection $patchCollection;
83

84
    protected Locker $locker;
85

86
    protected JsonFile $lockFile;
87

88
    /**
89
     * Apply plugin modifications to composer
90
     *
91
     * @param Composer $composer
92
     * @param IOInterface $io
93
     */
94
    public function activate(Composer $composer, IOInterface $io): void
95
    {
96
        $this->composer = $composer;
2✔
97
        $this->io = $io;
2✔
98
        $this->executor = new ProcessExecutor($this->io);
2✔
99
        $this->patches = array();
2✔
100
        $this->installedPatches = array();
2✔
101
        $this->lockFile = new JsonFile(
2✔
102
            dirname(realpath(\Composer\Factory::getComposerFile())) . '/patches.lock',
2✔
103
            null,
2✔
104
            $this->io
2✔
105
        );
2✔
106
        $this->locker = new Locker($this->lockFile);
2✔
107
        $this->configuration = [
2✔
108
            'disable-patching' => [
2✔
109
                'type' => 'bool',
2✔
110
                'default' => false,
2✔
111
            ],
2✔
112
            'disable-resolvers' => [
2✔
113
                'type' => 'list',
2✔
114
                'default' => [],
2✔
115
            ],
2✔
116
            'disable-downloaders' => [
2✔
117
                'type' => 'list',
2✔
118
                'default' => [],
2✔
119
            ],
2✔
120
            'disable-patchers' => [
2✔
121
                'type' => 'list',
2✔
122
                'default' => [],
2✔
123
            ],
2✔
124
            'default-patch-depth' => [
2✔
125
                'type' => 'int',
2✔
126
                'default' => 1,
2✔
127
            ],
2✔
128
            'patches-file' => [
2✔
129
                'type' => 'string',
2✔
130
                'default' => '',
2✔
131
            ]
2✔
132
        ];
2✔
133
        $this->configure($this->composer->getPackage()->getExtra(), 'composer-patches');
2✔
134
    }
135

136
    /**
137
     * Returns an array of event names this subscriber wants to listen to.
138
     *
139
     * @calls resolvePatches
140
     */
141
    public static function getSubscribedEvents(): array
142
    {
143
        return array(
1✔
144
            PackageEvents::PRE_PACKAGE_INSTALL => ['loadLockedPatches'],
1✔
145
            PackageEvents::PRE_PACKAGE_UPDATE => ['loadLockedPatches'],
1✔
146
            // The POST_PACKAGE_* events are a higher weight for compatibility with
147
            // https://github.com/AydinHassan/magento-core-composer-installer and more generally for compatibility with
148
            // any Composer Plugin which deploys downloaded packages to other locations. In the cast that you want
149
            // those plugins to deploy patched files, those plugins have to run *after* this plugin.
150
            // @see: https://github.com/cweagans/composer-patches/pull/153
151
            PackageEvents::POST_PACKAGE_INSTALL => ['patchPackage', 10],
1✔
152
            PackageEvents::POST_PACKAGE_UPDATE => ['patchPackage', 10],
1✔
153
        );
1✔
154
    }
155

156
    /**
157
     * Return a list of plugin capabilities.
158
     *
159
     * @return array
160
     */
161
    public function getCapabilities(): array
162
    {
163
        return [
1✔
164
            ResolverProvider::class => CoreResolverProvider::class,
1✔
165
            DownloaderProvider::class => CoreDownloaderProvider::class,
1✔
166
            PatcherProvider::class => CorePatcherProvider::class,
1✔
167
            CommandProviderCapability::class => CommandProvider::class,
1✔
168
        ];
1✔
169
    }
170

171
    /**
172
     * Discover patches using all available Resolvers.
173
     */
174
    public function resolvePatches()
175
    {
176
        $resolver = new Resolver($this->composer, $this->io, $this->getConfig('disable-resolvers'));
1✔
177
        return $resolver->loadFromResolvers();
1✔
178
    }
179

180
    /**
181
     * Resolve and download patches so that all sha256 sums can be included in the lock file.
182
     */
183
    public function createNewPatchesLock()
184
    {
185
        $this->patchCollection = $this->resolvePatches();
1✔
186
        $downloader = new Downloader($this->composer, $this->io, $this->getConfig('disable-downloaders'));
1✔
187
        foreach ($this->patchCollection->getPatchedPackages() as $package) {
1✔
188
            foreach ($this->patchCollection->getPatchesForPackage($package) as $patch) {
1✔
189
                $this->download($patch);
1✔
190
                $this->guessDepth($patch);
1✔
191
            }
192
        }
193
        $this->locker->setLockData($this->patchCollection);
1✔
194
    }
195

196
    /**
197
     * Load previously discovered patches from the Composer lock file.
198
     *
199
     * @param PackageEvent $event
200
     *   The event provided by Composer.
201
     */
202
    public function loadLockedPatches()
203
    {
204
        $locked = $this->locker->isLocked();
1✔
205
        if (!$locked) {
1✔
206
            $this->io->write('<warning>patches.lock does not exist. Creating a new patches.lock.</warning>');
1✔
207
            $this->createNewPatchesLock();
1✔
208
            return;
1✔
209
        }
210

211
        $this->patchCollection = PatchCollection::fromJson($this->locker->getLockData());
×
212
    }
213

214
    public function download(Patch $patch)
215
    {
216
        static $downloader;
1✔
217
        if (is_null($downloader)) {
1✔
218
            $downloader = new Downloader($this->composer, $this->io, $this->getConfig('disable-downloaders'));
1✔
219
        }
220

221
        $this->composer->getEventDispatcher()->dispatch(
1✔
222
            PatchEvents::PRE_PATCH_DOWNLOAD,
1✔
223
            new PatchEvent(PatchEvents::PRE_PATCH_DOWNLOAD, $patch)
1✔
224
        );
1✔
225
        $downloader->downloadPatch($patch);
1✔
226
        $this->composer->getEventDispatcher()->dispatch(
1✔
227
            PatchEvents::POST_PATCH_DOWNLOAD,
1✔
228
            new PatchEvent(PatchEvents::POST_PATCH_DOWNLOAD, $patch)
1✔
229
        );
1✔
230
    }
231

232
    public function guessDepth(Patch $patch)
233
    {
234
        $event = new PatchEvent(PatchEvents::PRE_PATCH_GUESS_DEPTH, $patch);
1✔
235
        $this->composer->getEventDispatcher()->dispatch(PatchEvents::PRE_PATCH_GUESS_DEPTH, $event);
1✔
236
        $patch = $event->getPatch();
1✔
237

238
        $depth = $patch->depth ??
1✔
239
            Util::getDefaultPackagePatchDepth($patch->package) ??
1✔
240
            $this->getConfig('default-patch-depth');
1✔
241
        $patch->depth = $depth;
1✔
242
    }
243

244
    public function apply(Patch $patch, string $install_path)
245
    {
246
        static $patcher;
1✔
247
        if (is_null($patcher)) {
1✔
248
            $patcher = new Patcher($this->composer, $this->io, $this->getConfig('disable-patchers'));
1✔
249
        }
250

251
        $this->guessDepth($patch);
1✔
252

253
        $event = new PatchEvent(PatchEvents::PRE_PATCH_APPLY, $patch);
1✔
254
        $this->composer->getEventDispatcher()->dispatch(PatchEvents::PRE_PATCH_APPLY, $event);
1✔
255
        $patch = $event->getPatch();
1✔
256

257
        $this->io->write(
1✔
258
            "      - Applying patch <info>{$patch->localPath}</info> (depth: {$patch->depth})",
1✔
259
            true,
1✔
260
            IOInterface::DEBUG
1✔
261
        );
1✔
262

263
        $status = $patcher->applyPatch($patch, $install_path);
1✔
264
        if ($status === false) {
1✔
265
            throw new Exception("No available patcher was able to apply patch {$patch->url} to {$patch->package}");
×
266
        }
267

268
        $this->composer->getEventDispatcher()->dispatch(
1✔
269
            PatchEvents::POST_PATCH_APPLY,
1✔
270
            new PatchEvent(PatchEvents::POST_PATCH_APPLY, $patch)
1✔
271
        );
1✔
272
    }
273

274

275
    /**
276
     * Download and apply patches.
277
     *
278
     * @param PackageEvent $event
279
     *   The event that Composer provided to us.
280
     */
281
    public function patchPackage(PackageEvent $event)
282
    {
283
        // Sometimes, patchPackage is called before a patch loading function (for instance, when composer-patches itself
284
        // is installed -- the pre-install event can't be invoked before this plugin is installed, but the post-install
285
        // event *can* be. Skipping composer-patches and composer-configurable-plugin ensures that this plugin and its
286
        // dependency won't cause an error to be thrown when attempting to read from an uninitialized PatchCollection.
287
        // This also means that neither composer-patches nor composer-configurable-plugin can have patches applied.
288
        $package = $this->getPackageFromOperation($event->getOperation());
1✔
289
        if (in_array($package->getName(), ['cweagans/composer-patches', 'cweagans/composer-configurable-plugin'])) {
1✔
290
            return;
1✔
291
        }
292

293
        // If there aren't any patches, there's nothing to do.
294
        if (empty($this->patchCollection->getPatchesForPackage($package->getName()))) {
1✔
295
            $this->io->write(
×
296
                "No patches found for <info>{$package->getName()}</info>",
×
297
                true,
×
298
                IOInterface::DEBUG,
×
299
            );
×
300
            return;
×
301
        }
302

303
        $install_path = $this->composer->getInstallationManager()
1✔
304
            ->getInstaller($package->getType())
1✔
305
            ->getInstallPath($package);
1✔
306

307
        $this->io->write("  - Patching <info>{$package->getName()}</info>");
1✔
308

309
        foreach ($this->patchCollection->getPatchesForPackage($package->getName()) as $patch) {
1✔
310
            /** @var $patch Patch */
311

312
            // Download patch.
313
            $this->io->write(
1✔
314
                "    - Downloading and applying patch <info>{$patch->url}</info> ({$patch->description})",
1✔
315
                true,
1✔
316
                IOInterface::VERBOSE
1✔
317
            );
1✔
318

319
            $this->io->write("      - Downloading patch <info>{$patch->url}</info>", true, IOInterface::DEBUG);
1✔
320

321
            $this->download($patch);
1✔
322
            $this->guessDepth($patch);
1✔
323

324
            // Apply patch.
325
            $this->io->write(
1✔
326
                "      - Applying downloaded patch <info>{$patch->localPath}</info>",
1✔
327
                true,
1✔
328
                IOInterface::DEBUG
1✔
329
            );
1✔
330

331
            $this->apply($patch, $install_path);
1✔
332
        }
333

334
        $this->io->write(
1✔
335
            "  - All patches for <info>{$package->getName()}</info> have been applied.",
1✔
336
            true,
1✔
337
            IOInterface::DEBUG
1✔
338
        );
1✔
339
    }
340

341
    /**
342
     * Get a Package object from an OperationInterface object.
343
     *
344
     * @param OperationInterface $operation
345
     * @return PackageInterface
346
     * @throws InvalidArgumentException
347
     */
348
    protected function getPackageFromOperation(OperationInterface $operation): PackageInterface
349
    {
350
        if ($operation instanceof InstallOperation) {
1✔
351
            $package = $operation->getPackage();
1✔
352
        } elseif ($operation instanceof UpdateOperation) {
×
353
            $package = $operation->getTargetPackage();
×
354
        } else {
355
            throw new InvalidArgumentException('Unknown operation: ' . get_class($operation));
×
356
        }
357

358
        return $package;
1✔
359
    }
360

361
    public function getLocker(): Locker
362
    {
363
        return $this->locker;
×
364
    }
365

366
    public function getLockFile(): JsonFile
367
    {
368
        return $this->lockFile;
×
369
    }
370

371
    public function getPatchCollection(): ?PatchCollection
372
    {
373
        return $this->patchCollection;
×
374
    }
375

376
    public function deactivate(Composer $composer, IOInterface $io)
377
    {
378
    }
×
379

380
    public function uninstall(Composer $composer, IOInterface $io)
381
    {
382
    }
×
383
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc