PHP code example of padosoft / laravel-pii-redactor
1. Go to this page and download the library: Download padosoft/laravel-pii-redactor library. Choose the download type require.
2. Extract the ZIP file and open the index.php.
3. Add this code to the index.php.
<?php
require_once('vendor/autoload.php');
/* Start to develop here. Best regards https://php-download.com/ */
$set = (new YamlCustomRuleLoader())->load(storage_path('app/pii-rules/it-albo.yaml'));
Pii::extend('custom_it_albo', new CustomRuleDetector('custom_it_albo', $set));
'packs' => [
// ItalyPack removed — codice fiscale / P.IVA / Italian phone / Italian address detectors NOT registered
],
// src/Packs/Iceland/Detectors/KennitalaDetector.php
namespace Padosoft\PiiRedactor\Packs\Iceland\Detectors;
use Padosoft\PiiRedactor\Detectors\Detection;
use Padosoft\PiiRedactor\Detectors\Detector;
final class KennitalaDetector implements Detector
{
public function name(): string
{
return 'kennitala';
}
public function detect(string $text): array
{
// 10 digits with mod-11 checksum on the first 9.
if (preg_match_all('/\b(\d{6}-?\d{4})\b/u', $text, $matches, PREG_OFFSET_CAPTURE) === false) {
return [];
}
$hits = [];
foreach ($matches[1] as $m) {
$value = preg_replace('/-/', '', (string) $m[0]);
if (! $this->validChecksum($value)) {
continue;
}
$hits[] = new Detection('kennitala', (string) $m[0], (int) $m[1], strlen((string) $m[0]));
}
return $hits;
}
private function validChecksum(string $kt): bool
{
// Weights: 3, 2, 7, 6, 5, 4, 3, 2 over the first 8 digits;
// ninth digit is the check digit; mod-11 with 11 - r mapping.
// ... real implementation here ...
return true;
}
}
// src/Packs/Iceland/IcelandPack.php
namespace Padosoft\PiiRedactor\Packs\Iceland;
use Padosoft\PiiRedactor\Packs\PackContract;
use Padosoft\PiiRedactor\Packs\Iceland\Detectors\KennitalaDetector;
final class IcelandPack implements PackContract
{
public function name(): string { return 'iceland'; }
public function countryCode(): string { return 'IS'; }
public function description(): string { return 'Icelandic kennitala (mod-11) + (future) phone / address detectors.'; }
public function detectors(): array
{
return [
new KennitalaDetector(),
];
}
}
// config/pii-redactor.php
'packs' => [
\Padosoft\PiiRedactor\Packs\Italy\ItalyPack::class,
\Padosoft\PiiRedactor\Packs\Iceland\IcelandPack::class, // your new pack
],
use Padosoft\PiiRedactor\Facades\Pii;
// Default mask strategy.
$clean = Pii::redact('Codice fiscale RSSMRA85T10A562S e P.IVA 12345678903.');
// "Codice fiscale [REDACTED] e P.IVA [REDACTED]."
// Audit a payload before redacting.
$report = Pii::scan('Email [email protected] IBAN IT60X0542811101000000123456.');
$report->countsByDetector(); // ['email' => 1, 'iban' => 1]
// One-off strategy override (without changing config).
use Padosoft\PiiRedactor\Strategies\HashStrategy;
$hashed = Pii::redact('[email protected]', new HashStrategy(salt: env('PII_REDACTOR_SALT')));
// "[hash:f72a1b09abc12345]" (16 hex chars — 64-bit namespace)
use Padosoft\PiiRedactor\Strategies\TokeniseStrategy;
$strategy = new TokeniseStrategy(salt: env('PII_REDACTOR_SALT'));
// Tokenise — same input always produces the same token under a fixed salt.
$redacted = Pii::redact($chatLog, $strategy);
// ... ship $redacted to a downstream system that does NOT need the originals ...
// Later, on the secure side, rehydrate when an auditor requests it.
$auditPayload = $strategy->detokeniseString($redacted);
use Padosoft\PiiRedactor\Detectors\Detection;
use Padosoft\PiiRedactor\Detectors\Detector;
use Padosoft\PiiRedactor\Facades\Pii;
class CodiceIscrizioneAlboDetector implements Detector
{
public function name(): string { return 'custom_albo'; }
public function detect(string $text): array
{
if (preg_match_all('/ISCR-\d{6,}/', $text, $matches, PREG_OFFSET_CAPTURE) === false) {
return [];
}
$hits = [];
foreach ($matches[0] as $m) {
$hits[] = new Detection('custom_albo', (string) $m[0], (int) $m[1], strlen((string) $m[0]));
}
return $hits;
}
}
Pii::extend('custom_albo', new CodiceIscrizioneAlboDetector);
declare(strict_types=1);
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Padosoft\PiiRedactor\RedactorEngine;
use Symfony\Component\HttpFoundation\Response;
final class RedactChatPii
{
public function __construct(
private readonly RedactorEngine $engine,
) {}
public function handle(Request $request, Closure $next): Response
{
// Gate 1 — the package's OWN master switch
// (`PII_REDACTOR_ENABLED` env / `pii-redactor.enabled` config).
// When the package is disabled at the env level, every call
// path skips redaction.
if (! (bool) config('pii-redactor.enabled', false)) {
return $next($request);
}
// Gate 2 — your host-app integration knob.
// Substitute `myapp.pii.middleware_active` with the config key
// your project actually uses.
if (! (bool) config('myapp.pii.middleware_active', false)) {
return $next($request);
}
$content = $request->input('content');
if (! is_string($content) || $content === '') {
return $next($request);
}
$request->merge([
'content' => $this->engine->redact($content),
]);
return $next($request);
}
}
// tests/Architecture/PiiMiddlewareScopeTest.php
final class PiiMiddlewareScopeTest extends TestCase
{
/**
* Substrings — NOT prefixes. A bare `admin` URI does not start
* with `admin/`, so a prefix match would let the binding reach
* the root admin endpoints unnoticed.
*/
private const FORBIDDEN_SUBSTRINGS = ['admin', 'ingest'];
public function test_redact_chat_pii_is_not_bound_to_admin_or_ingest_routes(): void
{
$router = $this->app->make(\Illuminate\Routing\Router::class);
foreach ($router->getRoutes() as $route) {
$bag = array_merge((array) $route->middleware(), (array) $route->gatherMiddleware());
if (! in_array('redact-chat-pii', $bag, true) && ! in_array(\App\Http\Middleware\RedactChatPii::class, $bag, true)) {
continue;
}
foreach (self::FORBIDDEN_SUBSTRINGS as $forbidden) {
$this->assertStringNotContainsString($forbidden, $route->uri());
}
}
}
}
use Padosoft\PiiRedactor\RedactorEngine;
use Padosoft\PiiRedactor\Strategies\MaskStrategy;
final class EmbeddingCacheService
{
public function __construct(
private readonly EmbeddingProvider $provider,
private readonly RedactorEngine $engine,
) {}
/** @param list<string> $texts */
public function generate(array $texts): EmbeddingsResponse
{
if (config('pii-redactor.enabled') && config('myapp.pii.redact_before_embeddings')) {
// Construct mask explicitly from the package's mask_token
// so the host's `PII_REDACTOR_MASK_TOKEN` override is honoured.
// Autowiring `app(MaskStrategy::class)` would create a fresh
// instance with the hard-coded `[REDACTED]` default and skip
// the configured token entirely.
$mask = new MaskStrategy(
(string) config('pii-redactor.mask_token', '[REDACTED]'),
);
$texts = array_map(
fn (string $t): string => $this->engine->redact($t, $mask),
$texts,
);
}
// Hash for cache key + send to provider — both now see the masked text.
// ...
}
}
final class IngestExternalChatJob implements ShouldQueue
{
public function handle(RedactorEngine $engine): void
{
$body = $this->payload['message'] ?? null;
if (! is_string($body) || $body === '') {
ChatLog::create(['body' => $body, /* ... */]);
return;
}
if (config('pii-redactor.enabled') && config('myapp.pii.redact_jobs')) {
$body = $engine->redact($body);
}
ChatLog::create(['body' => $body, /* ... */]);
}
}