Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for database URLs #196

Merged
merged 2 commits into from
Jun 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@
[![MIT Licensed](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md)
[![Total Downloads](https://img.shields.io/packagist/dt/spatie/db-dumper.svg?style=flat-square)](https://packagist.org/packages/spatie/db-dumper)

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.
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.

Here's are simple examples of how to create a database dump with different drivers:
Here are simple examples of how to create a database dump with different drivers:

**MySQL**

Expand Down Expand Up @@ -124,6 +123,20 @@ Spatie\DbDumper\Databases\MySql::create()
->dumpToFile('dump.sql');
```

### Use a Database URL

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.

```php
Spatie\DbDumper\Databases\MySql::create()
->setDatabaseUrl($databaseUrl)
->dumpToFile('dump.sql');
```

When providing a URL, the package will automatically parse it and provide the individual components to the applicable dumper.

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`.

### Dump specific tables

Using an array:
Expand Down
45 changes: 43 additions & 2 deletions src/DbDumper.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

abstract class DbDumper
{
protected string $databaseUrl = '';

protected string $dbName = '';

protected string $userName = '';
Expand Down Expand Up @@ -52,6 +54,20 @@ public function setDbName(string $dbName): self
return $this;
}

public function getDatabaseUrl(): string
{
return $this->databaseUrl;
}

public function setDatabaseUrl(string $databaseUrl): self
{
$this->databaseUrl = $databaseUrl;

$this->configureFromDatabaseUrl();

return $this;
}

public function setUserName(string $userName): self
{
$this->userName = $userName;
Expand Down Expand Up @@ -187,6 +203,31 @@ public function checkIfDumpWasSuccessFul(Process $process, string $outputFile):
}
}

protected function configureFromDatabaseUrl(): void
{
$parsed = (new DsnParser($this->databaseUrl))->parse();

$componentMap = [
'host' => 'setHost',
'port' => 'setPort',
'database' => 'setDbName',
'username' => 'setUserName',
'password' => 'setPassword',
];

foreach ($parsed as $component => $value) {
if (isset($componentMap[$component])) {
$setterMethod = $componentMap[$component];

if (! $value || in_array($value, ['', 'null'])) {
continue;
}

$this->$setterMethod($value);
}
}
}

protected function getCompressCommand(string $command, string $dumpFile): string
{
$compressCommand = $this->compressor->useCommand();
Expand All @@ -200,13 +241,13 @@ protected function getCompressCommand(string $command, string $dumpFile): string

protected function echoToFile(string $command, string $dumpFile): string
{
$dumpFile = '"'.addcslashes($dumpFile, '\\"').'"';
$dumpFile = '"' . addcslashes($dumpFile, '\\"') . '"';

if ($this->compressor) {
return $this->getCompressCommand($command, $dumpFile);
}

return $command.' > '.$dumpFile;
return $command . ' > ' . $dumpFile;
}

protected function determineQuote(): string
Expand Down
106 changes: 106 additions & 0 deletions src/DsnParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php

namespace Spatie\DbDumper;

use Spatie\DbDumper\Exceptions\InvalidDatabaseUrl;

class DsnParser
{
protected string $dsn;

public function __construct(string $dsn)
{
$this->dsn = $dsn;
}

public function parse(): array
{
$rawComponents = $this->parseUrl($this->dsn);

$decodedComponents = $this->parseNativeTypes(
array_map('rawurldecode', $rawComponents)
);

return array_merge(
$this->getPrimaryOptions($decodedComponents),
$this->getQueryOptions($rawComponents)
);
}

protected function getPrimaryOptions($url): array
{
return array_filter([
'database' => $this->getDatabase($url),
'host' => $url['host'] ?? null,
'port' => $url['port'] ?? null,
'username' => $url['user'] ?? null,
'password' => $url['pass'] ?? null,
], static fn ($value) => ! is_null($value));
}

protected function getDatabase($url): ?string
{
$path = $url['path'] ?? null;

if (! $path) {
return null;
}

if ($path === '/') {
return null;
}

if (isset($url['scheme']) && str_contains($url['scheme'], 'sqlite')) {
return $path;
}

return trim($path, '/');
}

protected function getQueryOptions($url)
{
$queryString = $url['query'] ?? null;

if (! $queryString) {
return [];
}

$query = [];

parse_str($queryString, $query);

return $this->parseNativeTypes($query);
}

protected function parseUrl($url): array
{
$url = preg_replace('#^(sqlite3?):///#', '$1://null/', $url);

$parsedUrl = parse_url($url);

if ($parsedUrl === false) {
throw InvalidDatabaseUrl::invalidUrl($url);
}

return $parsedUrl;
}

protected function parseNativeTypes($value)
{
if (is_array($value)) {
return array_map([$this, 'parseNativeTypes'], $value);
}

if (! is_string($value)) {
return $value;
}

$parsedValue = json_decode($value, true);

if (json_last_error() === JSON_ERROR_NONE) {
return $parsedValue;
}

return $value;
}
}
13 changes: 13 additions & 0 deletions src/Exceptions/InvalidDatabaseUrl.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Spatie\DbDumper\Exceptions;

use Exception;

class InvalidDatabaseUrl extends Exception
{
public static function invalidUrl(string $databaseUrl): static
{
return new static("Database URL `{$databaseUrl}` is invalid and cannot be parsed.");
}
}
11 changes: 11 additions & 0 deletions tests/MongoDbTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@
. ' --archive --host localhost --port 27017 > "dbname.gz"');
});

it('can generate a dump command using a database url', function () {
$dumpCommand = MongoDb::create()
->setDatabaseUrl('monogodb://username:password@localhost:27017/dbname')
->getDumpCommand('dbname.gz');

expect($dumpCommand)->toEqual(
'\'mongodump\' --db dbname'
. ' --archive --username \'username\' --password \'password\' --host localhost --port 27017 > "dbname.gz"'
);
});

it('can generate a dump command with gzip compressor enabled', function () {
$dumpCommand = MongoDb::create()
->setDbName('dbname')
Expand Down
10 changes: 10 additions & 0 deletions tests/MySqlTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@
);
});

it('can generate a dump command using a database url', function () {
$dumpCommand = Mysql::create()
->setDatabaseUrl('mysql://username:password@hostname:3306/dbname')
->getDumpCommand('dump.sql', 'credentials.txt');

expect($dumpCommand)->toEqual(
'\'mysqldump\' --defaults-extra-file="credentials.txt" --skip-comments --extended-insert dbname > "dump.sql"'
);
});

it('can generate a dump command with columnstatistics', function () {
$dumpCommand = MySql::create()
->setDbName('dbname')
Expand Down
Loading