Skip to content

Commit ad4dfaa

Browse files
committed
Issue #178 - Laravel support
1 parent a4fddaf commit ad4dfaa

File tree

5 files changed

+614
-0
lines changed

5 files changed

+614
-0
lines changed

composer.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@
2727
"doctrine/doctrine-bundle": "^2.10.0",
2828
"doctrine/orm": "^2.15|^3.0",
2929
"friendsofphp/php-cs-fixer": "^3.34",
30+
"illuminate/console": "^11.0",
31+
"illuminate/database": "^11.0",
32+
"illuminate/support": "^11.0",
3033
"phpstan/phpstan": "^1.10",
3134
"phpunit/phpunit": "^10.4",
3235
"symfony/dependency-injection": "^6.0|^7.0",
@@ -69,5 +72,12 @@
6972
"@phpcs --dry-run",
7073
"@phpstan"
7174
]
75+
},
76+
"extra": {
77+
"laravel": {
78+
"providers": [
79+
"MakinaCorpus\\DbToolsBundle\\Bridge\\Laravel\\DbToolsServiceProvider"
80+
]
81+
}
7282
}
7383
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MakinaCorpus\DbToolsBundle\Anonymization\Config\Loader;
6+
7+
use MakinaCorpus\DbToolsBundle\Error\ConfigurationException;
8+
9+
class PhpFileLoader extends ArrayLoader
10+
{
11+
public function __construct(
12+
private string $file,
13+
string $connectionName = 'default',
14+
) {
15+
parent::__construct([], $connectionName);
16+
}
17+
18+
#[\Override]
19+
protected function getData(): array
20+
{
21+
$data = require $this->file;
22+
23+
if (!\is_array($data)) {
24+
throw new ConfigurationException(\sprintf(
25+
"File \"%s\" is not a valid PHP anonymization configuration file (must return an array).",
26+
$this->file
27+
));
28+
}
29+
30+
return $data;
31+
}
32+
}
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MakinaCorpus\DbToolsBundle\Bridge\Laravel;
6+
7+
use Illuminate\Contracts\Config\Repository;
8+
use Illuminate\Contracts\Foundation\Application;
9+
use Illuminate\Foundation\Console\AboutCommand;
10+
use Illuminate\Support\ServiceProvider;
11+
use MakinaCorpus\DbToolsBundle\Anonymization\AnonymizatorFactory;
12+
use MakinaCorpus\DbToolsBundle\Anonymization\Anonymizer\AnonymizerRegistry;
13+
use MakinaCorpus\DbToolsBundle\Anonymization\Config\Loader\ArrayLoader;
14+
use MakinaCorpus\DbToolsBundle\Anonymization\Config\Loader\PhpFileLoader;
15+
use MakinaCorpus\DbToolsBundle\Anonymization\Config\Loader\YamlLoader;
16+
use MakinaCorpus\DbToolsBundle\Bridge\Symfony\DependencyInjection\DbToolsConfiguration;
17+
use MakinaCorpus\DbToolsBundle\Command\Anonymization\AnonymizeCommand;
18+
use MakinaCorpus\DbToolsBundle\Command\Anonymization\AnonymizerListCommand;
19+
use MakinaCorpus\DbToolsBundle\Command\Anonymization\CleanCommand;
20+
use MakinaCorpus\DbToolsBundle\Command\Anonymization\ConfigDumpCommand;
21+
use MakinaCorpus\DbToolsBundle\Command\BackupCommand;
22+
use MakinaCorpus\DbToolsBundle\Command\CheckCommand;
23+
use MakinaCorpus\DbToolsBundle\Command\RestoreCommand;
24+
use MakinaCorpus\DbToolsBundle\Command\StatsCommand;
25+
use MakinaCorpus\DbToolsBundle\Configuration\Configuration;
26+
use MakinaCorpus\DbToolsBundle\Configuration\ConfigurationRegistry;
27+
use MakinaCorpus\DbToolsBundle\Database\DatabaseSessionRegistry;
28+
use MakinaCorpus\DbToolsBundle\Error\ConfigurationException;
29+
use MakinaCorpus\DbToolsBundle\Storage\DefaultFilenameStrategy;
30+
use MakinaCorpus\DbToolsBundle\Storage\FilenameStrategyInterface;
31+
use MakinaCorpus\DbToolsBundle\Storage\Storage;
32+
use Symfony\Component\Config\Definition\Processor;
33+
34+
class DbToolsServiceProvider extends ServiceProvider
35+
{
36+
public function register(): void
37+
{
38+
$this->app->extend('config', function (Repository $config, Application $app) {
39+
$dbToolsConfig = $config->get('db-tools', []);
40+
$dbToolsConfig = ['db_tools' => $dbToolsConfig];
41+
42+
$processor = new Processor();
43+
$configuration = new DbToolsConfiguration(false, true);
44+
45+
$dbToolsConfig = $processor->processConfiguration($configuration, $dbToolsConfig);
46+
$dbToolsConfig = DbToolsConfiguration::fixLegacyOptions($dbToolsConfig);
47+
$dbToolsConfig = DbToolsConfiguration::appendPostConfig($dbToolsConfig);
48+
49+
$config->set('db-tools', $dbToolsConfig);
50+
51+
return $config;
52+
});
53+
54+
$this->app->singleton(DatabaseSessionRegistry::class, function (Application $app) {
55+
return new LaravelDatabaseSessionRegistry($app->make('db'));
56+
});
57+
58+
$this->app->singleton(ConfigurationRegistry::class, function (Application $app) {
59+
/** @var Repository $config */
60+
$config = $app->make('config');
61+
62+
$defaultConfig = new Configuration(
63+
backupBinary: $config->get('db-tools.backup_binary', '/usr/bin/pg_dump'),
64+
backupExcludedTables: $config->get('db-tools.backup_excluded_tables'),
65+
backupExpirationAge: $config->get('db-tools.backup_expiration_age', '3 months ago'),
66+
backupOptions: $config->get('db-tools.backup_options', '-Z 5 --lock-wait-timeout=120'),
67+
backupTimeout: $config->get('db-tools.backup_timeout', 600),
68+
restoreBinary: $config->get('db-tools.restore_binary', '/usr/bin/pg_restore'),
69+
restoreOptions: $config->get('db-tools.restore_options', '-j 2 --clean --if-exists --disable-triggers'),
70+
restoreTimeout: $config->get('db-tools.restore_timeout', 1800),
71+
storageDirectory: $config->get('db-tools.storage_directory', $app->storagePath('db_tools')),
72+
storageFilenameStrategy: $config->get('db-tools.storage_filename_strategy', 'default'),
73+
);
74+
75+
$connectionConfigs = [];
76+
foreach ($config->get('db-tools.connections', []) as $name => $data) {
77+
$connectionConfigs[$name] = new Configuration(
78+
backupBinary: $data['backup_binary'] ?? null,
79+
backupExcludedTables: $data['backup_excluded_tables'] ?? null,
80+
backupExpirationAge: $data['backup_expiration_age'] ?? null,
81+
backupOptions: $data['backup_options'] ?? null,
82+
backupTimeout: $data['backup_timeout'] ?? null,
83+
restoreBinary: $data['restore_binary'] ?? null,
84+
restoreOptions: $data['restore_options'] ?? null,
85+
restoreTimeout: $data['restore_timeout'] ?? null,
86+
parent: $defaultConfig,
87+
storageDirectory: $data['storage_directory'] ?? null,
88+
storageFilenameStrategy: $data['storage_filename_strategy'] ?? null,
89+
);
90+
}
91+
92+
return new ConfigurationRegistry(
93+
$defaultConfig,
94+
$connectionConfigs,
95+
$config->get('database.default')
96+
);
97+
});
98+
99+
$this->app->singleton(Storage::class, function (Application $app) {
100+
/** @var ConfigurationRegistry $configRegistry */
101+
$configRegistry = $app->make(ConfigurationRegistry::class);
102+
103+
$connections = $configRegistry->getConnectionConfigAll();
104+
if (empty($connections)) {
105+
$connections[$configRegistry->getDefaultConnection()] = $configRegistry->getDefaultConfig();
106+
}
107+
108+
// Register filename strategies.
109+
$strategies = [];
110+
foreach ($connections as $connectionName => $connection) {
111+
$strategyId = $connection->getStorageFilenameStrategy();
112+
113+
if ($strategyId === null || $strategyId === 'default' || $strategyId === 'datetime') {
114+
$strategy = new DefaultFilenameStrategy();
115+
} elseif ($app->bound($strategyId)) {
116+
$strategy = $app->make($strategyId);
117+
} elseif (\class_exists($strategyId)) {
118+
if (!\is_subclass_of($strategyId, FilenameStrategyInterface::class)) {
119+
throw new \InvalidArgumentException(\sprintf(
120+
'"db-tools.connections.%s.filename_strategy": class "%s" does not implement "%s"',
121+
$connectionName,
122+
$strategyId,
123+
FilenameStrategyInterface::class
124+
));
125+
}
126+
$strategy = $app->make($strategyId);
127+
} else {
128+
throw new \InvalidArgumentException(\sprintf(
129+
'"db-tools.connections.%s.filename_strategy": class or service "%s" does not exist or is not registered in container',
130+
$connectionName,
131+
$strategyId
132+
));
133+
}
134+
135+
$strategies[$connectionName] = $strategy;
136+
}
137+
138+
return new Storage($configRegistry, $strategies);
139+
});
140+
141+
$this->app->resolving(
142+
AnonymizatorFactory::class,
143+
function (AnonymizatorFactory $factory, Application $app): void {
144+
/** @var Repository $config */
145+
$config = $app->make('config');
146+
147+
foreach ($config->get('db-tools.anonymization_files', []) as $connectionName => $file) {
148+
// 0 is not a good index for extension, this fails for false and 0.
149+
if (!($pos = \strrpos($file, '.'))) {
150+
throw new ConfigurationException(\sprintf(
151+
"File extension cannot be guessed for \"%s\" file path.", $file
152+
));
153+
}
154+
155+
$ext = \substr($file, $pos + 1);
156+
$loader = match ($ext) {
157+
'php' => new PhpFileLoader($file, $connectionName),
158+
'yml', 'yaml' => new YamlLoader($file, $connectionName),
159+
default => throw new ConfigurationException(\sprintf(
160+
"File extension \"%s\" is unsupported (given path: \"%s\").", $ext, $file
161+
)),
162+
};
163+
164+
$factory->addConfigurationLoader($loader);
165+
}
166+
167+
foreach ($config->get('db-tools.anonymization', []) as $connectionName => $array) {
168+
$factory->addConfigurationLoader(new ArrayLoader($array, $connectionName));
169+
}
170+
}
171+
);
172+
173+
$this->app
174+
->when(AnonymizerRegistry::class)
175+
->needs('$paths')
176+
->give(function (Application $app) {
177+
// Validate user-given anonymizer paths.
178+
$anonymizerPaths = $app->make('config')->get('db-tools.anonymizer_paths');
179+
foreach ($anonymizerPaths as $path) {
180+
if (!\is_dir($path)) {
181+
throw new \InvalidArgumentException(\sprintf(
182+
'"db_tools.anonymizer_paths": path "%s" does not exist', $path
183+
));
184+
}
185+
}
186+
// Set the default anonymizer directory only if the folder
187+
// exists in order to avoid "directory does not exist" errors.
188+
$defaultDirectory = $app->basePath('app/Anonymizer');
189+
if (\is_dir($defaultDirectory)) {
190+
$anonymizerPaths[] = $defaultDirectory;
191+
}
192+
})
193+
;
194+
195+
/*$this->app->singleton(BackupperFactory::class, function (Application $app) {
196+
return new BackupperFactory(
197+
$app->make(DatabaseSessionRegistry::class),
198+
$app->make(ConfigurationRegistry::class),
199+
// Logger
200+
);
201+
});*/
202+
203+
/*$this->app->singleton(RestorerFactory::class, function (Application $app) {
204+
return new RestorerFactory(
205+
$app->make(DatabaseSessionRegistry::class),
206+
$app->make(ConfigurationRegistry::class),
207+
// Logger
208+
);
209+
});*/
210+
211+
/*$this->app->singleton(StatsProviderFactory::class, function (Application $app) {
212+
return new StatsProviderFactory($app->make(DatabaseSessionRegistry::class));
213+
});*/
214+
215+
/*$this->app
216+
->when([
217+
AnonymizatorFactory::class,
218+
BackupperFactory::class,
219+
RestorerFactory::class,
220+
StatsProviderFactory::class
221+
])
222+
->needs(LoggerInterface::class)
223+
->give(fn (Application $app) => $app->make('log'))
224+
;*/
225+
226+
$this->app
227+
->when([
228+
AnonymizeCommand::class,
229+
BackupCommand::class,
230+
RestoreCommand::class,
231+
])
232+
->needs('$connectionName')
233+
->giveConfig('database.default')
234+
;
235+
$this->app
236+
->when([
237+
CheckCommand::class,
238+
CleanCommand::class,
239+
StatsCommand::class,
240+
])
241+
->needs('$defaultConnectionName')
242+
->giveConfig('database.default')
243+
;
244+
}
245+
246+
/**
247+
* Bootstrap any package services.
248+
*/
249+
public function boot(): void
250+
{
251+
AboutCommand::add('DbToolsBundle', fn () => ['Version' => '2.0.0']);
252+
253+
$this->publishes([
254+
__DIR__ . '/Resources/config/db-tools.php' => $this->app->configPath('db-tools.php'),
255+
]);
256+
257+
if ($this->app->runningInConsole()) {
258+
$this->commands([
259+
AnonymizeCommand::class,
260+
AnonymizerListCommand::class,
261+
BackupCommand::class,
262+
CheckCommand::class,
263+
CleanCommand::class,
264+
ConfigDumpCommand::class,
265+
RestoreCommand::class,
266+
StatsCommand::class,
267+
]);
268+
}
269+
}
270+
}

0 commit comments

Comments
 (0)