Created
December 15, 2024 15:29
-
-
Save phpfour/4bac71aab5d91802f5e7a54985bf6437 to your computer and use it in GitHub Desktop.
Laravel Anonymization
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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