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

cweagans / composer-patches / 4154978819

pending completion
4154978819

Pull #470

github

GitHub
Merge e317b8c14 into c4e8dabf3
Pull Request #470: Add composer and io to all events + emit event before throwing exception when patch does not apply

24 of 24 new or added lines in 6 files covered. (100.0%)

485 of 563 relevant lines covered (86.15%)

3.99 hits per line

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

86.71
/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
            'package-depths' => [
2✔
129
                'type' => 'list',
2✔
130
                'default' => [],
2✔
131
            ],
2✔
132
            'patches-file' => [
2✔
133
                'type' => 'string',
2✔
134
                'default' => '',
2✔
135
            ]
2✔
136
        ];
2✔
137
        $this->configure($this->composer->getPackage()->getExtra(), 'composer-patches');
2✔
138
    }
139

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

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

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

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

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

215
        $this->patchCollection = PatchCollection::fromJson($this->locker->getLockData());
×
216
    }
217

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

225
        $this->composer->getEventDispatcher()->dispatch(
1✔
226
            PatchEvents::PRE_PATCH_DOWNLOAD,
1✔
227
            new PatchEvent(PatchEvents::PRE_PATCH_DOWNLOAD, $patch, $this->composer, $this->io)
1✔
228
        );
1✔
229
        $downloader->downloadPatch($patch);
1✔
230
        $this->composer->getEventDispatcher()->dispatch(
1✔
231
            PatchEvents::POST_PATCH_DOWNLOAD,
1✔
232
            new PatchEvent(PatchEvents::POST_PATCH_DOWNLOAD, $patch, $this->composer, $this->io)
1✔
233
        );
1✔
234
    }
235

236
    public function guessDepth(Patch $patch)
237
    {
238
        $event = new PatchEvent(PatchEvents::PRE_PATCH_GUESS_DEPTH, $patch, $this->composer, $this->io);
5✔
239
        $this->composer->getEventDispatcher()->dispatch(PatchEvents::PRE_PATCH_GUESS_DEPTH, $event);
5✔
240
        $patch = $event->getPatch();
5✔
241

242
        $depth = $patch->depth ??
5✔
243
            $this->getConfig('package-depths')[$patch->package] ??
5✔
244
            Util::getDefaultPackagePatchDepth($patch->package) ??
5✔
245
            $this->getConfig('default-patch-depth');
4✔
246
        $patch->depth = $depth;
5✔
247
    }
248

249
    public function apply(Patch $patch, string $install_path)
250
    {
251
        static $patcher;
1✔
252
        if (is_null($patcher)) {
1✔
253
            $patcher = new Patcher($this->composer, $this->io, $this->getConfig('disable-patchers'));
1✔
254
        }
255

256
        $this->guessDepth($patch);
1✔
257

258
        $event = new PatchEvent(PatchEvents::PRE_PATCH_APPLY, $patch, $this->composer, $this->io);
1✔
259
        $this->composer->getEventDispatcher()->dispatch(PatchEvents::PRE_PATCH_APPLY, $event);
1✔
260
        $patch = $event->getPatch();
1✔
261

262
        $this->io->write(
1✔
263
            "      - Applying patch <info>{$patch->localPath}</info> (depth: {$patch->depth})",
1✔
264
            true,
1✔
265
            IOInterface::DEBUG
1✔
266
        );
1✔
267

268
        $status = $patcher->applyPatch($patch, $install_path);
1✔
269
        if ($status === false) {
1✔
270
            $e = new Exception("No available patcher was able to apply patch {$patch->url} to {$patch->package}");
×
271

272
            $this->composer->getEventDispatcher()->dispatch(
×
273
                PatchEvents::POST_PATCH_APPLY_ERROR,
×
274
                new PatchEvent(PatchEvents::POST_PATCH_APPLY_ERROR, $patch, $this->composer, $this->io, $e)
×
275
            );
×
276

277
            throw $e;
×
278
        }
279

280
        $this->composer->getEventDispatcher()->dispatch(
1✔
281
            PatchEvents::POST_PATCH_APPLY,
1✔
282
            new PatchEvent(PatchEvents::POST_PATCH_APPLY, $patch, $this->composer, $this->io)
1✔
283
        );
1✔
284
    }
285

286

287
    /**
288
     * Download and apply patches.
289
     *
290
     * @param PackageEvent $event
291
     *   The event that Composer provided to us.
292
     */
293
    public function patchPackage(PackageEvent $event)
294
    {
295
        // Sometimes, patchPackage is called before a patch loading function (for instance, when composer-patches itself
296
        // is installed -- the pre-install event can't be invoked before this plugin is installed, but the post-install
297
        // event *can* be. Skipping composer-patches and composer-configurable-plugin ensures that this plugin and its
298
        // dependency won't cause an error to be thrown when attempting to read from an uninitialized PatchCollection.
299
        // This also means that neither composer-patches nor composer-configurable-plugin can have patches applied.
300
        $package = $this->getPackageFromOperation($event->getOperation());
1✔
301
        if (in_array($package->getName(), ['cweagans/composer-patches', 'cweagans/composer-configurable-plugin'])) {
1✔
302
            return;
1✔
303
        }
304

305
        // If there aren't any patches, there's nothing to do.
306
        if (empty($this->patchCollection->getPatchesForPackage($package->getName()))) {
1✔
307
            $this->io->write(
×
308
                "No patches found for <info>{$package->getName()}</info>",
×
309
                true,
×
310
                IOInterface::DEBUG,
×
311
            );
×
312
            return;
×
313
        }
314

315
        $install_path = $this->composer->getInstallationManager()
1✔
316
            ->getInstaller($package->getType())
1✔
317
            ->getInstallPath($package);
1✔
318

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

321
        foreach ($this->patchCollection->getPatchesForPackage($package->getName()) as $patch) {
1✔
322
            /** @var $patch Patch */
323

324
            // Download patch.
325
            $this->io->write(
1✔
326
                "    - Downloading and applying patch <info>{$patch->url}</info> ({$patch->description})",
1✔
327
                true,
1✔
328
                IOInterface::VERBOSE
1✔
329
            );
1✔
330

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

333
            $this->download($patch);
1✔
334
            $this->guessDepth($patch);
1✔
335

336
            // Apply patch.
337
            $this->io->write(
1✔
338
                "      - Applying downloaded patch <info>{$patch->localPath}</info>",
1✔
339
                true,
1✔
340
                IOInterface::DEBUG
1✔
341
            );
1✔
342

343
            $this->apply($patch, $install_path);
1✔
344
        }
345

346
        $this->io->write(
1✔
347
            "  - All patches for <info>{$package->getName()}</info> have been applied.",
1✔
348
            true,
1✔
349
            IOInterface::DEBUG
1✔
350
        );
1✔
351
    }
352

353
    /**
354
     * Get a Package object from an OperationInterface object.
355
     *
356
     * @param OperationInterface $operation
357
     * @return PackageInterface
358
     * @throws InvalidArgumentException
359
     */
360
    protected function getPackageFromOperation(OperationInterface $operation): PackageInterface
361
    {
362
        if ($operation instanceof InstallOperation) {
1✔
363
            $package = $operation->getPackage();
1✔
364
        } elseif ($operation instanceof UpdateOperation) {
×
365
            $package = $operation->getTargetPackage();
×
366
        } else {
367
            throw new InvalidArgumentException('Unknown operation: ' . get_class($operation));
×
368
        }
369

370
        return $package;
1✔
371
    }
372

373
    public function getLocker(): Locker
374
    {
375
        return $this->locker;
×
376
    }
377

378
    public function getLockFile(): JsonFile
379
    {
380
        return $this->lockFile;
×
381
    }
382

383
    public function getPatchCollection(): ?PatchCollection
384
    {
385
        return $this->patchCollection;
×
386
    }
387

388
    public function deactivate(Composer $composer, IOInterface $io)
389
    {
390
    }
×
391

392
    public function uninstall(Composer $composer, IOInterface $io)
393
    {
394
    }
×
395
}
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

© 2025 Coveralls, Inc