Skip to content

Commit d60fa13

Browse files
committed
Release 1.0.1
1 parent 7b8d1fe commit d60fa13

4 files changed

Lines changed: 184 additions & 50 deletions

File tree

README.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ bin/console sidworks:db:sync production
210210

211211
| Option | Description |
212212
|--------|-------------|
213-
| `--keep-dump, -k` | Keep the dump file in `var/dumps/` after import |
213+
| `--keep-dump, -k` | Keep the dump file in the project root after import |
214214
| `--skip-import` | Only download the dump, don't import |
215215
| `--no-gzip` | Don't compress the dump (faster for small databases) |
216216
| `--skip-overrides` | Skip applying local environment overrides |
@@ -264,7 +264,7 @@ ddev exec bin/console sidworks:db:sync staging
264264
### Execution Flow
265265

266266
1. **Validate configuration** — Check required SSH and environment settings
267-
2. **Fetch remote `.env`** — Read database credentials from the remote server via SSH
267+
2. **Fetch remote environment** — Read database credentials from the remote server via SSH (supports `.env.local.php`, `.env.local`, and `.env`)
268268
3. **Create remote dump** — Two-step mysqldump (structure + data) on the remote server
269269
4. **Download dump** — Transfer the compressed dump via rsync
270270
5. **Cleanup remote** — Delete the dump file from the remote server
@@ -321,12 +321,20 @@ The import pipeline applies several optimizations:
321321
Your SSH user needs:
322322
- Read access to the remote `.env` file
323323
- Execute permissions for `mysqldump`
324-
- Write permissions to `/tmp` on the remote server
324+
- Write permissions to the remote project directory (for temporary dump file)
325325

326326
### Remote .env Not Found
327327

328328
Verify that `SW_DB_SYNC_[ENV]_PROJECT_PATH` points to the Shopware root directory containing the `.env` file.
329329

330+
The plugin reads remote environment files in this priority order:
331+
332+
1. **`.env.local.php`** — Cached PHP array (takes full precedence if exists)
333+
2. **`.env.local`** — Local overrides (merged on top of `.env`)
334+
3. **`.env`** — Base environment file
335+
336+
This matches Symfony's environment loading behavior.
337+
330338
### Import Fails with DEFINER Errors
331339

332340
This is handled automatically. Both the remote dump and local import pipeline strip DEFINER clauses. If you still encounter errors, check that your MySQL user has `SUPER` or `PROXY` privileges, or verify the dump file isn't corrupted.

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "sidworks/sw-plugin-database-sync",
33
"description": "Sidworks Plugin Database Sync",
44
"type": "shopware-platform-plugin",
5-
"version": "1.0.0",
5+
"version": "1.0.1",
66
"require": {
77
"shopware/core": "~6.6.0 || ~6.7.0"
88
},

src/Command/DatabaseSyncCommand.php

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -141,19 +141,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int
141141
return Command::SUCCESS;
142142
}
143143

144-
// Create dumps directory
145-
$dumpDir = $this->syncService->getProjectDir() . '/var/dumps';
146-
if (!is_dir($dumpDir)) {
147-
if (!@mkdir($dumpDir, 0775, true) && !is_dir($dumpDir)) {
148-
$io->error('Could not create dumps directory: ' . $dumpDir);
149-
$io->text('Please create it manually: mkdir -p var/dumps && chmod 775 var/dumps');
150-
return Command::FAILURE;
151-
}
152-
}
153-
154144
$timestamp = date('Y-m-d_His');
155-
$remoteDumpFile = '/tmp/shopware_sync_' . $timestamp . '.sql' . ($useGzip ? '.gz' : '');
156-
$localDumpFile = $dumpDir . '/sync_' . $environment . '_' . $timestamp . '.sql' . ($useGzip ? '.gz' : '');
145+
$dumpFileName = 'sync_' . $environment . '_' . $timestamp . '.sql' . ($useGzip ? '.gz' : '');
146+
$remoteDumpFile = rtrim($sshConfig['project_path'], '/') . '/' . $dumpFileName;
147+
$localDumpFile = $this->syncService->getProjectDir() . '/' . $dumpFileName;
157148

158149
// Step 1: Create dump on remote server
159150
$io->section('Step 1/4: Creating remote dump');

src/Service/DatabaseSyncService.php

Lines changed: 169 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)