Skip to content

Instantly share code, notes, and snippets.

@phpfour
Created December 15, 2024 15:29
Show Gist options
  • Save phpfour/4bac71aab5d91802f5e7a54985bf6437 to your computer and use it in GitHub Desktop.
Save phpfour/4bac71aab5d91802f5e7a54985bf6437 to your computer and use it in GitHub Desktop.
Laravel Anonymization
<?php declare(strict_types=1);
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\File;
use ReflectionClass;
class AnonymizeCommand extends Command
{
/** @inheritDoc */
protected $signature = 'app:anonymize';
/** @inheritDoc */
protected $description = 'Anonymize the database model data';
/** @var array<class-string> */
protected array $classes = [];
// phpcs:ignore SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh
public function handle(): int
{
if (App::environment('production')) {
$this->error('This command can only be run in non-production environments.');
return self::FAILURE;
}
$this->loadAnonymizers();
if (empty($this->classes)) {
$this->info('No anonymizers found.');
return self::SUCCESS;
}
foreach ($this->classes as $class) {
$model = $class::model();
$count = $model::count();
if ($count === 0) {
continue;
}
$this->info("Anonymizing {$count} records in {$model}...");
$model::cursor()->each(function ($record) use ($class) {
if ($class::shouldSkip($record)) {
return;
}
$class::anonymize($record);
});
$this->info("Anonymized {$count} records in {$model}.");
}
return self::SUCCESS;
}
// phpcs:ignore SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh
private function loadAnonymizers(): void
{
$dir = base_path().'/app/Anonymizers';
$namespace = 'App\\Anonymizers\\';
if (! File::isDirectory($dir)) {
return;
}
foreach (File::allFiles($dir) as $file) {
$filename = $file->getFilenameWithoutExtension();
$class = $namespace.$filename;
if (class_exists($class)) {
try {
if ($this->isValidService($class)) {
$this->classes[] = $class;
}
} catch (\ReflectionException) {
continue; // Handle the exception if the class could not be reflected
}
}
}
}
private function isValidService(string $class): bool
{
$interface = 'App\Contracts\Anonymizer';
$reflection = new ReflectionClass($class);
return $reflection->isInstantiable()
&& $reflection->hasMethod('anonymize')
&& $reflection->implementsInterface($interface);
}
}
<?php declare(strict_types=1);
namespace App\Contracts;
use Illuminate\Database\Eloquent\Model;
interface Anonymizer
{
public static function model(): string;
public static function anonymize(Model $model): void;
public static function shouldSkip(Model $model): bool;
}
<?php declare(strict_types=1);
namespace App\Anonymizers;
use Faker\Factory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Hash;
use App\Contracts\Anonymizer;
use App\Models\User;
final class UserAnonymizer implements Anonymizer
{
// phpcs:ignore SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh
public static function anonymize(Model $model): void
{
$faker = Factory::create();
assert($model instanceof User);
$model->email = $faker->unique()->email();
if ($model->phone_number !== null) {
$model->phone_number = $faker->phoneNumber;
}
if ($model->first_name !== null) {
$model->first_name = $faker->firstName;
}
if ($model->last_name !== null) {
$model->last_name = $faker->lastName;
}
if ($model->address_1 !== null) {
$model->address_1 = $faker->streetAddress;
}
if ($model->address_2 !== null) {
$model->address_2 = $faker->secondaryAddress;
}
if ($model->city !== null) {
$model->city = $faker->city;
}
if ($model->state !== null) {
$model->state = $faker->state;
}
if ($model->postal_code !== null) {
$model->postal_code = $faker->postcode;
}
$model->password = Hash::make('password');
$model->save();
}
public static function model(): string
{
return User::class;
}
public static function shouldSkip(Model $model): bool
{
assert($model instanceof User);
return str_contains($model->email, '@klasio.com');
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment