Skip to content

Commit c092d87

Browse files
committed
feat: improve file copy command with error handling and path normalization
1 parent c721375 commit c092d87

1 file changed

Lines changed: 90 additions & 78 deletions

File tree

src/Console/Command/Theme/CopyFromVendorCommand.php

Lines changed: 90 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -43,91 +43,113 @@ protected function configure(): void
4343

4444
protected function executeCommand(InputInterface $input, OutputInterface $output): int
4545
{
46-
$sourceFile = $input->getArgument('file');
47-
$themeCode = $input->getArgument('theme');
46+
try {
47+
$sourceFileArg = $input->getArgument('file');
48+
$absoluteSourcePath = $this->getAbsoluteSourcePath($sourceFileArg);
49+
50+
// Update sourceFileArg if it was normalized to relative path
51+
$rootPath = $this->directoryList->getRoot();
52+
$sourceFile = str_starts_with($absoluteSourcePath, $rootPath . '/')
53+
? substr($absoluteSourcePath, strlen($rootPath) + 1)
54+
: $sourceFileArg;
55+
56+
$themeCode = $this->getThemeCode($input);
57+
$themePath = $this->getThemePath($themeCode);
58+
59+
$destinationPath = $this->vendorFileMapper->mapToThemePath($sourceFile, $themePath);
60+
$absoluteDestPath = $this->getAbsoluteDestPath($destinationPath, $rootPath);
61+
62+
if (!$this->confirmCopy($sourceFile, $absoluteDestPath, $rootPath)) {
63+
return Cli::RETURN_SUCCESS;
64+
}
65+
66+
$this->performCopy($absoluteSourcePath, $absoluteDestPath);
67+
$this->io->success("File copied successfully.");
68+
69+
return Cli::RETURN_SUCCESS;
70+
} catch (\Exception $e) {
71+
$this->io->error($e->getMessage());
72+
return Cli::RETURN_FAILURE;
73+
}
74+
}
4875

49-
// 1. Verify Source File
76+
private function getAbsoluteSourcePath(string $sourceFile): string
77+
{
5078
$rootPath = $this->directoryList->getRoot();
51-
// If absolute path provided
5279
if (str_starts_with($sourceFile, '/')) {
5380
$absoluteSourcePath = $sourceFile;
54-
// Make relative for display/proecessing
55-
if (str_starts_with($sourceFile, $rootPath . '/')) {
56-
$sourceFile = substr($sourceFile, strlen($rootPath) + 1);
57-
}
5881
} else {
59-
$absoluteSourcePath = $rootPath . '/' . $sourceFile;
82+
$absoluteSourcePath = $rootPath . '/' . $sourceFile;
6083
}
6184

6285
if (!file_exists($absoluteSourcePath)) {
63-
$this->io->error("Source file not found: $absoluteSourcePath");
64-
return Cli::RETURN_FAILURE;
86+
throw new \RuntimeException("Source file not found: $absoluteSourcePath");
6587
}
6688

67-
// 2. Select Theme if missing
68-
if (!$themeCode) {
69-
$themes = $this->themeList->getAllThemes();
70-
$options = [];
71-
foreach ($themes as $theme) {
72-
$options[$theme->getCode()] = $theme->getCode();
73-
}
89+
return $absoluteSourcePath;
90+
}
7491

75-
if (empty($options)) {
76-
$this->io->error('No themes found to copy to.');
77-
return Cli::RETURN_FAILURE;
78-
}
92+
private function getThemeCode(InputInterface $input): string
93+
{
94+
$themeCode = $input->getArgument('theme');
95+
if ($themeCode) {
96+
return $themeCode;
97+
}
7998

80-
// Fix Environment for DDEV (Required for Laravel Prompts)
81-
$this->fixPromptEnvironment();
82-
83-
$themeCode = search(
84-
label: 'Select target theme',
85-
options: fn (string $value) => array_filter(
86-
$options,
87-
fn ($option) => str_contains(strtolower($option), strtolower($value))
88-
),
89-
placeholder: 'Search for a theme...'
90-
);
99+
$themes = $this->themeList->getAllThemes();
100+
$options = [];
101+
foreach ($themes as $theme) {
102+
$options[$theme->getCode()] = $theme->getCode();
91103
}
92104

93-
// 3. Resolve Theme Path
105+
if (empty($options)) {
106+
throw new \RuntimeException('No themes found to copy to.');
107+
}
108+
109+
$this->fixPromptEnvironment();
110+
111+
return search(
112+
label: 'Select target theme',
113+
options: fn (string $value) => array_filter(
114+
$options,
115+
fn ($option) => str_contains(strtolower($option), strtolower($value))
116+
),
117+
placeholder: 'Search for a theme...'
118+
);
119+
}
120+
121+
private function getThemePath(string $themeCode): string
122+
{
94123
$theme = $this->themeList->getThemeByCode($themeCode);
95124
if (!$theme) {
96-
$this->io->error("Theme not found: $themeCode");
97-
return Cli::RETURN_FAILURE;
125+
throw new \RuntimeException("Theme not found: $themeCode");
98126
}
99127

100-
// Use ComponentRegistrar to get absolute path
101128
$regName = $theme->getArea() . '/' . $theme->getCode();
102129
$themePath = $this->componentRegistrar->getPath(ComponentRegistrar::THEME, $regName);
103130

104131
if (!$themePath) {
105-
// Fallback to model path if registrar fails
106-
$this->io->warning("Theme path not found via ComponentRegistrar for $regName, falling back to getFullPath()");
107-
$themePath = $theme->getFullPath();
132+
$this->io->warning("Theme path not found via ComponentRegistrar for $regName, falling back to getFullPath()");
133+
$themePath = $theme->getFullPath();
108134
}
109135

110-
// 4. Calculate Destination
111-
try {
112-
$destinationPath = $this->vendorFileMapper->mapToThemePath($sourceFile, $themePath);
113-
} catch (\Exception $e) {
114-
$this->io->error($e->getMessage());
115-
return Cli::RETURN_FAILURE;
116-
}
136+
return $themePath;
137+
}
117138

139+
private function getAbsoluteDestPath(string $destinationPath, string $rootPath): string
140+
{
118141
if (str_starts_with($destinationPath, '/')) {
119-
$absoluteDestPath = $destinationPath;
120-
} else {
121-
$absoluteDestPath = $rootPath . '/' . $destinationPath;
142+
return $destinationPath;
122143
}
144+
return $rootPath . '/' . $destinationPath;
145+
}
123146

124-
// Make destination relative for display if it's inside root
125-
$destinationDisplay = $absoluteDestPath;
126-
if (str_starts_with($absoluteDestPath, $rootPath . '/')) {
127-
$destinationDisplay = substr($absoluteDestPath, strlen($rootPath) + 1);
128-
}
147+
private function confirmCopy(string $sourceFile, string $absoluteDestPath, string $rootPath): bool
148+
{
149+
$destinationDisplay = str_starts_with($absoluteDestPath, $rootPath . '/')
150+
? substr($absoluteDestPath, strlen($rootPath) + 1)
151+
: $absoluteDestPath;
129152

130-
// 5. Preview & Confirm
131153
$this->io->section('Copy Preview');
132154
$this->io->text([
133155
"Source: <info>$sourceFile</info>",
@@ -138,31 +160,21 @@ protected function executeCommand(InputInterface $input, OutputInterface $output
138160

139161
if (file_exists($absoluteDestPath)) {
140162
$this->io->warning("File already exists at destination!");
141-
if (!$this->io->confirm('Overwrite existing file?', false)) {
142-
return Cli::RETURN_SUCCESS;
143-
}
144-
} else {
145-
if (!$this->io->confirm('Proceed with copy?', true)) {
146-
return Cli::RETURN_SUCCESS;
147-
}
163+
return $this->io->confirm('Overwrite existing file?', false);
148164
}
149165

150-
// 6. Perform Copy
151-
try {
152-
$directory = dirname($absoluteDestPath);
153-
if (!is_dir($directory)) {
154-
if (!mkdir($directory, 0777, true) && !is_dir($directory)) {
155-
throw new \RuntimeException(sprintf('Directory "%s" was not created', $directory));
156-
}
157-
}
158-
copy($absoluteSourcePath, $absoluteDestPath);
159-
$this->io->success("File copied successfully.");
160-
} catch (\Exception $e) {
161-
$this->io->error("Failed to copy file: " . $e->getMessage());
162-
return Cli::RETURN_FAILURE;
163-
}
166+
return $this->io->confirm('Proceed with copy?', true);
167+
}
164168

165-
return Cli::RETURN_SUCCESS;
169+
private function performCopy(string $absoluteSourcePath, string $absoluteDestPath): void
170+
{
171+
$directory = dirname($absoluteDestPath);
172+
if (!is_dir($directory)) {
173+
if (!mkdir($directory, 0777, true) && !is_dir($directory)) {
174+
throw new \RuntimeException(sprintf('Directory "%s" was not created', $directory));
175+
}
176+
}
177+
copy($absoluteSourcePath, $absoluteDestPath);
166178
}
167179

168180
private function fixPromptEnvironment(): void

0 commit comments

Comments
 (0)