Issues (18)

src/Composer/Commands/PatchCommand.php (1 issue)

1
<?php
2
/**
3
 * Copyright © Vaimo Group. All rights reserved.
4
 * See LICENSE_VAIMO.txt for license details.
5
 */
6
namespace Vaimo\ComposerPatches\Composer\Commands;
7
8
use Symfony\Component\Console\Input\InputInterface;
9
use Symfony\Component\Console\Output\OutputInterface;
10
use Symfony\Component\Console\Input\InputOption;
11
use Symfony\Component\Console\Input\InputArgument;
12
13
use Composer\Script\ScriptEvents;
14
15
use Vaimo\ComposerPatches\Patch\Definition as Patch;
16
use Vaimo\ComposerPatches\Repository\PatchesApplier\ListResolvers;
17
use Vaimo\ComposerPatches\Config;
18
use Vaimo\ComposerPatches\Interfaces\ListResolverInterface;
19
use Vaimo\ComposerPatches\Composer\Plugin\Behaviour;
20
use Vaimo\ComposerPatches\Environment;
21
22
/**
23
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
24
 */
25
class PatchCommand extends \Composer\Command\BaseCommand
26
{
27
    protected function configure()
28
    {
29
        $this->setName('patch');
30
        $this->setDescription('Apply registered patches to current project');
31
32
        $this->addArgument(
33
            'targets',
34
            InputArgument::IS_ARRAY,
35
            'Packages for the patcher to target',
36
            array()
37
        );
38
39
        $this->addOption(
40
            '--redo',
41
            null,
42
            InputOption::VALUE_NONE,
43
            'Re-patch all packages or a specific package when targets defined'
44
        );
45
46
        $this->addOption(
47
            '--undo',
48
            null,
49
            InputOption::VALUE_NONE,
50
            'Remove all patches or a specific patch when targets defined'
51
        );
52
53
        $this->addOption(
54
            '--no-dev',
55
            null,
56
            InputOption::VALUE_NONE,
57
            'Disables installation of require-dev packages'
58
        );
59
60
        $this->addOption(
61
            '--filter',
62
            null,
63
            InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
64
            'Apply only those patch files/sources that match with provided filter'
65
        );
66
67
        $this->addOption(
68
            '--explicit',
69
            null,
70
            InputOption::VALUE_NONE,
71
            'Show information for every patch that gets re-applied (due to package reset)'
72
        );
73
74
        $this->addOption(
75
            '--show-reapplies',
76
            null,
77
            InputOption::VALUE_NONE,
78
            'Alias for \'explicit\' argument'
79
        );
80
81
        $this->addOption(
82
            '--from-source',
83
            null,
84
            InputOption::VALUE_NONE,
85
            'Apply patches based on information directly from packages in vendor folder'
86
        );
87
88
        $this->addOption(
89
            '--graceful',
90
            null,
91
            InputOption::VALUE_NONE,
92
            'Continue even when some patch fails to apply'
93
        );
94
95
        $this->addOption(
96
            '--force',
97
            null,
98
            InputOption::VALUE_NONE,
99
            'Force package reset even when it has local change'
100
        );
101
102
        $this->addOption(
103
            '--no-scripts',
104
            null,
105
            InputOption::VALUE_NONE,
106
            'Skips the execution of all scripts defined in composer.json file.'
107
        );
108
    }
109
110
    /**
111
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
112
     *
113
     * @param InputInterface $input
114
     * @param OutputInterface $output
115
     * @return int|void|null
116
     * @throws \Exception
117
     */
118
    protected function execute(InputInterface $input, OutputInterface $output)
119
    {
120
        $composer = $this->getComposer();
0 ignored issues
show
Deprecated Code introduced by
The function Composer\Command\BaseCommand::getComposer() has been deprecated: since Composer 2.3.0 use requireComposer or tryComposer depending on whether you have $required set to true or false ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

120
        $composer = /** @scrutinizer ignore-deprecated */ $this->getComposer();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
121
122
        $appIO = $this->getIO();
123
124
        $isDevMode = !$input->getOption('no-dev');
125
126
        $behaviourFlags = $this->getBehaviourFlags($input);
127
128
        $shouldUndo = !$behaviourFlags[Behaviour::REDO] && $behaviourFlags[Behaviour::UNDO];
129
130
        $contextFactory = new \Vaimo\ComposerPatches\Factories\ComposerContextFactory($composer);
131
        $composerContext = $contextFactory->create();
132
133
        $configFactory = new \Vaimo\ComposerPatches\Factories\ConfigFactory($composerContext, array(
134
            Config::PATCHER_FORCE_REAPPLY => $behaviourFlags[Behaviour::REDO],
135
            Config::PATCHER_FROM_SOURCE => (bool)$input->getOption('from-source'),
136
            Config::PATCHER_GRACEFUL => (bool)$input->getOption('graceful')
137
                || $behaviourFlags[Behaviour::REDO]
138
                || $behaviourFlags[Behaviour::UNDO],
139
            Config::PATCHER_SOURCES => $this->createSourcesEnablerConfig()
140
        ));
141
142
        $filters = $this->resolveActiveFilters($input, $behaviourFlags);
143
144
        $listResolver = $this->createListResolver($behaviourFlags, $filters);
145
        $this->configureEnvironmentForBehaviour($behaviourFlags);
146
147
        $outputTriggers = $this->resolveOutputTriggers($filters, $behaviourFlags);
148
        $bootstrapFactory = new \Vaimo\ComposerPatches\Factories\BootstrapFactory($composerContext, $appIO);
149
150
        $outputStrategy = new \Vaimo\ComposerPatches\Strategies\OutputStrategy($outputTriggers);
151
        $bootstrap = $bootstrapFactory->create($configFactory, $listResolver, $outputStrategy);
152
        $compatExecutor = new \Vaimo\ComposerPatches\Compatibility\Executor();
153
154
        $runtimeUtils = new \Vaimo\ComposerPatches\Utils\RuntimeUtils();
155
        $lockSanitizer = new \Vaimo\ComposerPatches\Repository\Lock\Sanitizer($appIO);
156
        $repository = $composer->getRepositoryManager()->getLocalRepository();
157
        $installationManager = $composer->getInstallationManager();
158
159
        $result = $runtimeUtils->executeWithPostAction(
160
            function () use ($shouldUndo, $filters, $bootstrap, $isDevMode) {
161
                if ($shouldUndo && !array_filter($filters)) {
162
                    $bootstrap->stripPatches($isDevMode);
163
164
                    return true;
165
                }
166
167
                return $bootstrap->applyPatches($isDevMode);
168
            },
169
            function () use ($installationManager, $repository, $lockSanitizer, $isDevMode, $compatExecutor) {
170
                $compatExecutor->repositoryWrite($repository, $installationManager, $isDevMode);
171
                $lockSanitizer->sanitize();
172
            }
173
        );
174
175
        if (!$input->getOption('no-scripts')) {
176
            $composer->getEventDispatcher()->dispatchScript(ScriptEvents::POST_INSTALL_CMD, $isDevMode);
177
        }
178
179
        return (int)!$result;
180
    }
181
182
    private function createSourcesEnablerConfig()
183
    {
184
        $configDefaults = new \Vaimo\ComposerPatches\Config\Defaults();
185
186
        $defaultValues = $configDefaults->getPatcherConfig();
187
188
        if (isset($defaultValues[Config::PATCHER_SOURCES]) && is_array($defaultValues[Config::PATCHER_SOURCES])) {
189
            $sourceTypes = array_keys((array)$defaultValues[Config::PATCHER_SOURCES]);
190
191
            return array_fill_keys($sourceTypes, true);
192
        }
193
194
        return array();
195
    }
196
197
    protected function getBehaviourFlags(InputInterface $input)
198
    {
199
        return array(
200
            Behaviour::REDO => $this->getOptionGraceful($input, 'redo'),
201
            Behaviour::UNDO => $this->getOptionGraceful($input, 'undo'),
202
            Behaviour::FORCE => $this->getOptionGraceful($input, 'force'),
203
            Behaviour::EXPLICIT => $this->getOptionGraceful($input, 'explicit')
204
                || $this->getOptionGraceful($input, 'show-reapplies')
205
        );
206
    }
207
208
    private function getOptionGraceful(InputInterface $input, $name)
209
    {
210
        return $input->hasOption($name) && $input->getOption($name);
211
    }
212
213
    private function resolveActiveFilters(InputInterface $input, array $behaviourFlags)
214
    {
215
        $filters = array(
216
            Patch::SOURCE => $input->getOption('filter'),
217
            Patch::TARGETS => $input->getArgument('targets')
218
        );
219
220
        $hasFilers = (bool)array_filter($filters);
221
222
        if (!$hasFilers && $behaviourFlags[Behaviour::REDO]) {
223
            $filters[Patch::SOURCE] = array('*');
224
        }
225
226
        return $filters;
227
    }
228
229
    private function configureEnvironmentForBehaviour(array $behaviourFlags)
230
    {
231
        $runtimeUtils = new \Vaimo\ComposerPatches\Utils\RuntimeUtils();
232
233
        $runtimeUtils->setEnvironmentValues(array(
234
            Environment::FORCE_RESET => (int)$behaviourFlags[Behaviour::FORCE]
235
        ));
236
    }
237
238
    private function resolveOutputTriggers(array $filters, array $behaviourFlags)
239
    {
240
        $hasFilers = (bool)array_filter($filters);
241
242
        $isExplicit = $behaviourFlags[Behaviour::EXPLICIT];
243
244
        if (!$hasFilers && $behaviourFlags[Behaviour::REDO]) {
245
            $isExplicit = true;
246
        }
247
248
        $outputTriggerFlags = array(
249
            Patch::STATUS_NEW => !$hasFilers,
250
            Patch::STATUS_CHANGED => !$hasFilers,
251
            Patch::STATUS_MATCH => true,
252
            Patch::SOURCE => $isExplicit,
253
            Patch::URL => $isExplicit
254
        );
255
256
        return array_keys(
257
            array_filter($outputTriggerFlags)
258
        );
259
    }
260
261
    private function createListResolver(array $behaviourFlags, array $filters)
262
    {
263
        $listResolver = new ListResolvers\FilteredListResolver($filters);
264
265
        $isDefaultBehaviour = !$behaviourFlags[Behaviour::REDO] && !$behaviourFlags[Behaviour::UNDO];
266
267
        $listResolver = $this->attachBehaviourToListResolver($listResolver, $behaviourFlags);
268
269
        if ($isDefaultBehaviour) {
270
            $listResolver = new ListResolvers\ChangesListResolver($listResolver);
271
        }
272
273
        return $listResolver;
274
    }
275
276
    private function attachBehaviourToListResolver(ListResolverInterface $listResolver, array $behaviourFlags)
277
    {
278
        $shouldUndo = !$behaviourFlags[Behaviour::REDO] && $behaviourFlags[Behaviour::UNDO];
279
280
        if ($shouldUndo) {
281
            return new ListResolvers\InvertedListResolver($listResolver);
282
        }
283
284
        return new ListResolvers\InclusiveListResolver($listResolver);
285
    }
286
}
287