@@ -138,55 +138,195 @@ public function getSshOptions(array $config): string
138138
139139 public function fetchRemoteDbConfig (array $ sshConfig , string $ sshOptions , SymfonyStyle $ io ): bool
140140 {
141- $ io ->text ('Reading remote .env file ... ' );
141+ $ io ->text ('Reading remote environment configuration ... ' );
142142
143- $ envPath = rtrim ($ sshConfig ['project_path ' ], '/ ' ) . ' /.env ' ;
143+ $ projectPath = rtrim ($ sshConfig ['project_path ' ], '/ ' );
144144
145+ // Priority: .env.local.php > .env.local > .env
146+ // .env.local.php is a cached PHP array that takes full precedence
147+ // Otherwise, .env is loaded first, then .env.local overrides it
148+
149+ $ envLocalPhpPath = $ projectPath . '/.env.local.php ' ;
150+ $ envLocalPath = $ projectPath . '/.env.local ' ;
151+ $ envPath = $ projectPath . '/.env ' ;
152+
153+ // First, check if .env.local.php exists (cached env)
154+ $ checkCommand = sprintf (
155+ 'if [ -f %s ]; then echo "env.local.php"; elif [ -f %s ]; then echo "env.local"; elif [ -f %s ]; then echo "env"; else echo "none"; fi ' ,
156+ escapeshellarg ($ envLocalPhpPath ),
157+ escapeshellarg ($ envLocalPath ),
158+ escapeshellarg ($ envPath )
159+ );
160+
145161 $ sshCommand = sprintf (
146162 'ssh %s %s@%s %s ' ,
147163 $ sshOptions ,
148164 escapeshellarg ($ sshConfig ['user ' ]),
149165 escapeshellarg ($ sshConfig ['host ' ]),
150- escapeshellarg (' cat ' . escapeshellarg ( $ envPath ) )
166+ escapeshellarg ($ checkCommand )
151167 );
152168
153169 $ process = Process::fromShellCommandline ($ sshCommand );
154170 $ process ->setTimeout (30 );
155171 $ process ->run ();
156172
157173 if (!$ process ->isSuccessful ()) {
158- $ errorOutput = $ process ->getErrorOutput ();
159-
160- if (str_contains ($ errorOutput , 'Permission denied ' ) ||
161- str_contains ($ errorOutput , 'Host key verification failed ' ) ||
162- str_contains ($ errorOutput , 'Connection refused ' )) {
163- $ io ->error ('SSH connection failed! ' );
164- $ io ->text ('' );
165- $ io ->text ('If running inside DDEV, first run: <comment>ddev auth ssh</comment> ' );
166- $ io ->text ('This forwards your SSH agent to the container. ' );
167- $ io ->text ('' );
168- $ io ->text ('Other options: ' );
169- $ io ->text (' - Configure SSH key in plugin settings ' );
170- $ io ->text (' - Ensure SSH key is loaded in your SSH agent ' );
171- } else {
172- $ io ->error ('Failed to read remote .env file! ' );
173- $ io ->text ($ errorOutput );
174- }
174+ $ this ->handleSshError ($ process ->getErrorOutput (), $ io );
175+ return false ;
176+ }
177+
178+ $ availableEnvFile = trim ($ process ->getOutput ());
179+
180+ if ($ availableEnvFile === 'none ' ) {
181+ $ io ->error ('No .env file found on remote server. ' );
182+ $ io ->text ('Checked: .env.local.php, .env.local, .env ' );
183+ return false ;
184+ }
185+
186+ // Handle .env.local.php (PHP array format)
187+ if ($ availableEnvFile === 'env.local.php ' ) {
188+ $ io ->text (' Found .env.local.php (cached environment) ' );
189+ return $ this ->fetchRemoteEnvFromPhp ($ sshConfig , $ sshOptions , $ envLocalPhpPath , $ io );
190+ }
191+
192+ // For .env and .env.local, we need to merge them
193+ // First read .env as base
194+ $ io ->text (' Reading .env... ' );
195+ $ envContent = $ this ->fetchRemoteFile ($ sshConfig , $ sshOptions , $ envPath , $ io );
196+
197+ if ($ envContent === null ) {
175198 return false ;
176199 }
177200
178- // Parse the .env content
179- $ envContent = $ process ->getOutput ();
180201 $ this ->remoteDbConfig = $ this ->parseEnvContent ($ envContent );
181202
203+ // Then check if .env.local exists and override
204+ $ checkLocalCommand = sprintf ('[ -f %s ] && echo "exists" ' , escapeshellarg ($ envLocalPath ));
205+ $ sshCommand = sprintf (
206+ 'ssh %s %s@%s %s ' ,
207+ $ sshOptions ,
208+ escapeshellarg ($ sshConfig ['user ' ]),
209+ escapeshellarg ($ sshConfig ['host ' ]),
210+ escapeshellarg ($ checkLocalCommand )
211+ );
212+
213+ $ process = Process::fromShellCommandline ($ sshCommand );
214+ $ process ->setTimeout (30 );
215+ $ process ->run ();
216+
217+ if (trim ($ process ->getOutput ()) === 'exists ' ) {
218+ $ io ->text (' Reading .env.local (overrides)... ' );
219+ $ envLocalContent = $ this ->fetchRemoteFile ($ sshConfig , $ sshOptions , $ envLocalPath , $ io );
220+
221+ if ($ envLocalContent !== null ) {
222+ // Parse .env.local and merge (override) into base config
223+ $ localConfig = $ this ->parseEnvContent ($ envLocalContent );
224+ $ this ->remoteDbConfig = array_merge ($ this ->remoteDbConfig , array_filter ($ localConfig ));
225+ }
226+ }
227+
182228 if (empty ($ this ->remoteDbConfig ['name ' ])) {
183- $ io ->error ('Could not find DATABASE_NAME or DATABASE_URL in remote .env file . ' );
229+ $ io ->error ('Could not find DATABASE_NAME or DATABASE_URL in remote environment files . ' );
184230 return false ;
185231 }
186232
187233 return true ;
188234 }
189235
236+ private function fetchRemoteFile (array $ sshConfig , string $ sshOptions , string $ filePath , SymfonyStyle $ io ): ?string
237+ {
238+ $ sshCommand = sprintf (
239+ 'ssh %s %s@%s %s ' ,
240+ $ sshOptions ,
241+ escapeshellarg ($ sshConfig ['user ' ]),
242+ escapeshellarg ($ sshConfig ['host ' ]),
243+ escapeshellarg ('cat ' . escapeshellarg ($ filePath ))
244+ );
245+
246+ $ process = Process::fromShellCommandline ($ sshCommand );
247+ $ process ->setTimeout (30 );
248+ $ process ->run ();
249+
250+ if (!$ process ->isSuccessful ()) {
251+ $ this ->handleSshError ($ process ->getErrorOutput (), $ io );
252+ return null ;
253+ }
254+
255+ return $ process ->getOutput ();
256+ }
257+
258+ private function fetchRemoteEnvFromPhp (array $ sshConfig , string $ sshOptions , string $ filePath , SymfonyStyle $ io ): bool
259+ {
260+ // .env.local.php returns a PHP array, we need to execute it and extract DATABASE_URL
261+ $ phpCommand = sprintf (
262+ 'php -r %s ' ,
263+ escapeshellarg (sprintf (
264+ '$env = require %s; echo $env["DATABASE_URL"] ?? ""; ' ,
265+ escapeshellarg ($ filePath )
266+ ))
267+ );
268+
269+ $ sshCommand = sprintf (
270+ 'ssh %s %s@%s %s ' ,
271+ $ sshOptions ,
272+ escapeshellarg ($ sshConfig ['user ' ]),
273+ escapeshellarg ($ sshConfig ['host ' ]),
274+ escapeshellarg ($ phpCommand )
275+ );
276+
277+ $ process = Process::fromShellCommandline ($ sshCommand );
278+ $ process ->setTimeout (30 );
279+ $ process ->run ();
280+
281+ if (!$ process ->isSuccessful ()) {
282+ $ this ->handleSshError ($ process ->getErrorOutput (), $ io );
283+ return false ;
284+ }
285+
286+ $ databaseUrl = trim ($ process ->getOutput ());
287+
288+ if (empty ($ databaseUrl )) {
289+ $ io ->error ('Could not find DATABASE_URL in .env.local.php ' );
290+ return false ;
291+ }
292+
293+ $ parsed = $ this ->parseDatabaseUrl ($ databaseUrl );
294+
295+ if (!$ parsed ) {
296+ $ io ->error ('Could not parse DATABASE_URL from .env.local.php ' );
297+ return false ;
298+ }
299+
300+ $ this ->remoteDbConfig = [
301+ 'host ' => $ parsed ['host ' ] ?? 'localhost ' ,
302+ 'port ' => $ parsed ['port ' ] ?? 3306 ,
303+ 'name ' => $ parsed ['name ' ] ?? '' ,
304+ 'user ' => $ parsed ['user ' ] ?? '' ,
305+ 'pass ' => $ parsed ['pass ' ] ?? '' ,
306+ ];
307+
308+ return true ;
309+ }
310+
311+ private function handleSshError (string $ errorOutput , SymfonyStyle $ io ): void
312+ {
313+ if (str_contains ($ errorOutput , 'Permission denied ' ) ||
314+ str_contains ($ errorOutput , 'Host key verification failed ' ) ||
315+ str_contains ($ errorOutput , 'Connection refused ' )) {
316+ $ io ->error ('SSH connection failed! ' );
317+ $ io ->text ('' );
318+ $ io ->text ('If running inside DDEV, first run: <comment>ddev auth ssh</comment> ' );
319+ $ io ->text ('This forwards your SSH agent to the container. ' );
320+ $ io ->text ('' );
321+ $ io ->text ('Other options: ' );
322+ $ io ->text (' - Configure SSH key in plugin settings ' );
323+ $ io ->text (' - Ensure SSH key is loaded in your SSH agent ' );
324+ } else {
325+ $ io ->error ('Failed to read remote file! ' );
326+ $ io ->text ($ errorOutput );
327+ }
328+ }
329+
190330 public function getRemoteDbConfig (): ?array
191331 {
192332 return $ this ->remoteDbConfig ;
@@ -336,7 +476,7 @@ public function createRemoteDump(
336476 $ baseDumpFileEsc
337477 );
338478
339- // Combine both commands
479+ // Combine both commands: structure dump, then data dump
340480 $ combinedCommand = sprintf ('%s && %s ' , $ structureCommand , $ dataCommand );
341481
342482 // Add gzip if requested
@@ -435,7 +575,6 @@ public function importDump(
435575 SymfonyStyle $ io
436576 ): bool {
437577 $ io ->text ('Importing database into local environment... ' );
438- $ io ->text ('Stripping DEFINER statements... ' );
439578
440579 $ mysqlArgs = [
441580 'mysql ' ,
@@ -450,29 +589,25 @@ public function importDump(
450589 // This prevents deadlocks and speeds up the import significantly
451590 $ initCommands = 'SET FOREIGN_KEY_CHECKS=0; SET UNIQUE_CHECKS=0; SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO"; ' ;
452591
453- // Strip DEFINER statements locally before import to avoid privilege errors
454- // This handles views, stored procedures, triggers, and events in all MySQL comment formats
592+ // DEFINER statements are already stripped during remote dump creation
593+ // so we just need to decompress (if gzip) and pipe to mysql
455594 if ($ useGzip ) {
456595 $ importCommand = sprintf (
457- '(echo %s; gunzip -c %s | LANG=C LC_CTYPE=C LC_ALL=C sed -e %s ; echo "SET FOREIGN_KEY_CHECKS=1; SET UNIQUE_CHECKS=1;") | %s ' ,
596+ '(echo %s; gunzip -c %s; echo "SET FOREIGN_KEY_CHECKS=1; SET UNIQUE_CHECKS=1;") | %s ' ,
458597 escapeshellarg ($ initCommands ),
459598 escapeshellarg ($ localDumpFile ),
460- escapeshellarg ('s/\/\*![0-9]*\s*DEFINER=[^*]*\*\///g ' ),
461599 implode (' ' , array_map ('escapeshellarg ' , $ mysqlArgs ))
462600 );
463- $ process = Process::fromShellCommandline ($ importCommand );
464601 } else {
465- // For non-gzipped files, use sed to strip DEFINER before piping to mysql
466602 $ importCommand = sprintf (
467- '(echo %s; LANG=C LC_CTYPE=C LC_ALL=C sed -e %s %s; echo "SET FOREIGN_KEY_CHECKS=1; SET UNIQUE_CHECKS=1;") | %s ' ,
603+ '(echo %s; cat %s; echo "SET FOREIGN_KEY_CHECKS=1; SET UNIQUE_CHECKS=1;") | %s ' ,
468604 escapeshellarg ($ initCommands ),
469- escapeshellarg ('s/\/\*![0-9]*\s*DEFINER=[^*]*\*\///g ' ),
470605 escapeshellarg ($ localDumpFile ),
471606 implode (' ' , array_map ('escapeshellarg ' , $ mysqlArgs ))
472607 );
473- $ process = Process::fromShellCommandline ($ importCommand );
474608 }
475609
610+ $ process = Process::fromShellCommandline ($ importCommand );
476611 $ process ->setTimeout (600 ); // 10 minutes
477612 $ process ->run ();
478613
0 commit comments