Skip to content

Commit 0d5588b

Browse files
committed
Issue #178 - Laravel support
1 parent 69ab6bb commit 0d5588b

File tree

9 files changed

+646
-12
lines changed

9 files changed

+646
-12
lines changed

composer.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"doctrine/doctrine-bundle": "^2.10.0",
2828
"doctrine/orm": "^2.15|^3.0",
2929
"friendsofphp/php-cs-fixer": "^3.34",
30+
"laravel/framework": "^10.0|^11.0",
3031
"phpstan/phpstan": "^1.10",
3132
"phpunit/phpunit": "^10.4",
3233
"symfony/dependency-injection": "^6.0|^7.0",
@@ -71,5 +72,12 @@
7172
"@phpcs --dry-run",
7273
"@phpstan"
7374
]
75+
},
76+
"extra": {
77+
"laravel": {
78+
"providers": [
79+
"MakinaCorpus\\DbToolsBundle\\Bridge\\Laravel\\DbToolsServiceProvider"
80+
]
81+
}
7482
}
7583
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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+
try {
22+
$data = require $this->file;
23+
} catch (\Throwable $e) {
24+
throw new ConfigurationException(\sprintf(
25+
"An error occurred when loading \"%s\" configuration file: %s",
26+
$this->file,
27+
$e->getMessage()
28+
), 0, $e);
29+
}
30+
31+
if (!\is_array($data)) {
32+
throw new ConfigurationException(\sprintf(
33+
"File \"%s\" is not a valid PHP anonymization configuration file (must return an array).",
34+
$this->file
35+
));
36+
}
37+
38+
return $data;
39+
}
40+
}
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
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\Backupper\BackupperFactory;
17+
use MakinaCorpus\DbToolsBundle\Bridge\Symfony\DependencyInjection\DbToolsConfiguration;
18+
use MakinaCorpus\DbToolsBundle\Command\Anonymization\AnonymizeCommand;
19+
use MakinaCorpus\DbToolsBundle\Command\Anonymization\AnonymizerListCommand;
20+
use MakinaCorpus\DbToolsBundle\Command\Anonymization\CleanCommand;
21+
use MakinaCorpus\DbToolsBundle\Command\Anonymization\ConfigDumpCommand;
22+
use MakinaCorpus\DbToolsBundle\Command\BackupCommand;
23+
use MakinaCorpus\DbToolsBundle\Command\CheckCommand;
24+
use MakinaCorpus\DbToolsBundle\Command\RestoreCommand;
25+
use MakinaCorpus\DbToolsBundle\Command\StatsCommand;
26+
use MakinaCorpus\DbToolsBundle\Configuration\Configuration;
27+
use MakinaCorpus\DbToolsBundle\Configuration\ConfigurationRegistry;
28+
use MakinaCorpus\DbToolsBundle\Database\DatabaseSessionRegistry;
29+
use MakinaCorpus\DbToolsBundle\Error\ConfigurationException;
30+
use MakinaCorpus\DbToolsBundle\Restorer\RestorerFactory;
31+
use MakinaCorpus\DbToolsBundle\Stats\StatsProviderFactory;
32+
use MakinaCorpus\DbToolsBundle\Storage\DefaultFilenameStrategy;
33+
use MakinaCorpus\DbToolsBundle\Storage\FilenameStrategyInterface;
34+
use MakinaCorpus\DbToolsBundle\Storage\Storage;
35+
use Psr\Log\LoggerInterface;
36+
use Symfony\Component\Config\Definition\Processor;
37+
38+
class DbToolsServiceProvider extends ServiceProvider
39+
{
40+
#[\Override]
41+
public function register(): void
42+
{
43+
$this->app->extend('config', function (Repository $config, Application $app) {
44+
$dbToolsConfig = $config->get('db-tools', []);
45+
$dbToolsConfig = ['db_tools' => $dbToolsConfig];
46+
47+
$processor = new Processor();
48+
$configuration = new DbToolsConfiguration(false, true);
49+
50+
$dbToolsConfig = $processor->processConfiguration($configuration, $dbToolsConfig);
51+
$dbToolsConfig = DbToolsConfiguration::finalizeConfiguration($dbToolsConfig);
52+
53+
$config->set('db-tools', $dbToolsConfig);
54+
55+
return $config;
56+
});
57+
58+
$this->app->singleton(DatabaseSessionRegistry::class, function (Application $app) {
59+
return new IlluminateDatabaseSessionRegistry($app->make('db'));
60+
});
61+
62+
$this->app->singleton(ConfigurationRegistry::class, function (Application $app) {
63+
/** @var Repository $config */
64+
$config = $app->make('config');
65+
66+
$defaultConfig = new Configuration(
67+
backupBinary: $config->get('db-tools.backup_binary'),
68+
backupExcludedTables: $config->get('db-tools.backup_excluded_tables'),
69+
backupExpirationAge: $config->get('db-tools.backup_expiration_age'),
70+
backupOptions: $config->get('db-tools.backup_options'),
71+
backupTimeout: $config->get('db-tools.backup_timeout'),
72+
restoreBinary: $config->get('db-tools.restore_binary'),
73+
restoreOptions: $config->get('db-tools.restore_options'),
74+
restoreTimeout: $config->get('db-tools.restore_timeout'),
75+
storageDirectory: $config->get('db-tools.storage_directory', $app->storagePath('db_tools')),
76+
storageFilenameStrategy: $config->get('db-tools.storage_filename_strategy'),
77+
);
78+
79+
$connectionConfigs = [];
80+
foreach ($config->get('db-tools.connections', []) as $name => $data) {
81+
$connectionConfigs[$name] = new Configuration(
82+
backupBinary: $data['backup_binary'] ?? null,
83+
backupExcludedTables: $data['backup_excluded_tables'] ?? null,
84+
backupExpirationAge: $data['backup_expiration_age'] ?? null,
85+
backupOptions: $data['backup_options'] ?? null,
86+
backupTimeout: $data['backup_timeout'] ?? null,
87+
restoreBinary: $data['restore_binary'] ?? null,
88+
restoreOptions: $data['restore_options'] ?? null,
89+
restoreTimeout: $data['restore_timeout'] ?? null,
90+
parent: $defaultConfig,
91+
storageDirectory: $data['storage_directory'] ?? null,
92+
storageFilenameStrategy: $data['storage_filename_strategy'] ?? null,
93+
);
94+
}
95+
96+
return new ConfigurationRegistry(
97+
$defaultConfig,
98+
$connectionConfigs,
99+
$config->get('database.default')
100+
);
101+
});
102+
103+
$this->app->singleton(Storage::class, function (Application $app) {
104+
/** @var ConfigurationRegistry $configRegistry */
105+
$configRegistry = $app->make(ConfigurationRegistry::class);
106+
$connections = $configRegistry->getConnectionConfigAll();
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.",
152+
$file
153+
));
154+
}
155+
156+
$ext = \substr($file, $pos + 1);
157+
$loader = match ($ext) {
158+
'php' => new PhpFileLoader($file, $connectionName),
159+
'yml', 'yaml' => new YamlLoader($file, $connectionName),
160+
default => throw new ConfigurationException(\sprintf(
161+
"File extension \"%s\" is unsupported (given path: \"%s\").",
162+
$ext,
163+
$file
164+
)),
165+
};
166+
167+
$factory->addConfigurationLoader($loader);
168+
}
169+
170+
foreach ($config->get('db-tools.anonymization', []) as $connectionName => $array) {
171+
$factory->addConfigurationLoader(new ArrayLoader($array, $connectionName));
172+
}
173+
}
174+
);
175+
176+
$this->app->singleton(AnonymizerRegistry::class, function (Application $app) {
177+
/** @var Repository $config */
178+
$config = $app->make('config');
179+
180+
// Validate user-given anonymizer paths.
181+
$anonymizerPaths = $config->get('db-tools.anonymizer_paths');
182+
foreach ($anonymizerPaths as $path) {
183+
if (!\is_dir($path)) {
184+
throw new \InvalidArgumentException(\sprintf(
185+
'"db_tools.anonymizer_paths": path "%s" does not exist',
186+
$path
187+
));
188+
}
189+
}
190+
191+
// Set the default anonymizer directory only if the folder
192+
// exists in order to avoid "directory does not exist" errors.
193+
$defaultDirectory = $app->basePath('app/Anonymizer');
194+
if (\is_dir($defaultDirectory)) {
195+
$anonymizerPaths[] = $defaultDirectory;
196+
}
197+
198+
return new AnonymizerRegistry($anonymizerPaths);
199+
});
200+
201+
$this->app->singleton(BackupperFactory::class, function (Application $app) {
202+
return new BackupperFactory(
203+
$app->make(DatabaseSessionRegistry::class),
204+
$app->make(ConfigurationRegistry::class),
205+
$app->make(LoggerInterface::class),
206+
);
207+
});
208+
209+
$this->app->singleton(RestorerFactory::class, function (Application $app) {
210+
return new RestorerFactory(
211+
$app->make(DatabaseSessionRegistry::class),
212+
$app->make(ConfigurationRegistry::class),
213+
$app->make(LoggerInterface::class),
214+
);
215+
});
216+
217+
$this->app->singleton(StatsProviderFactory::class, function (Application $app) {
218+
return new StatsProviderFactory($app->make(DatabaseSessionRegistry::class));
219+
});
220+
221+
// Inject the default database connection name to services
222+
// which require it.
223+
$this->app
224+
->when([
225+
AnonymizeCommand::class,
226+
BackupCommand::class,
227+
RestoreCommand::class,
228+
])
229+
->needs('$connectionName')
230+
->giveConfig('database.default')
231+
;
232+
$this->app
233+
->when([
234+
CheckCommand::class,
235+
CleanCommand::class,
236+
StatsCommand::class,
237+
])
238+
->needs('$defaultConnectionName')
239+
->giveConfig('database.default')
240+
;
241+
}
242+
243+
/**
244+
* Bootstrap any package services.
245+
*/
246+
public function boot(): void
247+
{
248+
AboutCommand::add('DbToolsBundle', fn () => ['Version' => '2.0.0']);
249+
250+
$this->publishes([
251+
__DIR__ . '/Resources/config/db-tools.php' => $this->app->configPath('db-tools.php'),
252+
]);
253+
254+
if ($this->app->runningInConsole()) {
255+
$this->commands([
256+
AnonymizeCommand::class,
257+
AnonymizerListCommand::class,
258+
BackupCommand::class,
259+
CheckCommand::class,
260+
CleanCommand::class,
261+
ConfigDumpCommand::class,
262+
RestoreCommand::class,
263+
StatsCommand::class,
264+
]);
265+
}
266+
}
267+
}

0 commit comments

Comments
 (0)