-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathAbstractCommand.php
More file actions
255 lines (222 loc) · 8.95 KB
/
AbstractCommand.php
File metadata and controls
255 lines (222 loc) · 8.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
<?php
declare(strict_types=1);
/**
* This file is part of fast-forward/dev-tools.
*
* This source file is subject to the license bundled
* with this source code in the file LICENSE.
*
* @copyright Copyright (c) 2026 Felipe Sayão Lobato Abreu <github@mentordosnerds.com>
* @license https://opensource.org/licenses/MIT MIT License
*
* @see https://github.com/php-fast-forward/dev-tools
* @see https://github.com/php-fast-forward
* @see https://datatracker.ietf.org/doc/html/rfc2119
*/
namespace FastForward\DevTools\Command;
use RuntimeException;
use Symfony\Component\Console\Helper\ProcessHelper;
use Composer\Command\BaseCommand;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Filesystem\Path;
use Symfony\Component\Process\Process;
use function Safe\getcwd;
/**
* Provides a base configuration and common utilities for Composer commands.
* Extending classes MUST rely on this base abstraction to interact with the console
* application gracefully, and SHALL adhere to the expected return types for commands.
*/
abstract class AbstractCommand extends BaseCommand
{
/**
* @var Filesystem The filesystem instance used for file operations. This property MUST be utilized for interacting with the file system securely.
*/
protected readonly Filesystem $filesystem;
/**
* Constructs a new AbstractCommand instance.
*
* The method MAY accept a Filesystem instance; if omitted, it SHALL instantiate a new one.
*
* @param Filesystem|null $filesystem the filesystem utility to use
*/
public function __construct(?Filesystem $filesystem = null)
{
$this->filesystem = $filesystem ?? new Filesystem();
parent::__construct();
}
/**
* Executes a given system process gracefully and outputs its buffer.
*
* The method MUST execute the provided command ensuring the output is channeled
* to the OutputInterface. It SHOULD leverage TTY if supported. If the process
* fails, it MUST return `self::FAILURE`; otherwise, it SHALL return `self::SUCCESS`.
*
* @param Process $command the configured process instance to run
* @param OutputInterface $output the output interface to log warnings or results
*
* @return int the status code of the command execution
*/
protected function runProcess(Process $command, OutputInterface $output): int
{
/** @var ProcessHelper $processHelper */
$processHelper = $this->getHelper('process');
$command = $command->setWorkingDirectory($this->getCurrentWorkingDirectory());
$callback = null;
if (Process::isTtySupported()) {
$command->setTty(true);
} else {
$output->writeln(
'<comment>Warning: TTY is not supported. The command may not display output as expected.</comment>'
);
$callback = function (string $type, string $buffer) use ($output): void {
$output->write($buffer);
};
}
$process = $processHelper->run(output: $output, cmd: $command, callback: $callback);
if (! $process->isSuccessful()) {
$output->writeln(\sprintf(
'<error>Command "%s" failed with exit code %d. Please check the output above for details.</error>',
$command->getCommandLine(),
$command->getExitCode()
));
return self::FAILURE;
}
return self::SUCCESS;
}
/**
* Retrieves the current working directory of the application.
*
* The method MUST return the initial working directory defined by the application.
* If not available, it SHALL fall back to the safe current working directory.
*
* @return string the absolute path to the current working directory
*/
protected function getCurrentWorkingDirectory(): string
{
try {
return $this->getApplication()
->getInitialWorkingDirectory() ?: getcwd();
} catch (RuntimeException) {
return getcwd();
}
}
/**
* Computes the absolute path for a given relative or absolute path.
*
* This method MUST return the exact path if it is already absolute.
* If relative, it SHALL make it absolute relying on the current working directory.
*
* @param string $relativePath the path to evaluate or resolve
*
* @return string the resolved absolute path
*/
protected function getAbsolutePath(string $relativePath): string
{
if ($this->filesystem->isAbsolutePath($relativePath)) {
return $relativePath;
}
return Path::makeAbsolute($relativePath, $this->getCurrentWorkingDirectory());
}
/**
* Determines the correct absolute path to a configuration file.
*
* The method MUST attempt to resolve the configuration file locally in the working directory.
* If absent and not forced, it SHALL provide the default equivalent from the package itself.
*
* @param string $filename the name of the configuration file
* @param bool $force determines whether to bypass fallback and forcefully return the local file path
*
* @return string the resolved absolute path to the configuration file
*/
protected function getConfigFile(string $filename, bool $force = false): string
{
$rootPackagePath = $this->getCurrentWorkingDirectory();
if ($force || $this->filesystem->exists($rootPackagePath . '/' . $filename)) {
return Path::makeAbsolute($filename, $rootPackagePath);
}
return $this->getDevToolsFile($filename);
}
/**
* Resolves the absolute path to a file within the fast-forward/dev-tools package.
*
* This method uses Composer's InstalledVersions to determine the installation path of the
* fast-forward/dev-tools package and returns the absolute path to the given filename within it.
* It is used as a fallback when a configuration file is not found in the project root.
*
* @param string $filename the name of the file to resolve within the dev-tools package
*
* @return string the absolute path to the file inside the dev-tools package
*/
protected function getDevToolsFile(string $filename): string
{
return Path::makeAbsolute($filename, \dirname(__DIR__, 2));
}
/**
* Configures and executes a registered console command by name.
*
* The method MUST look up the command from the application and run it. It SHALL ignore generic
* validation errors and route the custom input and output correctly.
*
* @param string $commandName the name of the required command
* @param array|InputInterface $input the input arguments or array definition
* @param OutputInterface $output the interface for buffering output
*
* @return int the status code resulting from the dispatched command
*/
protected function runCommand(string $commandName, array|InputInterface $input, OutputInterface $output): int
{
$application = $this->getApplication();
$command = $application->find($commandName);
$command->ignoreValidationErrors();
if (\is_array($input)) {
$input = new ArrayInput($input);
}
return $command->run($input, $output);
}
/**
* Retrieves configured PSR-4 namespaces from the composer configuration.
*
* This method SHALL parse the underlying `composer.json` using the Composer instance,
* and MUST provide an empty array if no specific paths exist.
*
* @return array the PSR-4 namespaces mappings
*/
protected function getPsr4Namespaces(): array
{
$composer = $this->requireComposer();
$autoload = $composer->getPackage()
->getAutoload();
return $autoload['psr-4'] ?? [];
}
/**
* Computes the human-readable title or description of the current application.
*
* The method SHOULD utilize the package description as the title, but MUST provide
* the raw package name as a fallback mechanism.
*
* @return string the computed title or description string
*/
protected function getProjectName(): string
{
$composer = $this->requireComposer();
$package = $composer->getPackage();
return $package->getName();
}
/**
* Computes the human-readable description of the current application.
*
* The method SHOULD utilize the package description as the title, but MUST provide
* the raw package name as a fallback mechanism.
*
* @return string the computed title or description string
*/
protected function getProjectDescription(): string
{
$composer = $this->requireComposer();
$package = $composer->getPackage();
return $package->getDescription();
}
}