PHPackages                             padosoft/laravel-pii-redactor - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Security](/categories/security)
4. /
5. padosoft/laravel-pii-redactor

ActiveLibrary[Security](/categories/security)

padosoft/laravel-pii-redactor
=============================

Italian-first PII redaction pipeline for Laravel: deterministic regex + checksum-validated detectors (codice fiscale, P.IVA, IBAN), pluggable strategies (mask / hash / tokenise / drop), GDPR + EU AI Act ready.

v1.2.0(1mo ago)00[1 PRs](https://github.com/padosoft/laravel-pii-redactor/pulls)1Apache-2.0PHPPHP ^8.3CI passing

Since May 2Pushed 1mo agoCompare

[ Source](https://github.com/padosoft/laravel-pii-redactor)[ Packagist](https://packagist.org/packages/padosoft/laravel-pii-redactor)[ RSS](/packages/padosoft-laravel-pii-redactor/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (6)Dependencies (9)Versions (8)Used By (1)

laravel-pii-redactor
====================

[](#laravel-pii-redactor)

[![Tests](https://github.com/padosoft/laravel-pii-redactor/actions/workflows/ci.yml/badge.svg)](https://github.com/padosoft/laravel-pii-redactor/actions/workflows/ci.yml)[![Latest Version](https://camo.githubusercontent.com/e3e0e16df28e46c79ac6ec307aee962711a4e5ed4fd4730859e19f8c91bd7c35/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f7061646f736f66742f6c61726176656c2d7069692d7265646163746f722e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/padosoft/laravel-pii-redactor)[![PHP Version](https://camo.githubusercontent.com/51eb1d93f86f4a313354ebcb2d54323802de555c5f965dfce945a7c457fc7983/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f7061646f736f66742f6c61726176656c2d7069692d7265646163746f722e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/padosoft/laravel-pii-redactor)[![Laravel Version](https://camo.githubusercontent.com/8b3832fcd4d9ff6e48cf67b9b6ed8a94df5f239d799ef537e2ea5960cc1ab80a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c61726176656c2d31322e7825323025374325323031332e782d7265643f7374796c653d666c61742d737175617265266c6f676f3d6c61726176656c)](https://laravel.com)[![License](https://camo.githubusercontent.com/a39964403b3ad23090c20e0f4e86b834c7f13cd17f0fb9e042c3cce06f6a13b4/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4170616368652d2d322e302d626c75652e7376673f7374796c653d666c61742d737175617265)](LICENSE)[![Total Downloads](https://camo.githubusercontent.com/a7194bd2ecf1424892abc97c659ba1335a91d64f8a0b23a2523e92688a572e6e/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f7061646f736f66742f6c61726176656c2d7069692d7265646163746f722e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/padosoft/laravel-pii-redactor)

[![Laravel PII Redactor banner](resources/Laravel-PII-redactor-banner.png)](resources/Laravel-PII-redactor-banner.png)

> **EU-first PII redaction for Laravel — deterministic regex + checksum-validated detectors organised into opt-in country packs (Italy, Germany, Spain ship built-in; France / Netherlands / Portugal land in v1.2+), plus always-on multi-country detectors (email, IBAN mod-97 for every ISO 13616 country, credit card with Luhn) and a pluggable strategy layer (mask / hash / tokenise / drop) with persistent reverse-map storage (memory / database / cache), opt-in HuggingFace + spaCy NER drivers, and YAML custom-rule packs for tenant-specific identifiers. Zero external services in the default path, zero mandatory LLM cost, GDPR + EU AI Act ready.**

`laravel-pii-redactor` is the seventh deliverable of the [Padosoft v4.0 cycle](https://github.com/lopadova/AskMyDocs) (W7). It is a community Apache-2.0 package, **standalone-agnostic** (zero references to AskMyDocs / sister packages), and ships with the Padosoft AI vibe-coding pack so you can extend it with Claude Code or GitHub Copilot in minutes — not days.

```
use Padosoft\PiiRedactor\Facades\Pii;

$clean = Pii::redact('Codice fiscale RSSMRA85T10A562S, IBAN IT60X0542811101000000123456, mail: mario@example.com.');
// "Codice fiscale [REDACTED], IBAN [REDACTED], mail: [REDACTED]."

$report = Pii::scan('Telefono +39 333 1234567 e P.IVA 12345678903.');
// $report->countsByDetector() === ['phone_it' => 1, 'p_iva' => 1]
```

---

Table of contents
-----------------

[](#table-of-contents)

- [Why this package](#why-this-package)
- [Design rationale](#design-rationale)
- [Features at a glance](#features-at-a-glance)
- [🇪🇺 EU country pack architecture](#-eu-country-pack-architecture)
- [Build your own country pack — 3-step recipe](#build-your-own-country-pack--3-step-recipe)
- [Comparison vs alternatives](#comparison-vs-alternatives)
- [Installation](#installation)
- [Quick start](#quick-start)
- [Usage examples](#usage-examples)
- [Laravel integration recipes](#laravel-integration-recipes)
- [Web Panel UI](#web-panel-ui)
- [Admin panel readiness](#admin-panel-readiness)
- [Configuration reference](#configuration-reference)
- [Architecture](#architecture)
- [AI vibe-coding pack](#ai-vibe-coding-pack)
- [Testing — Default + Live](#testing--default--live)
- [Performance](#performance)
- [Roadmap](#roadmap)
- [Migration guide v0.x → v1.0](#migration-guide-v0x--v10)
- [Contributing](#contributing)
- [Security](#security)
- [License](#license)

---

Why this package
----------------

[](#why-this-package)

PII redaction is one of those domains where the existing options force a bad trade-off:

- **Build it yourself** with a few hand-crafted regexes — fast to write, but the moment a real Italian fiscal code shows up (16 alphanumeric characters with a checksum derived from a Decreto Ministeriale lookup table) your "good enough" regex starts emitting false positives that break audits.
- **Reach for Microsoft Presidio / AWS Comprehend / Google DLP** — robust, but they assume a US-centric set of identifiers. None of them validate the Italian `codice fiscale` checksum out of the box, and routing every chat-log line through a hosted PII service is operationally expensive and a GDPR amplifier.
- **Bolt an LLM-based redactor onto the pipeline** — works, but pays per-token to do something that is, fundamentally, a regular language problem.

`laravel-pii-redactor` covers the **deterministic** layer. v1.0 ships:

- **3 always-on multi-country detectors** — `email` (RFC-5321 shape), `iban` (ISO 13616 country-length table + mod-97 for **every** registered country, ~75), `credit_card` (Luhn).
- **3 shipped country packs** (v1.1):
    - **`ItalyPack`** — `codice_fiscale` (CIN checksum), `partita_iva` (Luhn-IT), `phone_it`, `address_it`.
    - **`GermanyPack`** (v1.1) — `steuer_id` (mod-11 ISO 7064 per §139b AO), `ust_idnr` (BMF Method 30 per §27a UStG), `phone_de`, `address_de`.
    - **`SpainPack`** (v1.1) — `dni` (23-letter checksum table per RD 1553/2005), `nie` (prefix-substituted DNI), `cif` (corporate ID), `phone_es`, `address_es`.
- **`PackContract` interface + `DetectorPackRegistry`** — opt-in jurisdiction bundles. Operate in Italy only? Keep `ItalyPack`. Operate across the EU? Add `GermanyPack` / `SpainPack` (shipped v1.1) or `FrancePack` (v1.2+ candidate). Operate outside Italy? Drop `ItalyPack` from the config.
- **4 pluggable replacement strategies** — `mask`, `hash`, `tokenise`, `drop`.
- **3 token-store drivers** — `memory` (default), `database` (Eloquent + shipped migration), `cache` (Redis / Memcached / DynamoDB / array).
- **2 production NER drivers (opt-in)** — `HuggingFaceNerDriver`, `SpaCyNerDriver`. Network calls fail open.
- **YAML custom-rule packs** — register tenant-specific detectors from `*.yaml` files; SP auto-registers when `pii-redactor.custom_rules.auto_register = true`.
- **Typed `DetectionReport`** — audit every redaction without re-running the engine.
- **Admin-ready headless APIs** — safe status snapshots, strategy factory, masked report formatter, token resolution, and custom-rule diagnostics for a separate Laravel 13 React/Tailwind admin package. See [Admin panel readiness](#admin-panel-readiness).

It is **deliberately small** and **deliberately offline by default**. You can extend it with custom detectors via `Pii::extend()` or your own country pack. The deterministic engine fits in ~200 lines of PHP, the v1.0 surface is locked under semver, and 300+ unit tests + a robustness suite describe every transition.

---

Design rationale
----------------

[](#design-rationale)

Five non-negotiable choices that drove the API:

### 1. EU-first via opt-in country packs. World-second.

[](#1-eu-first-via-opt-in-country-packs-world-second)

Every PII pipeline I have seen for Laravel either ignores European fiscal data or matches it with a bare regex that returns false positives on every retry CI run. **National identifiers need real code**: the Italian `codice fiscale` requires the official odd/even checksum table from the 1976 Decreto Ministeriale; the German Steuer-ID needs mod-11; the Spanish DNI needs a letter-checksum lookup; the French NIR needs mod-97. A regex alone won't do.

Hence **country packs**. v1.0 shipped `ItalyPack` as the reference implementation (4 Italian detectors with the full CIN checksum + Luhn-IT). v1.1 makes good on the promise with **two more concrete bundles** — `GermanyPack` (Steuer-ID mod-11 ISO 7064 per §139b AO + USt-IdNr BMF Method 30 per §27a UStG + German phone/address) and `SpainPack` (DNI 23-letter checksum table per RD 1553/2005 + NIE + CIF + Spanish phone/address). Both opt-in via a single FQCN in `config('pii-redactor.packs')`. The `PackContract` interface + `DetectorPackRegistry` make it equally trivial for the community to contribute `FrancePack`, `NetherlandsPack`, `PortugalPack` next — each as a self-contained bundle of detectors with checksum-source citations and 10/5 valid/invalid fixtures.

Multi-country detectors (`email`, `iban` with mod-97 for every ISO 13616 country, `credit_card` with Luhn) stay always-on regardless of which packs you load — they have no jurisdictional flavour.

### 2. Deterministic regex + checksum, no LLM in the hot path

[](#2-deterministic-regex--checksum-no-llm-in-the-hot-path)

Every first-party detector is a pure function of its input. No external HTTP call, no per-token cost, no rate limit. A 1 MB chat log redacts in ~280 ms and the output is identical on every machine. The optional NER layer (v0.3+) ships behind a config switch; the default path never touches a network.

### 3. Strategy is a runtime decision, not a compile-time one

[](#3-strategy-is-a-runtime-decision-not-a-compile-time-one)

The same detected match can be **masked** (`[REDACTED]` for human-facing logs), **hashed** (`[hash:abc123ef01234567]` for cross-record joins on pseudonymous data), **tokenised** (`[tok:email:abc123ef01234567]` with a reversible salt-derived map for forensic recovery), or **dropped** (empty string for forwarding to lossy systems). Switching strategy is a one-line override on `Pii::redact($text, new HashStrategy(...))` — no detector code changes.

### 4. Detector overlap is resolved deterministically

[](#4-detector-overlap-is-resolved-deterministically)

When two detectors emit overlapping byte ranges (e.g. an email-shaped string that also matches a phone heuristic), the engine keeps the **earlier** match (lower offset) and drops the latecomer. The behaviour is documented, tested, and predictable — callers can audit it via `Pii::scan()`.

### 5. Standalone-agnostic — zero AskMyDocs symbols

[](#5-standalone-agnostic--zero-askmydocs-symbols)

`laravel-pii-redactor` is a **community** package. It is not coupled to AskMyDocs, the sister patent-box tracker, the eval-harness, the Regolo driver, or any other Padosoft project. An architecture test (`tests/Architecture/StandaloneAgnosticTest.php`) walks `src/` with `RecursiveDirectoryIterator` on every CI run and asserts the forbidden-substring list (KnowledgeDocument, KbSearchService, AskMyDocs, PatentBoxTracker, LaravelFlow, EvalHarness, Regolo, ...) never appears.

---

Features at a glance
--------------------

[](#features-at-a-glance)

- **🇪🇺 EU country pack architecture** — `PackContract` interface + `DetectorPackRegistry` boots country packs from `config('pii-redactor.packs')`. **Three packs ship in v1.1**: `ItalyPack` (default), `GermanyPack` (opt-in), `SpainPack` (opt-in). `FrancePack`, `NetherlandsPack`, `PortugalPack` are community PRs welcome (see [CONTRIBUTING-PACKS.md](CONTRIBUTING-PACKS.md)).
- **3 always-on multi-country detectors** (no pack required):
    - `email` — pragmatic RFC-5321 shape match.
    - `iban` — ISO 13616 IBAN for every registered country (~75) + mod-97 verification.
    - `credit_card` — 13–19 digit PAN with Luhn validation.
- **`ItalyPack` (default — 4 detectors)**:
    - `codice_fiscale` — 16-char Italian fiscal code with full CIN checksum (Decreto Ministeriale 23/12/1976).
    - `p_iva` — 11-digit Italian VAT with Luhn-style checksum + zero-payload sentinel rejection.
    - `phone_it` — Italian mobile + landline (with optional `+39` / `0039` prefix).
    - `address_it` — Italian street address heuristic (Via / Viale / Piazza / Corso / Largo / Strada / Vicolo / Lungomare + compound forms `Via dei`, `Via della`, `Via d'…`); civic number + 5-digit CAP + city optional.
- **4 pluggable redaction strategies**: `MaskStrategy`, `HashStrategy` (deterministic, salt-derived, namespaced per detector), `TokeniseStrategy` (reversible pseudonymisation with `detokenise()` + `dumpMap()` / `loadMap()` for cross-process recovery), `DropStrategy`.
- **Persistent reverse-map storage (v0.2)** — `TokenStore` interface + `InMemoryTokenStore` (default, process-local) + `DatabaseTokenStore` (Eloquent-backed, shipped migration `pii_token_maps`). The same `[tok:...]` token detokenises across deploys / queue workers when the database driver is wired. Switch via `PII_REDACTOR_TOKEN_STORE=database` and run `php artisan vendor:publish --tag=pii-redactor-migrations && php artisan migrate`.
- **Audit-trail event (v0.2)** — opt-in `PiiRedactionPerformed` Laravel event fired after a `redact()` call that **produced at least one detection**, when `PII_REDACTOR_AUDIT_TRAIL=true` (or the structured `audit_trail.enabled` key is set). No-op redactions (engine disabled, empty input, zero detections) skip the dispatch — the event signals "redaction occurred", not "request processed". Event carries **counts only** (detector → match count, total, strategy name) — NEVER raw PII or redacted output. GDPR-friendly by construction.
- **NER drivers (v0.2 scaffold + v0.3 production)** — `NerDriver` interface + `StubNerDriver` (no-op default), `HuggingFaceNerDriver` (HuggingFace Inference API via `Http::`, opt-in via `PII_REDACTOR_HUGGINGFACE_API_KEY`), `SpaCyNerDriver` (generic spaCy HTTP server protocol returning `Doc.to_json()` shape, opt-in via `PII_REDACTOR_SPACY_SERVER_URL`). Both real drivers fail open on HTTP errors so a NER outage cannot block deterministic redaction. Driver detections merge into the same overlap-resolution pipeline as first-party detectors.
- **Cache-backed `TokenStore` (v0.3)** — third driver alongside `InMemoryTokenStore` and `DatabaseTokenStore`. Uses Laravel's `Illuminate\Contracts\Cache\Repository` so deployments swap between Redis / Memcached / DynamoDB / array (test) without touching package code. Maintains an explicit index entry so `dump()` / `clear()` work without scanning the backend keyspace. Optional TTL via `PII_REDACTOR_TOKEN_STORE_CACHE_TTL`. Switch with `PII_REDACTOR_TOKEN_STORE=cache`.
- **Custom-rule YAML packs (v0.3 + v1.0 auto-register)** — register tenant-specific detectors from `*.yaml` files. v1.0 adds an SP-level auto-register loop driven by `config('pii-redactor.custom_rules.packs')` so you can drop YAML packs into a config array and the SP wires them at boot. The host-controlled API still works for tenant-specific bootstrap logic: ```
    $set = (new YamlCustomRuleLoader())->load(storage_path('app/pii-rules/it-albo.yaml'));
    Pii::extend('custom_it_albo', new CustomRuleDetector('custom_it_albo', $set));
    ```

    Each rule has a `name` + PCRE `pattern` + optional `flags` (default `u`). Invalid PCRE is rejected at first-match time with a clear `CustomRuleException`. Useful for Italian professional registry IDs (`ISCR-...`, `Tess-XX-...`), tenant-specific account codes, project tracker identifiers, etc.
- **Live test suite (v0.3)** — `tests/Live/` houses opt-in tests against real APIs (HuggingFace, spaCy server). Each test self-skips unless `PII_REDACTOR_LIVE=1` AND its driver-specific credentials are set. CI runs `Unit` + `Architecture` only — Live tests are operator-driven. See `tests/Live/README.md` for the convention.
- **Typed `DetectionReport`** — `total()`, `countsByDetector()`, `samplesByDetector(cap)`, `toArray()`. Stable JSON shape for downstream auditors.
- **Admin-ready headless APIs** — `RedactorAdminInspector` exposes a secret-free runtime snapshot; `RedactionStrategyFactory` builds `mask` / `hash` / `tokenise` / `drop` strategies for admin previews; `DetectionReportFormatter` masks samples by default; `TokenResolutionService` detokenises through the configured `TokenStore` even when the current strategy is not `tokenise`; `CustomRulePackInspector` reports YAML pack health without registering detectors. Full implementation plan for the separate Laravel 13 + Vite + React + Tailwind UI package lives in [docs/admin-panel-architecture-plan.md](docs/admin-panel-architecture-plan.md).
- **`Pii::extend()` registry** for custom detectors (`custom_codice_iscrizione_albo`, project-specific account ids, etc.).
- **Artisan command** — `php artisan pii:scan path/to/file.txt --pretty` or `cat data | php artisan pii:scan --from=stdin` (samples masked by default; pass `--show-samples` for raw values during interactive forensics).
- **Standalone-agnostic** — zero coupling to AskMyDocs / sister packages, enforced by an architecture test.
- **PHP 8.3 / 8.4 / 8.5** × **Laravel 12 / 13** matrix. Pint + PHPStan level 6 + 400+ PHPUnit tests on every push.
- **Padosoft AI vibe-coding pack** (`.claude/`) — Claude Code skills (R36 review loop, R10–R37 rules) + agents (review pre-push) + commands (`/create-job`, `/domain-scaffold`).

---

🇪🇺 EU country pack architecture
-------------------------------

[](#-eu-country-pack-architecture)

**Why country packs exist.** Italian fiscal codes need PHP code with checksum logic. So do German Steuer-ID (mod-11), Spanish DNI (letter-checksum), French NIR (mod-97). Pure regex isn't enough. Each country needs its own bundle of detectors — but the package shouldn't ship all of EU's IDs by default if you only operate in Italy. **Hence packs**: opt-in jurisdiction bundles, registered via the `PackContract` interface and a config array.

```
Padosoft\PiiRedactor\
├── Detectors\                         (multi-country, always-on)
│   ├── EmailDetector (RFC-5321 shape)
│   ├── IbanDetector (ISO 13616 mod-97 — every EU country)
│   └── CreditCardDetector (Luhn)
└── Packs\
    ├── PackContract                   (interface)
    └── Italy\
        ├── ItalyPack                  (default — config('pii-redactor.packs'))
        │   └── detectors() returns:
        │       ├── CodiceFiscaleDetector (CIN checksum)
        │       ├── PartitaIvaDetector (Luhn-IT)
        │       ├── PhoneItalianDetector
        │       └── AddressItalianDetector

```

**Enable / disable example**:

```
// config/pii-redactor.php
'packs' => [
    \Padosoft\PiiRedactor\Packs\Italy\ItalyPack::class,
    // \Padosoft\PiiRedactor\Packs\Germany\GermanyPack::class, // shipped v1.1 — opt-in
    // \Padosoft\PiiRedactor\Packs\Spain\SpainPack::class,     // shipped v1.1 — opt-in
],
```

To disable Italy on an English-only deployment:

```
'packs' => [
    // ItalyPack removed — codice fiscale / P.IVA / Italian phone / Italian address detectors NOT registered
],
```

The multi-country detectors (Email, IBAN, CreditCard) keep working regardless — they are **never** part of a country pack because they have no jurisdictional flavour.

---

Build your own country pack — 3-step recipe
-------------------------------------------

[](#build-your-own-country-pack--3-step-recipe)

The recipe below uses Iceland (small, real European country, no community pack ships yet) as a "blank slate" example. The real `kennitala` checksum is mod-11 over the first 9 digits.

### Step 1 — Create the detector(s)

[](#step-1--create-the-detectors)

```
// 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;
    }
}
```

### Step 2 — Wrap them in a pack

[](#step-2--wrap-them-in-a-pack)

```
// 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(),
        ];
    }
}
```

### Step 3 — Register it

[](#step-3--register-it)

```
// config/pii-redactor.php
'packs' => [
    \Padosoft\PiiRedactor\Packs\Italy\ItalyPack::class,
    \Padosoft\PiiRedactor\Packs\Iceland\IcelandPack::class,  // your new pack
],
```

That's it. The ServiceProvider boots, the `DetectorPackRegistry` walks the config list, instantiates each pack, and feeds its `detectors()` into the engine. `Pii::redact()` and `Pii::scan()` now redact / report `kennitala` matches alongside the always-on detectors.

> **🚀 Contribute your country pack**
>
> Built a `GermanyPack` / `SpainPack` / `FrancePack` / etc. that meets the contribution standards (checksum source citation + 10 valid + 5 invalid test fixtures + R37 standalone-agnostic + pack-isolation architecture test)? **Open a PR** — see [CONTRIBUTING-PACKS.md](CONTRIBUTING-PACKS.md) for the workflow. Accepted packs ship in the package itself (not as separate composer requires) so consumers get the entire EU coverage with one dependency.

---

Comparison vs alternatives
--------------------------

[](#comparison-vs-alternatives)

✅ = supported out of the box · 🟡 = partial / requires custom code or paid tier · ❌ = not supported

### Platform &amp; deployment

[](#platform--deployment)

laravel-pii-redactorMicrosoft PresidioSpatie data-redactionAWS Comprehend PIIGoogle Cloud DLPNative Laravel facade + ServiceProvider✅ YES❌ NO (Python)✅ YES (different scope)❌ NO (AWS SDK)❌ NO (GCP SDK)`composer require` install✅ YES❌ NO✅ YES (different scope)❌ NO❌ NOAdmin web UI / dashboard✅ YES ([companion package](https://github.com/padosoft/laravel-pii-redactor-admin))🟡 Presidio Analyzer UI only❌ NO❌ Console only❌ Console onlyOperates entirely offline (default path)✅ YES✅ YES (self-hosted)✅ YES❌ NO (AWS API)❌ NO (GCP API)GDPR data-minimisation friendly✅ YES (no transit)✅ YES✅ YES❌ NO (US transit)❌ NO (US transit)Cost per 1M characters✅ EUR 0🟡 self-hosted compute✅ EUR 0❌ ~ EUR 1❌ ~ EUR 1.50### EU country detector coverage (deterministic, checksum-validated)

[](#eu-country-detector-coverage-deterministic-checksum-validated)

laravel-pii-redactorMicrosoft PresidioSpatie data-redactionAWS Comprehend PIIGoogle Cloud DLP🇮🇹 Codice fiscale (CIN checksum)✅ YES (`ItalyPack`)🟡 regex shape only❌ NO❌ NO🟡 regex shape only🇮🇹 Partita IVA (Luhn-IT)✅ YES (`ItalyPack`)❌ NO❌ NO❌ NO❌ NO🇩🇪 Steuer-ID (mod-11 ISO 7064 + §139b AO)✅ YES (`GermanyPack` v1.1)❌ NO❌ NO❌ NO🟡 regex only (no checksum)🇩🇪 USt-IdNr (BMF Method 30 mod-11)✅ YES (`GermanyPack` v1.1)❌ NO❌ NO❌ NO❌ NO🇪🇸 DNI / NIE (23-letter checksum)✅ YES (`SpainPack` v1.1)🟡 regex shape only❌ NO❌ NO🟡 regex shape only🇪🇸 CIF (AEAT dual digit/letter control)✅ YES (`SpainPack` v1.1)❌ NO❌ NO❌ NO❌ NO🇫🇷 NIR / SSN (mod-97)🟡 v1.2+ candidate (community PR)🟡 regex shape only❌ NO❌ NO🟡 regex shape only🇳🇱 BSN (eleven-test mod-11)🟡 v1.2+ candidate (community PR)❌ NO❌ NO❌ NO❌ NO🇵🇹 NIF (mod-11)🟡 v1.2+ candidate (community PR)❌ NO❌ NO❌ NO❌ NOISO 13616 IBAN mod-97 (every country)✅ YES🟡 structural only❌ NO🟡 partial (US-leaning)🟡 partial (US-leaning)Per-country phone number heuristics✅ YES (IT/DE/ES + community packs)🟡 limited❌ NO🟡 limited🟡 limitedPer-country street-address heuristics✅ YES (IT/DE/ES)❌ NO❌ NO❌ NO❌ NO### Replacement strategies (mask / hash / tokenise / drop)

[](#replacement-strategies-mask--hash--tokenise--drop)

laravel-pii-redactorMicrosoft PresidioSpatie data-redactionAWS Comprehend PIIGoogle Cloud DLPMask strategy (`[REDACTED]`)✅ YES✅ YES✅ YES✅ YES✅ YESDeterministic salted hash strategy✅ YES🟡 custom anonymizer🟡 custom❌ NO🟡 cryptoHashConfigPer-detector hash namespacing✅ YES❌ NO❌ NO❌ NO🟡 partialReversible pseudonymisation (`detokenise`)✅ YES (`TokeniseStrategy`)❌ NO🟡 custom❌ NO🟡 DLP de-identifyDrop strategy (empty replacement)✅ YES✅ YES✅ YES❌ NO✅ YESStrategy override per-call (`Pii::redact($t, new HashStrategy(...))`)✅ YES🟡 anonymizer chains🟡 manual❌ NO🟡 deidentifyTemplate### Persistence &amp; infrastructure

[](#persistence--infrastructure)

laravel-pii-redactorMicrosoft PresidioSpatie data-redactionAWS Comprehend PIIGoogle Cloud DLPIn-memory token store (process-local)✅ YES (`InMemoryTokenStore`)❌ NO (stateless)❌ NO❌ NO❌ NODatabase token store (Eloquent + migration)✅ YES (`DatabaseTokenStore` v0.2)❌ NO❌ NO❌ NO❌ NOCache token store (Redis / Memcached / array)✅ YES (`CacheTokenStore` v0.3)❌ NO❌ NO❌ NO❌ NOCross-process / cross-deploy detokenisation✅ YES (database / cache drivers)❌ NO❌ NO❌ NO❌ NOAudit-trail event (counts only, GDPR-safe)✅ YES (`PiiRedactionPerformed` v0.2)❌ NO❌ NO🟡 CloudWatch (paid)🟡 audit logs (paid)### Extensibility &amp; community

[](#extensibility--community)

laravel-pii-redactorMicrosoft PresidioSpatie data-redactionAWS Comprehend PIIGoogle Cloud DLPPer-tenant custom detectors✅ `Pii::extend()` (one-liner)🟡 yaml + Python class🟡 manual🟡 custom entities🟡 custom infoTypesYAML-loaded custom rule packs✅ YES (`YamlCustomRuleLoader` v0.3)🟡 yaml + Python config❌ NO❌ NO❌ NOPluggable country pack architecture✅ YES (`PackContract` v1.0)❌ NO❌ NO❌ NO❌ NOCommunity-contributed country packs✅ YES (DE + ES shipped v1.1; FR/NL/PT welcome)❌ NO❌ NO❌ NO❌ NOHuggingFace NER driver (opt-in, fail-open)✅ YES (`HuggingFaceNerDriver` v0.3)✅ YES (HF integration)❌ NO🟡 separate service❌ NOspaCy NER driver (opt-in, generic HTTP)✅ YES (`SpaCyNerDriver` v0.3)✅ YES (built-in)❌ NO❌ NO❌ NOAI vibe-coding pack for contributors✅ YES (`.claude/` skills + agents)❌ NO❌ NO❌ NO❌ NOApache-2.0 license✅ YES✅ YES (MIT)✅ YES (MIT)🟡 proprietary🟡 proprietary### Quality gates &amp; guarantees

[](#quality-gates--guarantees)

laravel-pii-redactorMicrosoft PresidioSpatie data-redactionAWS Comprehend PIIGoogle Cloud DLPStable surface lock (semver v1.x)✅ YES (v1.0+)🟡 0.x line✅ YES🟡 service versioning🟡 service versioningPHP 8.3 / 8.4 / 8.5 × Laravel 12 / 13 matrix CI✅ YES❌ N/A✅ YES❌ N/A❌ N/A600+ unit tests + robustness suite✅ YES✅ YES🟡 smaller surface❌ N/A (managed service)❌ N/A (managed service)Cross-pack architecture isolation enforced✅ YES (per-pack architecture test)❌ NO❌ NO❌ NO❌ NOPerformance benchmarks (1MB doc &lt; 2s)✅ YES (`PerfBenchTest`)🟡 unpublished🟡 unpublished🟡 SLA only🟡 SLA onlyStandalone-agnostic invariant (no host coupling)✅ YES (R37 architecture test)✅ YES✅ YES❌ N/A❌ N/A`laravel-pii-redactor` is **not** a Presidio replacement for fuzzy named-entity recognition — Presidio's transformer-backed NER layer (PERSON, ORG, LOC) is genuinely more capable as a free-form classifier, and you can plug it (or any HuggingFace / spaCy model) into this package via the `NerDriver` interface (v0.3+). The deterministic regex + checksum + per-country pack core stays the strongest layer where the existing EU-aware options are weakest, and the persistent reverse-map storage + community-contributable pack architecture are unique to this package across the comparison set.

---

Installation
------------

[](#installation)

```
composer require padosoft/laravel-pii-redactor
```

Laravel auto-discovery wires the `PiiRedactorServiceProvider` and the `Pii` facade alias. Publish the config to override defaults:

```
php artisan vendor:publish --tag=pii-redactor-config
```

Set the salt for the hash / tokenise strategies in your `.env`:

```
PII_REDACTOR_STRATEGY=mask
PII_REDACTOR_SALT=
```

---

Quick start
-----------

[](#quick-start)

```
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 mario@example.com IBAN IT60X0542811101000000123456.');
$report->countsByDetector(); // ['email' => 1, 'iban' => 1]

// One-off strategy override (without changing config).
use Padosoft\PiiRedactor\Strategies\HashStrategy;
$hashed = Pii::redact('mario@example.com', new HashStrategy(salt: env('PII_REDACTOR_SALT')));
// "[hash:f72a1b09abc12345]"  (16 hex chars — 64-bit namespace)
```

---

Usage examples
--------------

[](#usage-examples)

### Reversible pseudonymisation for forensic exports

[](#reversible-pseudonymisation-for-forensic-exports)

```
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);
```

### Custom detector via `Pii::extend()`

[](#custom-detector-via-piiextend)

```
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);
```

### CLI — scan a file in CI

[](#cli--scan-a-file-in-ci)

```
# Samples are masked by default to keep raw PII out of CI logs.
php artisan pii:scan storage/exports/chat-log.txt --pretty

# Pass --show-samples for interactive forensics on a trusted terminal.
php artisan pii:scan storage/exports/chat-log.txt --pretty --show-samples
```

Default (masked-samples) output:

```
{
    "total": 4,
    "counts": { "email": 2, "iban": 1, "p_iva": 1 },
    "samples": {
        "email": ["[email]", "[email]"],
        "iban": ["[iban]"],
        "p_iva": ["[p_iva]"]
    }
}
```

With `--show-samples` (raw values restored):

```
{
    "total": 4,
    "counts": { "email": 2, "iban": 1, "p_iva": 1 },
    "samples": {
        "email": ["mario@example.com", "anna@example.com"],
        "iban": ["IT60X0542811101000000123456"],
        "p_iva": ["12345678903"]
    }
}
```

---

Laravel integration recipes
---------------------------

[](#laravel-integration-recipes)

The package is **transport-agnostic** — `Pii::redact()` and `RedactorEngine::redact()` accept a string and return a redacted string, so they slot into HTTP, queue, CLI, and event paths identically.

> **Side-effects to expect.** `redact()` is not strictly pure: when the active strategy is `tokenise` it persists `(token, original)`rows to the configured `TokenStore` (see `pii-redactor.token_store`), and when audit-trail is enabled it dispatches a `PiiRedactionPerformed`event after every call. Both behaviours are documented in the `RedactorEngine` source. Keep this in mind if you wrap the call in a transaction or invoke it from a hot loop.

This section documents the two production-tested integration shapes plus the strategy decision tree.

> **Real-world reference**: AskMyDocs (the v4.1+ enterprise RAG / chat platform) wires this package at four observable touch-points using the patterns below. Source available under `app/Http/Middleware/RedactChatPii.php`, `app/Services/Kb/EmbeddingCacheService.php`, `app/Services/Admin/AiInsightsService.php`, and `app/Http/Controllers/Api/Admin/LogViewerController.php` of [`lopadova/AskMyDocs`](https://github.com/lopadova/AskMyDocs) — feel free to copy.

### A note on config namespaces — package vs host

[](#a-note-on-config-namespaces--package-vs-host)

This package's own runtime knobs (master switch, default strategy, salt, mask token, NER driver, …) live under `pii-redactor.*` (file: `config/pii-redactor.php`) and are driven by the documented `PII_REDACTOR_*` env vars. **Do not invent a parallel `app.pii_redactor` tree** — turning the package on via `PII_REDACTOR_ENABLED=true` will not flip a guard that reads `config('app.pii_redactor.enabled')`.

What you DO need is your own per-touch-point integration knobs (e.g. "the chat middleware is active", "the embedding pre-redact is active"). Pick a host-app config namespace and document the env-var names alongside the package's own. The recipes below use a placeholder namespace `myapp.pii.*` — substitute your project's real config key (AskMyDocs uses `kb.pii_redactor.*`, for example).

### Integration shape A — HTTP middleware (best practice for chat / API write paths)

[](#integration-shape-a--http-middleware-best-practice-for-chat--api-write-paths)

This is the recommended pattern when redaction must happen **before**the controller persists the request to a database, dispatches a queue job, or calls an external LLM. The middleware mutates a specific request field (typically `content` / `message` / `body`) so every downstream consumer (controller, model `creating` event, queue job, log driver) sees the redacted form automatically.

**1. Create the middleware:**

```
