Skip to content

Commit 1027775

Browse files
committed
feature: experiment connection provider api for standalone database URL management
1 parent 65a599b commit 1027775

9 files changed

Lines changed: 410 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Next
44

5+
* [feature] 🌟 Experimental `ConnectionProvider` for standalone usage where fetching the database connection DSN is delegated to user custom code (#245).
56
* [feature] 🌟 String pattern anonymizer, build complex strings by fetching values from other anonymizers.
67

78
## 2.0.3
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
# Standalone connection provider
2+
3+
When dealing with edge case environement where a static database URL cannot
4+
work, the *connection provider* feature may help you.
5+
6+
Basic idea is to delegate the database connection URL string to a custom function.
7+
8+
:::warning
9+
This feature is experimental and API is subject to change in the future.
10+
:::
11+
12+
## Basic example
13+
14+
Use case: you are working in a multi-tenant environment, each client database
15+
has the same database schema, but has different database credentials.
16+
17+
Let's consider you already have a central component you can access using PHP code
18+
which allows you to find a client database connection:
19+
20+
```php
21+
namespace MyApp;
22+
23+
class MyClientInstanceRegistry
24+
{
25+
public static function getDatabaseCredentialsForClient(string $id): array;
26+
}
27+
```
28+
29+
We are making this example simple for educational purpose, but in real life
30+
use cases you probably will have a more complex component initialization process
31+
for this.
32+
33+
And you have the following anonymization configuration:
34+
35+
```yaml
36+
connections:
37+
any_client: pgsql://clientid:clientpassword@somehostname/client1
38+
39+
anonymization:
40+
any_client:
41+
users:
42+
lastname: fr-fr.lastname
43+
```
44+
45+
Your main obstacle here is to give the correct database URL for the client you
46+
with to act upon. Let's consider that your solution is to use an environment
47+
variable for acting upon each database independently, such as:
48+
49+
```sh
50+
CLIENT_ID="client1" vendor/bin/db-tools anonymize
51+
```
52+
53+
Following chapter will guide you throught three different methods to achieve this.
54+
55+
## Implementing the connection provider interface
56+
57+
You can implement the `ConnectionProvider` interface as such:
58+
59+
```php
60+
namespace MyApp\DbToolsBundle;
61+
62+
use MakinaCorpus\DbToolsBundle\Bridge\Standalone\ConnectionProvider;
63+
64+
class MyClientConnectionProvider implements ConnectionProvider
65+
{
66+
#[\Override]
67+
public function createConnectionDsn(string $name): string
68+
{
69+
if (!$clientId = getenv('CLIENT_ID')) {
70+
throw new \RuntimeException("Did you forget to set the CLIENT_ID environment variable?");
71+
}
72+
73+
$credentials = MyClientInstanceRegistry::getDatabaseCredentialsForClient($clientId);
74+
75+
return \sprintf(
76+
'pgsql://%s:%s@%s:%d/%s?some_option=some_value',
77+
\rawurlencode($credentials['username']),
78+
\rawurlencode($credentials['password']),
79+
\rawurlencode($credentials['hostname']),
80+
$credentials['port'],
81+
\rawurlencode($credentials['database']),
82+
);
83+
}
84+
}
85+
```
86+
87+
Then adapt your YAML configuration:
88+
89+
```yaml
90+
connections:
91+
any_client: MyApp\DbToolsBundle\MyClientConnectionProvider
92+
```
93+
94+
## Using an invokable class
95+
96+
You otherwise simply can write any class implenting the `__invoke()` method:
97+
98+
```php
99+
namespace MyApp\DbToolsBundle;
100+
101+
class MyInvokableClientConnectionProvider
102+
{
103+
public function __invoke()
104+
{
105+
if (!$clientId = getenv('CLIENT_ID')) {
106+
throw new \RuntimeException("Did you forget to set the CLIENT_ID environment variable?");
107+
}
108+
109+
$credentials = MyClientInstanceRegistry::getDatabaseCredentialsForClient($clientId);
110+
111+
return \sprintf(
112+
'pgsql://%s:%s@%s:%d/%s?some_option=some_value',
113+
\rawurlencode($credentials['username']),
114+
\rawurlencode($credentials['password']),
115+
\rawurlencode($credentials['hostname']),
116+
$credentials['port'],
117+
\rawurlencode($credentials['database']),
118+
);
119+
}
120+
}
121+
```
122+
123+
Then adapt your YAML configuration:
124+
125+
```yaml
126+
connections:
127+
any_client: MyApp\DbToolsBundle\MyInvokableClientConnectionProvider
128+
```
129+
130+
## Using a class static method
131+
132+
You can use an object static method:
133+
134+
```php
135+
namespace MyApp\SomeNamespace;
136+
137+
class SomeExistingClass
138+
{
139+
// ... your other code (or not).
140+
141+
public static function myDbToolsConnectionProvider()
142+
{
143+
if (!$clientId = getenv('CLIENT_ID')) {
144+
throw new \RuntimeException("Did you forget to set the CLIENT_ID environment variable?");
145+
}
146+
147+
$credentials = MyClientInstanceRegistry::getDatabaseCredentialsForClient($clientId);
148+
149+
return \sprintf(
150+
'pgsql://%s:%s@%s:%d/%s?some_option=some_value',
151+
\rawurlencode($credentials['username']),
152+
\rawurlencode($credentials['password']),
153+
\rawurlencode($credentials['hostname']),
154+
$credentials['port'],
155+
\rawurlencode($credentials['database']),
156+
);
157+
}
158+
}
159+
```
160+
161+
Then adapt your YAML configuration:
162+
163+
```yaml
164+
connections:
165+
any_client: MyApp\SomeNamespace\SomeExistingClass::myDbToolsConnectionProvider
166+
```
167+
168+
## Using an arbitrary function
169+
170+
Or simply use any PHP function which was loaded using the autoloader:
171+
172+
```php
173+
function my_dbtools_client_connection_provider(): string
174+
{
175+
if (!$clientId = getenv('CLIENT_ID')) {
176+
throw new \RuntimeException("Did you forget to set the CLIENT_ID environment variable?");
177+
}
178+
179+
$credentials = MyClientInstanceRegistry::getDatabaseCredentialsForClient($clientId);
180+
181+
return \sprintf(
182+
'pgsql://%s:%s@%s:%d/%s?some_option=some_value',
183+
\rawurlencode($credentials['username']),
184+
\rawurlencode($credentials['password']),
185+
\rawurlencode($credentials['hostname']),
186+
$credentials['port'],
187+
\rawurlencode($credentials['database']),
188+
);
189+
}
190+
```
191+
192+
Then adapt your YAML configuration:
193+
194+
```yaml
195+
connections:
196+
any_client: my_dbtools_client_connection_provider
197+
```
198+
199+
## Notes
200+
201+
In all cases, the method signature function is always the same `connection_provider(string $name): string`.
202+
203+
You may omit the `$name` parameter if you don't intend to use it. It contains
204+
the connection name as specified in the YAML configuration `connections` value.

docs/content/configuration/reference.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,13 @@ connections: "pgsql://username:password@hostname:port/database?version=16.0&othe
434434
If you configure this parameter with a single URL string with no connection name,
435435
the connection name will be `default`.
436436
:::
437+
438+
:::tip
439+
If you cannot write a static database connection URL, the standalone configuration
440+
file allows you to delegate the connection URL creation to a PHP function.
441+
442+
See [standalone connection provider documentation](./connection_provider) for more information.
443+
:::
437444
@@@
438445

439446
:::tip
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MakinaCorpus\DbToolsBundle\Bridge\Standalone;
6+
7+
/**
8+
* Allow user to plug into the database session creation.
9+
*
10+
* This is intented for edge cases, such as multi-tenant architectures for
11+
* example where you would like to be able to change the database URL using
12+
* environment variables for example.
13+
*
14+
* As of now, this is intended to use in standalone flavor only, in most
15+
* cases when integrating with a framework such as Symfony or Laravel, the
16+
* framework will fully handle the database connections by itself.
17+
*
18+
* @experimental
19+
*/
20+
interface ConnectionProvider
21+
{
22+
/**
23+
* Dynamically create the database DSN, such as:
24+
* "pgsql://username:password@hostname:port/database?version=16.0"
25+
*
26+
* If you handle a single connection using your implementation, you may
27+
* skip entirely using the $name parameter and simply return the DSN.
28+
*
29+
* If you manage more than one connection and $name is erroneous or
30+
* unknown, you are free to raise any exception, this will force the
31+
* process to stop.
32+
*/
33+
public function createConnectionDsn(string $name): string;
34+
}

src/Bridge/Standalone/StandaloneDatabaseSessionRegistry.php

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,26 @@ public function getDatabaseSession(string $name): DatabaseSession
5656

5757
protected function getConnectionUri(string $name): string
5858
{
59-
return $this->connections[$name] ?? throw new ConfigurationException(\sprintf("'%s': connection does not exist.", $name));
59+
$value = $this->connections[$name] ?? throw new ConfigurationException(\sprintf("'%s': connection does not exist.", $name));
60+
61+
$callable = null;
62+
63+
if (\is_callable($value)) {
64+
$callable = \Closure::fromCallable($value);
65+
} elseif (\class_exists($value)) {
66+
$object = new $value();
67+
68+
if (\is_callable($object)) {
69+
// Class implements __invoke().
70+
$callable = $object;
71+
} else {
72+
if (!$object instanceof ConnectionProvider) {
73+
throw new ConfigurationException(\sprintf("'%s': connection provider object must either implement %s or the __invoke() method.", $name, ConnectionProvider::class));
74+
}
75+
$callable = $object->createConnectionDsn(...);
76+
}
77+
}
78+
79+
return $callable ? $callable($name) : $value;
6080
}
6181
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MakinaCorpus\DbToolsBundle\Tests\Mock;
6+
7+
class ConnectionProviderInvokable
8+
{
9+
public function __invoke()
10+
{
11+
return 'vendor://user1:pass1@host1:2345/db1?opt1=val1';
12+
}
13+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MakinaCorpus\DbToolsBundle\Tests\Mock;
6+
7+
use MakinaCorpus\DbToolsBundle\Bridge\Standalone\ConnectionProvider;
8+
9+
class ConnectionProviderNormal implements ConnectionProvider
10+
{
11+
#[\Override]
12+
public function createConnectionDsn(string $name): string
13+
{
14+
return 'vendor://user3:pass3@host3:2345/db3?opt3=' . $name;
15+
}
16+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MakinaCorpus\DbToolsBundle\Tests\Mock;
6+
7+
class ConnectionProviderStatic
8+
{
9+
public static function createConnectionDsn(string $name): string
10+
{
11+
return 'vendor://user5:pass5@host5:2345/db5?opt5=' . $name;
12+
}
13+
}

0 commit comments

Comments
 (0)