juststeveking/masking-engine
A Laravel/PHP masking engine for obscuring sensitive data such as emails, names, and phone numbers. Apply configurable mask strategies to protect PII in logs, exports, and API responses through a simple, extensible API.
Install, pinned. This is a 0.0.1 with a single tag and no changelog discipline — do not let Composer float you onto a surprise behaviour change.
composer require juststeveking/masking-engine:0.0.1
Commit the exact version. If the package later grows a config file or service provider, pin before publishing anything so an upstream default change can't silently weaken masks.
Where to look first. Before writing a line of application code, spend 20 minutes in the repository source rather than the README:
composer.json — check the require block for dev-only packages (Faker and friends) leaking into production installs, and note the PHP / illuminate/* constraint ranges against your lowest-supported versions.src/ — find the entry point (the chainable masker class), the shipped strategy/masker classes, and whether there is a service provider with auto-discovery.null, empty strings, integers, and unrecognised input.Answer one question before anything else: what happens to input that matches no rule? Pass-through, throw, or partial mask? Write the answer down. Silent pass-through is fine in a seeder and disqualifying in a production log path, and this single fact determines every use case below.
First use case — staging seeders. It is the lowest-blast-radius place to learn the API. Mask values inside factories/seeders before they hit the database, so a bad mask costs a re-seed, not an incident:
// Shape, not verbatim API — confirm class/method names in src/.
$masker = new Masker();
$user = User::factory()->create([
'name' => $masker->name()->mask($faker->name()),
'email' => $masker->email()->mask($faker->safeEmail()),
'phone' => $masker->phone()->mask($faker->phoneNumber()),
]);
Once that reads cleanly in one seeder, you know the ergonomics are tolerable and you can move to log context.
Wrap it behind your own interface on day one. This is the single pattern that makes everything else cheap. Your call sites depend on App\Support\Masking\Masker, not on the package's namespace:
interface Masker
{
public function maskValue(string $key, mixed $value): mixed;
public function maskArray(array $payload): array;
}
final class PackageMasker implements Masker { /* delegates to masking-engine */ }
One hour of work converts a future abandonment from an application-wide refactor into a one-file swap. It also gives you a place to add fail-closed behaviour the package may not have.
Config-driven field map for arrays. Rather than chaining masks at every call site, keep a per-context map of sensitive keys and walk the array with it. This is what makes nested payloads and column dumps tractable:
final class FieldMap
{
public const LOG_CONTEXT = [
'email' => 'email', 'password' => 'full', 'token' => 'full',
'card_number' => 'partial', 'phone' => 'phone',
];
}
Treat the map as the inventory you actually audit — the library is just the executor.
Seeder and dump flows. Mask after generation, before persistence. For db:dump/restore work, chunk the query (->chunkById(500)) and mask row arrays inside the loop; never load a table into memory to mask it. Assert on the shape of the masked output in a test (still a valid email, still the right length) so a downstream consumer doesn't break on a format change.
Log context scrubbing. In Laravel this is a Monolog processor or a Log::listen tap that calls maskArray() on $context before the record is written. Mask the whole context array, not just the keys you remembered — unrecognised keys rely on your fail-mode answer, which is exactly why you established it first. Measure log volume and cardinality impact before/after; aggressive masking of high-cardinality fields can flatten useful grouping.
Serialization boundaries. For CSV/report exports, mask at the point you build the row array, immediately before the writer. Masking after json_encode() means running regexes over escaped JSON and is where nested-array bugs live.
Custom strategies for domain values. Where the shipped masks don't fit (internal account IDs, tenant slugs), write your own masker class or callback and register it once in your wrapper — not copy-pasted regex at each call site. That is the extension point that makes the package feel yours.
Contract tests. Three assertions, upstream-facing:
it('fails closed on unrecognised input'); // whatever you decided
it('produces a stable pseudonym for a given email');
it('masks every key in the log context map');
These turn an upstream behaviour change into a red build instead of a quiet weakening.
orders, users, and events becomes three different values and joins silently stop working. If you need cross-table consistency, either wrap with your own keyed hash or accept that this package won't give it to you — it's not a tokenisation solution.dd(), failed job payloads in failed_jobs, and query logs are all outside the masker. Masking the log record doesn't help if a stack trace contains the raw value.How can I help you explore Laravel packages today?