Skip to content

Commit

Permalink
Add support for database URLs (#196)
Browse files Browse the repository at this point in the history
* add support for database urls

* fix: use static InvalidDatabaseUrl constructor
  • Loading branch information
mikerockett committed Jun 27, 2023
1 parent 1cce535 commit 4918ddc
Show file tree
Hide file tree
Showing 8 changed files with 242 additions and 20 deletions.
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

0 comments on commit 4918ddc

Please sign in to comment.