├── src ├── Compressors │ ├── Compressor.php │ ├── GzipCompressor.php │ └── Bzip2Compressor.php ├── Exceptions │ ├── CannotStartDump.php │ ├── InvalidDatabaseUrl.php │ ├── CannotSetParameter.php │ └── DumpFailed.php ├── Databases │ ├── MariaDb.php │ ├── Sqlite.php │ ├── MongoDb.php │ ├── PostgreSql.php │ └── MySql.php ├── DsnParser.php └── DbDumper.php ├── LICENSE.md ├── composer.json └── README.md /src/Compressors/Compressor.php: -------------------------------------------------------------------------------- 1 | determineQuote(); 12 | 13 | $command = [ 14 | "{$quote}{$this->dumpBinaryPath}mariadb-dump{$quote}", 15 | "--defaults-extra-file=\"{$temporaryCredentialsFile}\"", 16 | ]; 17 | 18 | $finalDumpCommand = $this->getCommonDumpCommand($command) . $this->determineSandboxMode(); 19 | 20 | return $this->echoToFile($finalDumpCommand, $dumpFile); 21 | } 22 | 23 | public function withoutSandboxMode(): self 24 | { 25 | $this->withSandboxMode = false; 26 | 27 | return $this; 28 | } 29 | 30 | public function determineSandboxMode(): string 31 | { 32 | // allow mariadb/MySQL compatability: https://mariadb.org/mariadb-dump-file-compatibility-change/ 33 | return $this->withSandboxMode ? '' : '|tail +2'; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Spatie bvba 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spatie/db-dumper", 3 | "description": "Dump databases", 4 | "keywords": [ 5 | "spatie", 6 | "dump", 7 | "database", 8 | "mysqldump", 9 | "db-dumper" 10 | ], 11 | "homepage": "https://github.com/spatie/db-dumper", 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Freek Van der Herten", 16 | "email": "freek@spatie.be", 17 | "homepage": "https://spatie.be", 18 | "role": "Developer" 19 | } 20 | ], 21 | "require": { 22 | "php": "^8.0", 23 | "symfony/process": "^5.0|^6.0|^7.0|^8.0" 24 | }, 25 | "require-dev": { 26 | "pestphp/pest": "^1.22" 27 | }, 28 | "autoload": { 29 | "psr-4": { 30 | "Spatie\\DbDumper\\": "src" 31 | } 32 | }, 33 | "autoload-dev": { 34 | "psr-4": { 35 | "Spatie\\DbDumper\\Test\\": "tests" 36 | } 37 | }, 38 | "scripts": { 39 | "test": "vendor/bin/pest" 40 | }, 41 | "config": { 42 | "allow-plugins": { 43 | "pestphp/pest-plugin": true 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Exceptions/DumpFailed.php: -------------------------------------------------------------------------------- 1 | getOutput() ?: ''; 34 | $errorOutput = $process->getErrorOutput() ?: ''; 35 | $exitCodeText = $process->getExitCodeText() ?: ''; 36 | 37 | return <<getExitCode()}: {$exitCodeText} 42 | 43 | Output 44 | ====== 45 | {$output} 46 | 47 | Error Output 48 | ============ 49 | {$errorOutput} 50 | CONSOLE; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Databases/Sqlite.php: -------------------------------------------------------------------------------- 1 | getProcess($dumpFile); 14 | 15 | $process->run(); 16 | 17 | $this->checkIfDumpWasSuccessFul($process, $dumpFile); 18 | } 19 | 20 | public function getDbTables(): array 21 | { 22 | $db = new SQLite3($this->dbName); 23 | $query = $db->query("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';"); 24 | $tables = []; 25 | while ($table = $query->fetchArray(SQLITE3_ASSOC)) { 26 | $tables[] = $table['name']; 27 | } 28 | $db->close(); 29 | 30 | return $tables; 31 | } 32 | 33 | public function getDumpCommand(string $dumpFile): string 34 | { 35 | $includeTables = rtrim(' ' . implode(' ', $this->includeTables)); 36 | if (empty($includeTables) && ! empty($this->excludeTables)) { 37 | $tables = $this->getDbTables(); 38 | $includeTables = rtrim(' ' . implode(' ', array_diff($tables, $this->excludeTables))); 39 | } 40 | $dumpInSqlite = "echo 'BEGIN IMMEDIATE;\n.dump{$includeTables}'"; 41 | if ($this->isWindows()) { 42 | $dumpInSqlite = "(echo BEGIN IMMEDIATE; & echo .dump{$includeTables})"; 43 | } 44 | $quote = $this->determineQuote(); 45 | 46 | $command = sprintf( 47 | "{$dumpInSqlite} | {$quote}%ssqlite3{$quote} --bail {$quote}%s{$quote}", 48 | $this->dumpBinaryPath, 49 | $this->dbName 50 | ); 51 | 52 | return $this->echoToFile($command, $dumpFile); 53 | } 54 | 55 | public function getProcess(string $dumpFile): Process 56 | { 57 | $command = $this->getDumpCommand($dumpFile); 58 | 59 | return Process::fromShellCommandline($command, null, null, null, $this->timeout); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/DsnParser.php: -------------------------------------------------------------------------------- 1 | dsn = $dsn; 14 | } 15 | 16 | public function parse(): array 17 | { 18 | $rawComponents = $this->parseUrl($this->dsn); 19 | 20 | $decodedComponents = $this->parseNativeTypes( 21 | array_map('rawurldecode', $rawComponents) 22 | ); 23 | 24 | return array_merge( 25 | $this->getPrimaryOptions($decodedComponents), 26 | $this->getQueryOptions($rawComponents) 27 | ); 28 | } 29 | 30 | protected function getPrimaryOptions($url): array 31 | { 32 | return array_filter([ 33 | 'database' => $this->getDatabase($url), 34 | 'host' => $url['host'] ?? null, 35 | 'port' => $url['port'] ?? null, 36 | 'username' => $url['user'] ?? null, 37 | 'password' => $url['pass'] ?? null, 38 | ], static fn ($value) => ! is_null($value)); 39 | } 40 | 41 | protected function getDatabase($url): ?string 42 | { 43 | $path = $url['path'] ?? null; 44 | 45 | if (! $path) { 46 | return null; 47 | } 48 | 49 | if ($path === '/') { 50 | return null; 51 | } 52 | 53 | if (isset($url['scheme']) && str_contains($url['scheme'], 'sqlite')) { 54 | return $path; 55 | } 56 | 57 | return trim($path, '/'); 58 | } 59 | 60 | protected function getQueryOptions($url) 61 | { 62 | $queryString = $url['query'] ?? null; 63 | 64 | if (! $queryString) { 65 | return []; 66 | } 67 | 68 | $query = []; 69 | 70 | parse_str($queryString, $query); 71 | 72 | return $this->parseNativeTypes($query); 73 | } 74 | 75 | protected function parseUrl($url): array 76 | { 77 | $url = preg_replace('#^(sqlite3?):///#', '$1://null/', $url); 78 | 79 | $parsedUrl = parse_url($url); 80 | 81 | if ($parsedUrl === false) { 82 | throw InvalidDatabaseUrl::invalidUrl($url); 83 | } 84 | 85 | return $parsedUrl; 86 | } 87 | 88 | protected function parseNativeTypes($value) 89 | { 90 | if (is_array($value)) { 91 | return array_map([$this, 'parseNativeTypes'], $value); 92 | } 93 | 94 | if (! is_string($value)) { 95 | return $value; 96 | } 97 | 98 | $parsedValue = json_decode($value, true); 99 | 100 | if (json_last_error() === JSON_ERROR_NONE) { 101 | return $parsedValue; 102 | } 103 | 104 | return $value; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Databases/MongoDb.php: -------------------------------------------------------------------------------- 1 | guardAgainstIncompleteCredentials(); 20 | 21 | $process = $this->getProcess($dumpFile); 22 | 23 | $process->run(); 24 | 25 | $this->checkIfDumpWasSuccessFul($process, $dumpFile); 26 | } 27 | 28 | /** 29 | * Verifies if the dbname and host options are set. 30 | * 31 | * @throws \Spatie\DbDumper\Exceptions\CannotStartDump 32 | * 33 | * @return void 34 | */ 35 | public function guardAgainstIncompleteCredentials(): void 36 | { 37 | foreach (['dbName', 'host'] as $requiredProperty) { 38 | if (strlen($this->$requiredProperty) === 0) { 39 | throw CannotStartDump::emptyParameter($requiredProperty); 40 | } 41 | } 42 | } 43 | 44 | public function setCollection(string $collection): self 45 | { 46 | $this->collection = $collection; 47 | 48 | return $this; 49 | } 50 | 51 | public function setAuthenticationDatabase(string $authenticationDatabase): self 52 | { 53 | $this->authenticationDatabase = $authenticationDatabase; 54 | 55 | return $this; 56 | } 57 | 58 | public function getDumpCommand(string $filename): string 59 | { 60 | $quote = $this->determineQuote(); 61 | 62 | $command = [ 63 | "{$quote}{$this->dumpBinaryPath}mongodump{$quote}", 64 | "--db {$this->dbName}", 65 | '--archive', 66 | ]; 67 | 68 | if ($this->userName) { 69 | $command[] = "--username {$quote}{$this->userName}{$quote}"; 70 | } 71 | 72 | if ($this->password) { 73 | $command[] = "--password {$quote}{$this->password}{$quote}"; 74 | } 75 | 76 | if (isset($this->host)) { 77 | $command[] = "--host {$this->host}"; 78 | } 79 | 80 | if (isset($this->port)) { 81 | $command[] = "--port {$this->port}"; 82 | } 83 | 84 | if (isset($this->collection)) { 85 | $command[] = "--collection {$this->collection}"; 86 | } 87 | 88 | if ($this->authenticationDatabase) { 89 | $command[] = "--authenticationDatabase {$this->authenticationDatabase}"; 90 | } 91 | 92 | return $this->echoToFile(implode(' ', $command), $filename); 93 | } 94 | 95 | public function getProcess(string $dumpFile): Process 96 | { 97 | $command = $this->getDumpCommand($dumpFile); 98 | 99 | return Process::fromShellCommandline($command, null, null, null, $this->timeout); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Databases/PostgreSql.php: -------------------------------------------------------------------------------- 1 | port = 5432; 23 | } 24 | 25 | public function useInserts(): self 26 | { 27 | $this->useInserts = true; 28 | 29 | return $this; 30 | } 31 | 32 | public function dumpToFile(string $dumpFile): void 33 | { 34 | $this->guardAgainstIncompleteCredentials(); 35 | 36 | $tempFileHandle = tmpfile(); 37 | $this->setTempFileHandle($tempFileHandle); 38 | 39 | $process = $this->getProcess($dumpFile); 40 | 41 | $process->run(); 42 | 43 | $this->checkIfDumpWasSuccessFul($process, $dumpFile); 44 | } 45 | 46 | public function getDumpCommand(string $dumpFile): string 47 | { 48 | $quote = $this->determineQuote(); 49 | 50 | $command = [ 51 | "{$quote}{$this->dumpBinaryPath}pg_dump{$quote}", 52 | "-U \"{$this->userName}\"", 53 | '-h '.($this->socket === '' ? $this->host : $this->socket), 54 | "-p {$this->port}", 55 | ]; 56 | 57 | if ($this->useInserts) { 58 | $command[] = '--inserts'; 59 | } 60 | 61 | if (! $this->createTables) { 62 | $command[] = '--data-only'; 63 | } 64 | 65 | if (! $this->includeData) { 66 | $command[] = '--schema-only'; 67 | } 68 | 69 | foreach ($this->extraOptions as $extraOption) { 70 | $command[] = $extraOption; 71 | } 72 | 73 | if (! empty($this->includeTables)) { 74 | $command[] = '-t '.implode(' -t ', $this->includeTables); 75 | } 76 | 77 | if (! empty($this->excludeTables)) { 78 | $command[] = '-T '.implode(' -T ', $this->excludeTables); 79 | } 80 | 81 | return $this->echoToFile(implode(' ', $command), $dumpFile); 82 | } 83 | 84 | public function getContentsOfCredentialsFile(): string 85 | { 86 | $contents = [ 87 | $this->escapeCredentialEntry($this->host), 88 | $this->escapeCredentialEntry((string) $this->port), 89 | $this->escapeCredentialEntry($this->dbName), 90 | $this->escapeCredentialEntry($this->userName), 91 | $this->escapeCredentialEntry($this->password), 92 | ]; 93 | 94 | return implode(':', $contents); 95 | } 96 | 97 | protected function escapeCredentialEntry($entry): string 98 | { 99 | return str_replace(['\\', ':'], ['\\\\', '\\:'], $entry); 100 | } 101 | 102 | public function guardAgainstIncompleteCredentials() 103 | { 104 | foreach (['userName', 'dbName', 'host'] as $requiredProperty) { 105 | if (empty($this->$requiredProperty)) { 106 | throw CannotStartDump::emptyParameter($requiredProperty); 107 | } 108 | } 109 | } 110 | 111 | protected function getEnvironmentVariablesForDumpCommand(string $temporaryCredentialsFile): array 112 | { 113 | return [ 114 | 'PGPASSFILE' => $temporaryCredentialsFile, 115 | 'PGDATABASE' => $this->dbName, 116 | ]; 117 | } 118 | 119 | public function doNotCreateTables(): self 120 | { 121 | $this->createTables = false; 122 | 123 | return $this; 124 | } 125 | 126 | public function doNotDumpData(): self 127 | { 128 | $this->includeData = false; 129 | 130 | return $this; 131 | } 132 | 133 | public function getProcess(string $dumpFile): Process 134 | { 135 | $command = $this->getDumpCommand($dumpFile); 136 | 137 | fwrite($this->getTempFileHandle(), $this->getContentsOfCredentialsFile()); 138 | $temporaryCredentialsFile = stream_get_meta_data($this->getTempFileHandle())['uri']; 139 | 140 | $envVars = $this->getEnvironmentVariablesForDumpCommand($temporaryCredentialsFile); 141 | 142 | return Process::fromShellCommandline($command, null, $envVars, null, $this->timeout); 143 | } 144 | 145 | /** 146 | * @return false|resource 147 | */ 148 | public function getTempFileHandle() 149 | { 150 | return $this->tempFileHandle; 151 | } 152 | 153 | /** 154 | * @param false|resource $tempFileHandle 155 | */ 156 | public function setTempFileHandle($tempFileHandle): void 157 | { 158 | $this->tempFileHandle = $tempFileHandle; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/DbDumper.php: -------------------------------------------------------------------------------- 1 | dbName; 50 | } 51 | 52 | public function setDbName(string $dbName): self 53 | { 54 | $this->dbName = $dbName; 55 | 56 | return $this; 57 | } 58 | 59 | public function getDatabaseUrl(): string 60 | { 61 | return $this->databaseUrl; 62 | } 63 | 64 | public function setDatabaseUrl(string $databaseUrl): self 65 | { 66 | $this->databaseUrl = $databaseUrl; 67 | 68 | $this->configureFromDatabaseUrl(); 69 | 70 | return $this; 71 | } 72 | 73 | public function setUserName(string $userName): self 74 | { 75 | $this->userName = $userName; 76 | 77 | return $this; 78 | } 79 | 80 | public function setPassword(string $password): self 81 | { 82 | $this->password = $password; 83 | 84 | return $this; 85 | } 86 | 87 | public function setHost(string $host): self 88 | { 89 | $this->host = $host; 90 | 91 | return $this; 92 | } 93 | 94 | public function getHost(): string 95 | { 96 | return $this->host; 97 | } 98 | 99 | public function setPort(int $port): self 100 | { 101 | $this->port = $port; 102 | 103 | return $this; 104 | } 105 | 106 | public function setSocket(string $socket): self 107 | { 108 | $this->socket = $socket; 109 | 110 | return $this; 111 | } 112 | 113 | public function setTimeout(int $timeout): self 114 | { 115 | $this->timeout = $timeout; 116 | 117 | return $this; 118 | } 119 | 120 | public function setDumpBinaryPath(string $dumpBinaryPath = ''): self 121 | { 122 | if ($dumpBinaryPath !== '' && ! str_ends_with($dumpBinaryPath, '/')) { 123 | $dumpBinaryPath .= '/'; 124 | } 125 | 126 | $this->dumpBinaryPath = $dumpBinaryPath; 127 | 128 | return $this; 129 | } 130 | 131 | public function getCompressorExtension(): string 132 | { 133 | return $this->compressor->useExtension(); 134 | } 135 | 136 | public function useCompressor(Compressor $compressor): self 137 | { 138 | if ($this->appendMode) { 139 | throw CannotSetParameter::conflictingParameters('compressor', 'append mode'); 140 | } 141 | 142 | $this->compressor = $compressor; 143 | 144 | return $this; 145 | } 146 | 147 | public function includeTables(string | array $includeTables): self 148 | { 149 | if (! empty($this->excludeTables)) { 150 | throw CannotSetParameter::conflictingParameters('includeTables', 'excludeTables'); 151 | } 152 | 153 | if (! is_array($includeTables)) { 154 | $includeTables = explode(', ', $includeTables); 155 | } 156 | 157 | $this->includeTables = $includeTables; 158 | 159 | return $this; 160 | } 161 | 162 | public function excludeTables(string | array $excludeTables): self 163 | { 164 | if (! empty($this->includeTables)) { 165 | throw CannotSetParameter::conflictingParameters('excludeTables', 'includeTables'); 166 | } 167 | 168 | if (! is_array($excludeTables)) { 169 | $excludeTables = explode(', ', $excludeTables); 170 | } 171 | 172 | $this->excludeTables = $excludeTables; 173 | 174 | return $this; 175 | } 176 | 177 | public function addExtraOption(string $extraOption): self 178 | { 179 | if (! empty($extraOption)) { 180 | $this->extraOptions[] = $extraOption; 181 | } 182 | 183 | return $this; 184 | } 185 | 186 | public function addExtraOptionAfterDbName(string $extraOptionAfterDbName): self 187 | { 188 | if (! empty($extraOptionAfterDbName)) { 189 | $this->extraOptionsAfterDbName[] = $extraOptionAfterDbName; 190 | } 191 | 192 | return $this; 193 | } 194 | 195 | abstract public function dumpToFile(string $dumpFile): void; 196 | 197 | public function checkIfDumpWasSuccessFul(Process $process, string $outputFile): void 198 | { 199 | if (! $process->isSuccessful()) { 200 | throw DumpFailed::processDidNotEndSuccessfully($process); 201 | } 202 | 203 | if (! file_exists($outputFile)) { 204 | throw DumpFailed::dumpfileWasNotCreated($process); 205 | } 206 | 207 | if (filesize($outputFile) === 0) { 208 | throw DumpFailed::dumpfileWasEmpty($process); 209 | } 210 | } 211 | 212 | protected function configureFromDatabaseUrl(): void 213 | { 214 | $parsed = (new DsnParser($this->databaseUrl))->parse(); 215 | 216 | $componentMap = [ 217 | 'host' => 'setHost', 218 | 'port' => 'setPort', 219 | 'database' => 'setDbName', 220 | 'username' => 'setUserName', 221 | 'password' => 'setPassword', 222 | ]; 223 | 224 | foreach ($parsed as $component => $value) { 225 | if (isset($componentMap[$component])) { 226 | $setterMethod = $componentMap[$component]; 227 | 228 | if (empty($value) || $value === 'null') { 229 | continue; 230 | } 231 | 232 | $this->$setterMethod($value); 233 | } 234 | } 235 | } 236 | 237 | protected function getCompressCommand(string $command, string $dumpFile): string 238 | { 239 | $compressCommand = $this->compressor->useCommand(); 240 | 241 | if ($this->isWindows()) { 242 | return "{$command} | {$compressCommand} > {$dumpFile}"; 243 | } 244 | 245 | return "(((({$command}; echo \$? >&3) | {$compressCommand} > {$dumpFile}) 3>&1) | (read x; exit \$x))"; 246 | } 247 | 248 | protected function echoToFile(string $command, string $dumpFile): string 249 | { 250 | $dumpFile = '"' . addcslashes($dumpFile, '\\"') . '"'; 251 | 252 | if ($this->compressor) { 253 | return $this->getCompressCommand($command, $dumpFile); 254 | } 255 | 256 | if ($this->appendMode) { 257 | return $command . ' >> ' . $dumpFile; 258 | } 259 | 260 | return $command . ' > ' . $dumpFile; 261 | } 262 | 263 | protected function determineQuote(): string 264 | { 265 | return $this->isWindows() ? '"' : "'"; 266 | } 267 | 268 | protected function isWindows(): bool 269 | { 270 | return PHP_OS_FAMILY === 'Windows'; 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /src/Databases/MySql.php: -------------------------------------------------------------------------------- 1 | port = 3306; 49 | } 50 | 51 | public function setSkipSsl(bool $skipSsl = true): self 52 | { 53 | $this->skipSsl = $skipSsl; 54 | 55 | return $this; 56 | } 57 | 58 | public function setSslFlag(string $sslFlag = ''): self 59 | { 60 | $allowedValues = [ 61 | 'skip-ssl', 62 | 'ssl-mode=DISABLED', 63 | 'ssl-mode=PREFERRED', 64 | ]; 65 | 66 | if (in_array($sslFlag, $allowedValues)) { 67 | $this->sslFlag = $sslFlag; 68 | } 69 | 70 | return $this; 71 | } 72 | 73 | public function skipComments(): self 74 | { 75 | $this->skipComments = true; 76 | 77 | return $this; 78 | } 79 | 80 | public function dontSkipComments(): self 81 | { 82 | $this->skipComments = false; 83 | 84 | return $this; 85 | } 86 | 87 | public function useExtendedInserts(): self 88 | { 89 | $this->useExtendedInserts = true; 90 | 91 | return $this; 92 | } 93 | 94 | public function dontUseExtendedInserts(): self 95 | { 96 | $this->useExtendedInserts = false; 97 | 98 | return $this; 99 | } 100 | 101 | public function useSingleTransaction(): self 102 | { 103 | $this->useSingleTransaction = true; 104 | 105 | return $this; 106 | } 107 | 108 | public function dontUseSingleTransaction(): self 109 | { 110 | $this->useSingleTransaction = false; 111 | 112 | return $this; 113 | } 114 | 115 | public function skipLockTables(): self 116 | { 117 | $this->skipLockTables = true; 118 | 119 | return $this; 120 | } 121 | 122 | public function doNotUseColumnStatistics(): self 123 | { 124 | $this->doNotUseColumnStatistics = true; 125 | 126 | return $this; 127 | } 128 | 129 | public function dontSkipLockTables(): self 130 | { 131 | $this->skipLockTables = false; 132 | 133 | return $this; 134 | } 135 | 136 | public function useQuick(): self 137 | { 138 | $this->useQuick = true; 139 | 140 | return $this; 141 | } 142 | 143 | public function dontUseQuick(): self 144 | { 145 | $this->useQuick = false; 146 | 147 | return $this; 148 | } 149 | 150 | public function setDefaultCharacterSet(string $characterSet): self 151 | { 152 | $this->defaultCharacterSet = $characterSet; 153 | 154 | return $this; 155 | } 156 | 157 | public function setGtidPurged(string $setGtidPurged): self 158 | { 159 | $this->setGtidPurged = $setGtidPurged; 160 | 161 | return $this; 162 | } 163 | 164 | public function skipAutoIncrement(): self 165 | { 166 | $this->skipAutoIncrement = true; 167 | 168 | return $this; 169 | } 170 | 171 | public function dontSkipAutoIncrement(): self 172 | { 173 | $this->skipAutoIncrement = false; 174 | 175 | return $this; 176 | } 177 | 178 | public function dumpToFile(string $dumpFile): void 179 | { 180 | $this->guardAgainstIncompleteCredentials(); 181 | 182 | $tempFileHandle = tmpfile(); 183 | $this->setTempFileHandle($tempFileHandle); 184 | 185 | $process = $this->getProcess($dumpFile); 186 | 187 | $process->run(); 188 | 189 | $this->checkIfDumpWasSuccessFul($process, $dumpFile); 190 | } 191 | 192 | public function addExtraOption(string $extraOption): self 193 | { 194 | if (str_contains($extraOption, '--all-databases')) { 195 | $this->dbNameWasSetAsExtraOption = true; 196 | $this->allDatabasesWasSetAsExtraOption = true; 197 | } 198 | 199 | if (preg_match('/^--databases (\S+)/', $extraOption, $matches) === 1) { 200 | $this->setDbName($matches[1]); 201 | $this->dbNameWasSetAsExtraOption = true; 202 | } 203 | 204 | return parent::addExtraOption($extraOption); 205 | } 206 | 207 | public function doNotCreateTables(): self 208 | { 209 | $this->createTables = false; 210 | 211 | return $this; 212 | } 213 | 214 | public function doNotDumpData(): self 215 | { 216 | $this->includeData = false; 217 | 218 | return $this; 219 | } 220 | 221 | public function useAppendMode(): self 222 | { 223 | if ($this->compressor) { 224 | throw CannotSetParameter::conflictingParameters('append mode', 'compress'); 225 | } 226 | 227 | $this->appendMode = true; 228 | 229 | return $this; 230 | } 231 | 232 | public function getDumpCommand(string $dumpFile, string $temporaryCredentialsFile): string 233 | { 234 | $quote = $this->determineQuote(); 235 | 236 | $command = [ 237 | "{$quote}{$this->dumpBinaryPath}mysqldump{$quote}", 238 | "--defaults-extra-file=\"{$temporaryCredentialsFile}\"", 239 | ]; 240 | $finalDumpCommand = $this->getCommonDumpCommand($command); 241 | 242 | return $this->echoToFile($finalDumpCommand, $dumpFile); 243 | } 244 | 245 | public function getCommonDumpCommand(array $command): string 246 | { 247 | if (! $this->createTables) { 248 | $command[] = '--no-create-info'; 249 | } 250 | 251 | if (! $this->includeData) { 252 | $command[] = '--no-data'; 253 | } 254 | 255 | if ($this->skipComments) { 256 | $command[] = '--skip-comments'; 257 | } 258 | 259 | $command[] = $this->useExtendedInserts ? '--extended-insert' : '--skip-extended-insert'; 260 | 261 | if ($this->useSingleTransaction) { 262 | $command[] = '--single-transaction'; 263 | } 264 | 265 | if ($this->skipLockTables) { 266 | $command[] = '--skip-lock-tables'; 267 | } 268 | 269 | if ($this->doNotUseColumnStatistics) { 270 | $command[] = '--column-statistics=0'; 271 | } 272 | 273 | if ($this->useQuick) { 274 | $command[] = '--quick'; 275 | } 276 | 277 | if ($this->socket !== '') { 278 | $command[] = "--socket={$this->socket}"; 279 | } 280 | 281 | foreach ($this->excludeTables as $tableName) { 282 | $command[] = "--ignore-table={$this->dbName}.{$tableName}"; 283 | } 284 | 285 | if (! empty($this->defaultCharacterSet)) { 286 | $command[] = '--default-character-set=' . $this->defaultCharacterSet; 287 | } 288 | 289 | foreach ($this->extraOptions as $extraOption) { 290 | $command[] = $extraOption; 291 | } 292 | 293 | if ($this->setGtidPurged !== 'AUTO') { 294 | $command[] = '--set-gtid-purged=' . $this->setGtidPurged; 295 | } 296 | 297 | if (! $this->dbNameWasSetAsExtraOption) { 298 | $command[] = $this->dbName; 299 | } 300 | 301 | if (! empty($this->includeTables)) { 302 | $includeTables = implode(' ', $this->includeTables); 303 | $command[] = "--tables {$includeTables}"; 304 | } 305 | 306 | foreach ($this->extraOptionsAfterDbName as $extraOptionAfterDbName) { 307 | $command[] = $extraOptionAfterDbName; 308 | } 309 | 310 | $finalDumpCommand = implode(' ', $command); 311 | 312 | if ($this->skipAutoIncrement) { 313 | $sedCommand = "sed 's/ AUTO_INCREMENT=[0-9]*\b//'"; 314 | $finalDumpCommand .= " | {$sedCommand}"; 315 | } 316 | 317 | return $finalDumpCommand; 318 | } 319 | 320 | public function getContentsOfCredentialsFile(): string 321 | { 322 | $contents = [ 323 | '[client]', 324 | "user = '{$this->userName}'", 325 | "password = '{$this->password}'", 326 | "port = '{$this->port}'", 327 | ]; 328 | 329 | if ($this->socket === '') { 330 | $contents[] = "host = '{$this->host}'"; 331 | } 332 | 333 | if ($this->skipSsl) { 334 | $contents[] = $this->getSSLFlag(); 335 | } 336 | 337 | return implode(PHP_EOL, $contents); 338 | } 339 | 340 | public function guardAgainstIncompleteCredentials(): void 341 | { 342 | foreach (['userName', 'host'] as $requiredProperty) { 343 | if (strlen($this->$requiredProperty) === 0) { 344 | throw CannotStartDump::emptyParameter($requiredProperty); 345 | } 346 | } 347 | 348 | if (strlen($this->dbName) === 0 && ! $this->allDatabasesWasSetAsExtraOption) { 349 | throw CannotStartDump::emptyParameter('dbName'); 350 | } 351 | } 352 | 353 | /** 354 | * @param string $dumpFile 355 | * @return Process 356 | */ 357 | public function getProcess(string $dumpFile): Process 358 | { 359 | fwrite($this->getTempFileHandle(), $this->getContentsOfCredentialsFile()); 360 | $temporaryCredentialsFile = stream_get_meta_data($this->getTempFileHandle())['uri']; 361 | 362 | $command = $this->getDumpCommand($dumpFile, $temporaryCredentialsFile); 363 | 364 | return Process::fromShellCommandline($command, null, null, null, $this->timeout); 365 | } 366 | 367 | /** 368 | * @return false|resource 369 | */ 370 | public function getTempFileHandle(): mixed 371 | { 372 | return $this->tempFileHandle; 373 | } 374 | 375 | /** 376 | * @param false|resource $tempFileHandle 377 | */ 378 | public function setTempFileHandle($tempFileHandle) 379 | { 380 | $this->tempFileHandle = $tempFileHandle; 381 | } 382 | 383 | /** 384 | * Since MySQL 8.0.26, --skip-ssl has been deprecated and replaced with ssl-mode=DISABLED. 385 | * Since MySQL 8.4.0, --skip-ssl has been removed. 386 | * 387 | * https://dev.mysql.com/doc/relnotes/mysql/8.4/en/news-8-4-0.html 388 | * 389 | * @return string 390 | */ 391 | protected function getSSLFlag(): string 392 | { 393 | if ($this->sslFlag !== '') { 394 | return $this->sslFlag; 395 | } 396 | 397 | $sslFlag = 'skip-ssl'; 398 | $mysqlVersion = DB::selectOne('SELECT VERSION() AS version'); 399 | 400 | if (version_compare($mysqlVersion->version, '8.4.0', '>=')) { 401 | $sslFlag = 'ssl-mode=DISABLED'; 402 | } 403 | 404 | return $sslFlag; 405 | } 406 | } 407 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dump the contents of a database 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/db-dumper.svg?style=flat-square)](https://packagist.org/packages/spatie/db-dumper) 4 | ![run-tests](https://github.com/spatie/db-dumper/workflows/run-tests/badge.svg) 5 | [![MIT Licensed](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/spatie/db-dumper.svg?style=flat-square)](https://packagist.org/packages/spatie/db-dumper) 7 | 8 | This repo contains an easy to use class to dump a database using PHP. Currently MySQL, PostgreSQL, SQLite and MongoDB are supported. Behind the scenes `mysqldump`, `pg_dump`, `sqlite3` and `mongodump` are used. 9 | 10 | Here are simple examples of how to create a database dump with different drivers: 11 | 12 | **MySQL** 13 | 14 | ```php 15 | Spatie\DbDumper\Databases\MySql::create() 16 | ->setDbName($databaseName) 17 | ->setUserName($userName) 18 | ->setPassword($password) 19 | ->dumpToFile('dump.sql'); 20 | ``` 21 | 22 | **PostgreSQL** 23 | 24 | ```php 25 | Spatie\DbDumper\Databases\PostgreSql::create() 26 | ->setDbName($databaseName) 27 | ->setUserName($userName) 28 | ->setPassword($password) 29 | ->dumpToFile('dump.sql'); 30 | ``` 31 | 32 | **SQLite** 33 | 34 | ```php 35 | Spatie\DbDumper\Databases\Sqlite::create() 36 | ->setDbName($pathToDatabaseFile) 37 | ->dumpToFile('dump.sql'); 38 | ``` 39 | 40 | ⚠️ Sqlite version 3.32.0 is required when using the `includeTables` option. 41 | 42 | **MongoDB** 43 | 44 | ```php 45 | Spatie\DbDumper\Databases\MongoDb::create() 46 | ->setDbName($databaseName) 47 | ->setUserName($userName) 48 | ->setPassword($password) 49 | ->dumpToFile('dump.gz'); 50 | ``` 51 | 52 | ## Support us 53 | 54 | [](https://spatie.be/github-ad-click/db-dumper) 55 | 56 | We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). 57 | 58 | We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). 59 | 60 | ## Requirements 61 | 62 | For dumping MySQL-db's `mysqldump` should be installed. 63 | 64 | For dumping PostgreSQL-db's `pg_dump` should be installed. 65 | 66 | For dumping SQLite-db's `sqlite3` should be installed. 67 | 68 | For dumping MongoDB-db's `mongodump` should be installed. 69 | 70 | For compressing dump files, `gzip` and/or `bzip2` should be installed. 71 | 72 | ## Installation 73 | 74 | You can install the package via composer: 75 | 76 | ```bash 77 | composer require spatie/db-dumper 78 | ``` 79 | 80 | ## Usage 81 | 82 | This is the simplest way to create a dump of a MySql db: 83 | 84 | ```php 85 | Spatie\DbDumper\Databases\MySql::create() 86 | ->setDbName($databaseName) 87 | ->setUserName($userName) 88 | ->setPassword($password) 89 | ->dumpToFile('dump.sql'); 90 | ``` 91 | 92 | If you're working with PostgreSQL just use that dumper, most methods are available on both the MySql. and PostgreSql-dumper. 93 | 94 | ```php 95 | Spatie\DbDumper\Databases\PostgreSql::create() 96 | ->setDbName($databaseName) 97 | ->setUserName($userName) 98 | ->setPassword($password) 99 | ->dumpToFile('dump.sql'); 100 | ``` 101 | 102 | If the `mysqldump` (or `pg_dump`) binary is installed in a non default location you can let the package know by using the`setDumpBinaryPath()`-function: 103 | 104 | ```php 105 | Spatie\DbDumper\Databases\MySql::create() 106 | ->setDumpBinaryPath('/custom/location') 107 | ->setDbName($databaseName) 108 | ->setUserName($userName) 109 | ->setPassword($password) 110 | ->dumpToFile('dump.sql'); 111 | ``` 112 | 113 | If your application is deployed and you need to change the host (default is 127.0.0.1), you can add the `setHost()`-function: 114 | 115 | ```php 116 | Spatie\DbDumper\Databases\MySql::create() 117 | ->setDbName($databaseName) 118 | ->setUserName($userName) 119 | ->setPassword($password) 120 | ->setHost($host) 121 | ->dumpToFile('dump.sql'); 122 | ``` 123 | 124 | ### Handling AUTO_INCREMENT Values in Dumps 125 | 126 | When creating a database dump, you might need to control the inclusion of AUTO_INCREMENT values. This can be crucial for avoiding primary key conflicts or for maintaining ID consistency when transferring data across environments. 127 | 128 | #### Skipping AUTO_INCREMENT Values 129 | 130 | To omit the AUTO_INCREMENT values from the tables in your dump, use the skipAutoIncrement method. This is particularly useful to prevent conflicts when importing the dump into another database where those specific AUTO_INCREMENT values might already exist, or when the exact values are not relevant. 131 | 132 | ```php 133 | Spatie\DbDumper\Databases\MySql::create() 134 | ->setDbName('dbname') 135 | ->setUserName('username') 136 | ->setPassword('password') 137 | ->skipAutoIncrement() 138 | ->dumpToFile('dump.sql'); 139 | ``` 140 | 141 | ### Including AUTO_INCREMENT values in the dump 142 | 143 | By default, the AUTO_INCREMENT values are included in the dump. However, if you previously used the skipAutoIncrement method and wish to ensure that the AUTO_INCREMENT values are included in a subsequent dump, use the dontSkipAutoIncrement method to explicitly include them. 144 | 145 | ```php 146 | Spatie\DbDumper\Databases\MySql::create() 147 | ->setDbName('dbname') 148 | ->setUserName('username') 149 | ->setPassword('password') 150 | ->dontSkipAutoIncrement() 151 | ->dumpToFile('dump.sql'); 152 | ``` 153 | 154 | ### Use a Database URL 155 | 156 | In some applications or environments, database credentials are provided as URLs instead of individual components. In this case, you can use the `setDatabaseUrl` method instead of the individual methods. 157 | 158 | ```php 159 | Spatie\DbDumper\Databases\MySql::create() 160 | ->setDatabaseUrl($databaseUrl) 161 | ->dumpToFile('dump.sql'); 162 | ``` 163 | 164 | When providing a URL, the package will automatically parse it and provide the individual components to the applicable dumper. 165 | 166 | For example, if you provide the URL `mysql://username:password@hostname:3306/dbname`, the dumper will use the `hostname` host, running on port `3306`, and will connect to `dbname` with `username` and `password`. 167 | 168 | ### Dump specific tables 169 | 170 | Using an array: 171 | 172 | ```php 173 | Spatie\DbDumper\Databases\MySql::create() 174 | ->setDbName($databaseName) 175 | ->setUserName($userName) 176 | ->setPassword($password) 177 | ->includeTables(['table1', 'table2', 'table3']) 178 | ->dumpToFile('dump.sql'); 179 | ``` 180 | 181 | Using a string: 182 | 183 | ```php 184 | Spatie\DbDumper\Databases\MySql::create() 185 | ->setDbName($databaseName) 186 | ->setUserName($userName) 187 | ->setPassword($password) 188 | ->includeTables('table1, table2, table3') 189 | ->dumpToFile('dump.sql'); 190 | ``` 191 | 192 | ### Don't use column_statics table with some old version of MySql service. 193 | 194 | In order to use "_--column-statistics=0_" as option in mysqldump command you can use _doNotUseColumnStatistics()_ method. 195 | 196 | If you have installed _mysqldump 8_, it queries by default _column_statics_ table in _information_schema_ database. 197 | In some old version of MySql (service) like 5.7, this table doesn't exist. So you could have an exception during the execution of mysqldump. To avoid this, you could use _doNotUseColumnStatistics()_ method. 198 | 199 | ```php 200 | Spatie\DbDumper\Databases\MySql::create() 201 | ->setDbName($databaseName) 202 | ->setUserName($userName) 203 | ->setPassword($password) 204 | ->doNotUseColumnStatistics() 205 | ->dumpToFile('dump.sql'); 206 | ``` 207 | 208 | ### Excluding tables from the dump 209 | 210 | You can exclude tables from the dump by using an array: 211 | 212 | ```php 213 | Spatie\DbDumper\Databases\MySql::create() 214 | ->setDbName($databaseName) 215 | ->setUserName($userName) 216 | ->setPassword($password) 217 | ->excludeTables(['table1', 'table2', 'table3']) 218 | ->dumpToFile('dump.sql'); 219 | ``` 220 | 221 | Or by using a string: 222 | 223 | ```php 224 | Spatie\DbDumper\Databases\MySql::create() 225 | ->setDbName($databaseName) 226 | ->setUserName($userName) 227 | ->setPassword($password) 228 | ->excludeTables('table1, table2, table3') 229 | ->dumpToFile('dump.sql'); 230 | ``` 231 | 232 | ### Do not write CREATE TABLE statements that create each dumped table 233 | 234 | You can use `doNotCreateTables` to prevent writing create statements. 235 | 236 | ```php 237 | $dumpCommand = MySql::create() 238 | ->setDbName('dbname') 239 | ->setUserName('username') 240 | ->setPassword('password') 241 | ->doNotCreateTables() 242 | ->getDumpCommand('dump.sql', 'credentials.txt'); 243 | ``` 244 | 245 | ### Do not write row data 246 | 247 | You can use `doNotDumpData` to prevent writing row data. 248 | 249 | 250 | ```php 251 | $dumpCommand = MySql::create() 252 | ->setDbName('dbname') 253 | ->setUserName('username') 254 | ->setPassword('password') 255 | ->doNotDumpData() 256 | ->getDumpCommand('dump.sql', 'credentials.txt'); 257 | ``` 258 | 259 | ### Append instead of overwriting a dump file 260 | 261 | You can use `useAppendMode` with MySQL to append to the file instead of overwriting it. 262 | This is useful for two-step dumps when you want to dump the whole schema but only some of the data 263 | (for example: only migrations, or only product but not customer data). 264 | 265 | ```php 266 | $dumpCommand = MySql::create() 267 | ->setDbName('dbname') 268 | ->setUserName('username') 269 | ->setPassword('password') 270 | ->useAppendMode() 271 | ->getDumpCommand('dump.sql', 'credentials.txt'); 272 | ``` 273 | 274 | ### Adding extra options 275 | 276 | If you want to add an arbitrary option to the dump command you can use `addExtraOption` 277 | 278 | ```php 279 | $dumpCommand = MySql::create() 280 | ->setDbName('dbname') 281 | ->setUserName('username') 282 | ->setPassword('password') 283 | ->addExtraOption('--xml') 284 | ->getDumpCommand('dump.sql', 'credentials.txt'); 285 | ``` 286 | 287 | If you're working with MySql you can set the database name using `--databases` as an extra option. This is particularly useful when used in conjunction with the `--add-drop-database` `mysqldump` option (see the [mysqldump docs](https://dev.mysql.com/doc/refman/5.7/en/mysqldump.html#option_mysqldump_add-drop-database)). 288 | 289 | ```php 290 | $dumpCommand = MySql::create() 291 | ->setUserName('username') 292 | ->setPassword('password') 293 | ->addExtraOption('--databases dbname') 294 | ->addExtraOption('--add-drop-database') 295 | ->getDumpCommand('dump.sql', 'credentials.txt'); 296 | ``` 297 | 298 | With MySql, you also have the option to use the `--all-databases` extra option. This is useful when you want to run a full backup of all the databases in the specified MySQL connection. 299 | 300 | ```php 301 | $dumpCommand = MySql::create() 302 | ->setUserName('username') 303 | ->setPassword('password') 304 | ->addExtraOption('--all-databases') 305 | ->getDumpCommand('dump.sql', 'credentials.txt'); 306 | ``` 307 | 308 | Please note that using the `->addExtraOption('--databases dbname')` or `->addExtraOption('--all-databases')` will override the database name set on a previous `->setDbName()` call. 309 | 310 | ### Using compression 311 | 312 | If you want the output file to be compressed, you can use a compressor class. 313 | 314 | There are two compressors that come out of the box: 315 | 316 | - `GzipCompressor` - This will compress your db dump with `gzip`. Make sure `gzip` is installed on your system before using this. 317 | - `Bzip2Compressor` - This will compress your db dump with `bzip2`. Make sure `bzip2` is installed on your system before using this. 318 | 319 | ```php 320 | $dumpCommand = MySql::create() 321 | ->setDbName('dbname') 322 | ->setUserName('username') 323 | ->setPassword('password') 324 | ->useCompressor(new GzipCompressor()) // or `new Bzip2Compressor()` 325 | ->dumpToFile('dump.sql.gz'); 326 | ``` 327 | 328 | ### Creating your own compressor 329 | 330 | You can create you own compressor implementing the `Compressor` interface. Here's how that interface looks like: 331 | 332 | ```php 333 | namespace Spatie\DbDumper\Compressors; 334 | 335 | interface Compressor 336 | { 337 | public function useCommand(): string; 338 | 339 | public function useExtension(): string; 340 | } 341 | ``` 342 | 343 | The `useCommand` should simply return the compression command the db dump will get pumped to. Here's the implementation of `GzipCompression`. 344 | 345 | ```php 346 | namespace Spatie\DbDumper\Compressors; 347 | 348 | class GzipCompressor implements Compressor 349 | { 350 | public function useCommand(): string 351 | { 352 | return 'gzip'; 353 | } 354 | 355 | public function useExtension(): string 356 | { 357 | return 'gz'; 358 | } 359 | } 360 | ``` 361 | 362 | ## Testing 363 | 364 | ```bash 365 | $ composer test 366 | ``` 367 | 368 | ## Changelog 369 | 370 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 371 | 372 | ## Contributing 373 | 374 | Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. 375 | 376 | ## Security Vulnerabilities 377 | 378 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 379 | 380 | ## Credits 381 | 382 | - [Freek Van der Herten](https://github.com/freekmurze) 383 | - [All Contributors](../../contributors) 384 | 385 | Initial PostgreSQL support was contributed by [Adriano Machado](https://github.com/ammachado). SQlite support was contributed by [Peter Matseykanets](https://twitter.com/pmatseykanets). 386 | 387 | ## License 388 | 389 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 390 | --------------------------------------------------------------------------------