PHPackages                             dynamik-dev/cloak-php - 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. [Utility &amp; Helpers](/categories/utility)
4. /
5. dynamik-dev/cloak-php

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

dynamik-dev/cloak-php
=====================

Redact sensitive data from strings and reveal them later using placeholder tokens

v0.2.1(5mo ago)4631MITPHPPHP ^8.2CI passing

Since Nov 25Pushed 5mo agoCompare

[ Source](https://github.com/dynamik-dev/cloak-php)[ Packagist](https://packagist.org/packages/dynamik-dev/cloak-php)[ RSS](/packages/dynamik-dev-cloak-php/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (4)Dependencies (4)Versions (7)Used By (1)

Cloak
=====

[](#cloak)

[![Cloak PHP Example](cloak-php.png)](cloak-php.png)

A simple, extensible PHP package for redacting sensitive data from strings and revealing them later.

```
$cloaked = cloak('Email me at john@example.com');
// "Email me at {{EMAIL_x7k2m9_1}}"

$original = uncloak($cloaked);
// "Email me at john@example.com"
```

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

[](#installation)

```
composer require dynamik-dev/cloak-php
```

Requirements
------------

[](#requirements)

- PHP 8.2+
- ext-mbstring (required by libphonenumber)

Quick Start
-----------

[](#quick-start)

### Using Helper Functions

[](#using-helper-functions)

```
$text = 'Contact: john@example.com, Phone: 555-123-4567';
$cloaked = cloak($text);
// "Contact: {{EMAIL_x7k2m9_1}}, Phone: {{PHONE_x7k2m9_1}}"

$original = uncloak($cloaked);
// "Contact: john@example.com, Phone: 555-123-4567"
```

### Using Specific Detectors

[](#using-specific-detectors)

```
use DynamikDev\Cloak\Detector;

// Only detect emails
$cloaked = cloak($text, [Detector::email()]);

// Multiple detectors
$cloaked = cloak($text, [
    Detector::email(),
    Detector::phone('US'),
    Detector::ssn(),
]);
```

### Using the Cloak Class

[](#using-the-cloak-class)

For more control, use the `Cloak` class directly:

```
use DynamikDev\Cloak\Cloak;

$cloak = Cloak::make();

$cloaked = $cloak->cloak($text);
$original = $cloak->uncloak($cloaked);
```

### Configuring with Builder Methods

[](#configuring-with-builder-methods)

```
use DynamikDev\Cloak\Cloak;
use DynamikDev\Cloak\Detector;
use DynamikDev\Cloak\Encryptors\OpenSslEncryptor;

$cloak = Cloak::make()
    ->withDetectors([Detector::email()])
    ->encrypt(OpenSslEncryptor::generateKey());

$cloaked = $cloak->cloak('Sensitive: john@example.com');
```

Built-in Detectors
------------------

[](#built-in-detectors)

Cloak provides several built-in detectors for common sensitive data types:

```
use DynamikDev\Cloak\Detector;

Detector::email();           // Email addresses
Detector::phone('US');       // Phone numbers (specify region code)
Detector::ssn();             // Social Security Numbers (XXX-XX-XXXX)
Detector::creditCard();      // Credit card numbers
Detector::all();             // All built-in detectors (uses US for phone)
```

### Phone Number Detection

[](#phone-number-detection)

Phone detection uses [libphonenumber-for-php](https://github.com/giggsey/libphonenumber-for-php) for robust international phone number validation with intelligent false positive prevention.

**Features:**

- International format support for all countries
- Validates actual phone numbers (not just digit patterns)
- Filters out order IDs, timestamps, serial numbers, etc.
- Handles various formats: `(212) 456-7890`, `212-456-7890`, `+44 117 496 0123`

**Examples:**

```
// US numbers
Detector::phone('US')->detect('Call 212-456-7890');

// UK numbers
Detector::phone('GB')->detect('Ring 0117 496 0123');

// International format
Detector::phone()->detect('Call +44 117 496 0123');

// Filters false positives
Detector::phone('US')->detect('Order #123456789012'); // Not detected
```

Custom Detectors
----------------

[](#custom-detectors)

Cloak supports two approaches for custom detectors: **factory methods** (concise) and **direct instantiation** (explicit).

### Pattern-based Detector

[](#pattern-based-detector)

```
use DynamikDev\Cloak\Detector;
use DynamikDev\Cloak\Detectors\Pattern;

// Factory method (concise)
$detector = Detector::pattern('/\b[A-Z]{2}\d{6}\b/', 'passport');

// Direct instantiation (explicit)
$detector = new Pattern('/\b[A-Z]{2}\d{6}\b/', 'passport');

$cloaked = $cloak->cloak('Passport: AB123456', [$detector]);
// "Passport: {{PASSPORT_x7k2m9_1}}"
```

### Word-based Detector

[](#word-based-detector)

```
use DynamikDev\Cloak\Detector;
use DynamikDev\Cloak\Detectors\Words;

// Factory method
$detector = Detector::words(['password', 'secret'], 'sensitive');

// Direct instantiation
$detector = new Words(['password', 'secret'], 'sensitive');

$cloaked = $cloak->cloak('The password is secret123', [$detector]);
// "The {{SENSITIVE_x7k2m9_1}} is {{SENSITIVE_x7k2m9_2}}123"
```

### Callable Detector

[](#callable-detector)

```
use DynamikDev\Cloak\Detector;
use DynamikDev\Cloak\Detectors\Callback;

// Factory method
$detector = Detector::using(function (string $text): array {
    $matches = [];
    if (preg_match_all('/\bAPI_KEY_\w+\b/', $text, $found)) {
        foreach ($found[0] as $match) {
            $matches[] = ['match' => $match, 'type' => 'api_key'];
        }
    }
    return $matches;
});

// Direct instantiation
$detector = new Callback(function (string $text): array {
    // ... same logic
});
```

### Mixed Approach

[](#mixed-approach)

You can mix both patterns freely:

```
use DynamikDev\Cloak\Detector;
use DynamikDev\Cloak\Detectors\{Email, Pattern, Words};

$cloak->cloak($text, [
    new Email(),                                    // Direct
    Detector::phone('US'),                          // Factory
    new Pattern('/\b[A-Z]{2}\d{6}\b/', 'passport'), // Direct
    Detector::words(['secret'], 'sensitive'),       // Factory
]);
```

### Implementing DetectorInterface

[](#implementing-detectorinterface)

For full control, implement the `DetectorInterface`:

```
use DynamikDev\Cloak\Contracts\DetectorInterface;

class IpAddressDetector implements DetectorInterface
{
    public function detect(string $text): array
    {
        $matches = [];
        $pattern = '/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/';

        if (preg_match_all($pattern, $text, $found)) {
            foreach ($found[0] as $match) {
                $matches[] = ['match' => $match, 'type' => 'ip_address'];
            }
        }

        return $matches;
    }
}

$cloaked = $cloak->cloak('Server: 192.168.1.1', [new IpAddressDetector()]);
// "Server: {{IP_ADDRESS_x7k2m9_1}}"
```

Advanced Features
-----------------

[](#advanced-features)

### Filtering Detections

[](#filtering-detections)

Use filters to exclude certain detections from being cloaked:

```
// Exclude test emails
$cloak = Cloak::make()
    ->filter(fn ($detection) => !str_ends_with($detection['match'], '@test.local'));

$text = 'prod@company.com and test@test.local';
$cloaked = $cloak->cloak($text, [Detector::email()]);
// "{{EMAIL_x7k2m9_1}} and test@test.local"
```

### Multiple Filters

[](#multiple-filters)

Filters are applied in sequence, and all must return `true` for a detection to be included:

```
$cloak = Cloak::make()
    ->filter(fn ($d) => $d['type'] === 'email')
    ->filter(fn ($d) => !str_contains($d['match'], 'noreply'));
```

### Lifecycle Callbacks

[](#lifecycle-callbacks)

Hook into the cloaking/uncloaking process:

```
$cloak = Cloak::make()
    ->beforeCloak(function (string $text) {
        // Normalize whitespace before processing
        return preg_replace('/\s+/', ' ', $text);
    })
    ->afterCloak(function (string $original, string $cloaked) {
        // Log the cloaking operation
        logger()->info('Cloaked text', ['original_length' => strlen($original)]);
    })
    ->beforeUncloak(function (string $text) {
        // Validate before uncloaking
        return $text;
    })
    ->afterUncloak(function (string $text) {
        // Post-process after uncloaking
        return trim($text);
    });
```

### Encryption

[](#encryption)

Encrypt sensitive values at rest using the convenient `encrypt()` method:

```
use DynamikDev\Cloak\Encryptors\OpenSslEncryptor;

$cloak = Cloak::make()
    ->encrypt(OpenSslEncryptor::generateKey());

$cloaked = $cloak->cloak('Secret: john@example.com', [Detector::email()]);
// Values are encrypted in storage, but placeholders remain the same
```

**Environment Variable Support:**

```
// Reads from CLOAK_PRIVATE_KEY environment variable
$cloak = Cloak::make()->encrypt();

// Or specify a custom environment variable
$encryptor = new OpenSslEncryptor(null, 'MY_ENCRYPTION_KEY');
$cloak = Cloak::make()->withEncryptor($encryptor);
```

**Custom Encryptors:**

For full control, use `withEncryptor()` with a custom implementation:

```
$customEncryptor = new MyEncryptor($key);
$cloak = Cloak::make()->withEncryptor($customEncryptor);
```

Storage
-------

[](#storage)

### Default Store (ArrayStore)

[](#default-store-arraystore)

By default, Cloak uses `ArrayStore` for in-memory storage. This is perfect for:

- Testing
- Single-request scenarios
- Simple use cases without persistence

```
use DynamikDev\Cloak\Stores\ArrayStore;

$store = new ArrayStore();
$cloak = Cloak::using($store);
```

### Custom Store Implementation

[](#custom-store-implementation)

For persistent storage across requests, implement `StoreInterface`:

```
use DynamikDev\Cloak\Contracts\StoreInterface;

class RedisStore implements StoreInterface
{
    public function __construct(
        private Redis $redis,
        private int $ttl = 3600
    ) {}

    public function put(string $key, array $map): void
    {
        $this->redis->setex($key, $this->ttl, json_encode($map));
    }

    public function get(string $key): ?array
    {
        $data = $this->redis->get($key);
        return $data ? json_decode($data, true) : null;
    }

    public function forget(string $key): void
    {
        $this->redis->del($key);
    }
}

// Configure TTL via constructor
$cloak = Cloak::using(new RedisStore($redis, ttl: 7200));
```

### Laravel Cache Store Example

[](#laravel-cache-store-example)

```
use DynamikDev\Cloak\Contracts\StoreInterface;
use Illuminate\Support\Facades\Cache;

class LaravelCacheStore implements StoreInterface
{
    public function __construct(private int $ttl = 3600) {}

    public function put(string $key, array $map): void
    {
        Cache::put($key, $map, $this->ttl);
    }

    public function get(string $key): ?array
    {
        return Cache::get($key);
    }

    public function forget(string $key): void
    {
        Cache::forget($key);
    }
}
```

Framework Integration
---------------------

[](#framework-integration)

### Custom Resolver

[](#custom-resolver)

For framework adapters (like Laravel, Symfony, etc.), you can override how `Cloak::make()` resolves instances using `resolveUsing()`:

```
use DynamikDev\Cloak\Cloak;

// Set a custom resolver (typically in a service provider)
Cloak::resolveUsing(fn() => app(Cloak::class));

// Now Cloak::make() resolves from your container
$cloak = Cloak::make();  // Uses your container binding
```

This allows framework packages to:

- Integrate with dependency injection containers
- Use framework-specific storage drivers
- Apply framework configuration automatically
- Let developers extend and customize via container bindings

**Example Laravel Service Provider:**

```
use DynamikDev\Cloak\Cloak;
use Illuminate\Support\ServiceProvider;

class CloakServiceProvider extends ServiceProvider
{
    public function register()
    {
        // Bind Cloak to the container with your configuration
        $this->app->bind(Cloak::class, function ($app) {
            return Cloak::using($app->make(CacheStore::class))
                ->withDetectors(config('cloak.detectors', Detector::all()));
        });

        // Make helpers use container resolution
        Cloak::resolveUsing(fn() => app(Cloak::class));
    }
}
```

Now developers can customize behavior through container bindings:

```
// In AppServiceProvider
$this->app->bind(Cloak::class, function ($app) {
    return CustomCloak::using($app->make(CacheStore::class));
});
```

**Clearing the Resolver:**

```
Cloak::clearResolver();  // Reverts to default behavior
```

Extending Cloak
---------------

[](#extending-cloak)

Cloak follows a compositional architecture, making it easy to extend with custom implementations.

### Custom Placeholder Generator

[](#custom-placeholder-generator)

Create custom placeholder formats by implementing `PlaceholderGeneratorInterface`:

```
use DynamikDev\Cloak\Contracts\PlaceholderGeneratorInterface;
use Ramsey\Uuid\Uuid;

class UuidPlaceholderGenerator implements PlaceholderGeneratorInterface
{
    public function generate(array $detections): array
    {
        $key = Uuid::uuid4()->toString();
        $map = [];

        foreach ($detections as $detection) {
            $uuid = Uuid::uuid4()->toString();
            $placeholder = "[{$detection['type']}:{$uuid}]";
            $map[$placeholder] = $detection['match'];
        }

        return ['key' => $key, 'map' => $map];
    }

    public function replace(string $text, array $map): string
    {
        foreach ($map as $placeholder => $original) {
            $text = str_replace($original, $placeholder, $text);
        }
        return $text;
    }

    public function parse(string $text): array
    {
        // Extract [TYPE:UUID] placeholders and group by key
        // Implementation details...
        return [];
    }
}

$cloak = Cloak::make()
    ->withPlaceholderGenerator(new UuidPlaceholderGenerator());
```

### Custom Encryptor

[](#custom-encryptor)

Implement `EncryptorInterface` for custom encryption strategies:

```
use DynamikDev\Cloak\Contracts\EncryptorInterface;

class SodiumEncryptor implements EncryptorInterface
{
    public function __construct(private string $key) {}

    public function encrypt(string $value): string
    {
        $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
        $encrypted = sodium_crypto_secretbox($value, $nonce, $this->key);

        return base64_encode($nonce . $encrypted);
    }

    public function decrypt(string $encrypted): string
    {
        $decoded = base64_decode($encrypted);
        $nonce = substr($decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
        $ciphertext = substr($decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);

        $decrypted = sodium_crypto_secretbox_open($ciphertext, $nonce, $this->key);

        if ($decrypted === false) {
            throw new \RuntimeException('Decryption failed');
        }

        return $decrypted;
    }
}

$cloak = Cloak::make()
    ->withEncryptor(new SodiumEncryptor($key));
```

Placeholder Format
------------------

[](#placeholder-format)

Placeholders follow the format `{{TYPE_KEY_INDEX}}`:

- `TYPE`: Uppercase detector type (EMAIL, PHONE, SSN, CREDIT\_CARD)
- `KEY`: 6-character alphanumeric unique key
- `INDEX`: Integer counter per type, starting at 1

**Example:** `{{EMAIL_x7k2m9_1}}`

The default format can be customized by implementing a custom `PlaceholderGeneratorInterface`.

API Reference
-------------

[](#api-reference)

### Factory Methods

[](#factory-methods)

```
Cloak::make(?StoreInterface $store = null): self
Cloak::using(StoreInterface $store): self
```

### Builder Methods

[](#builder-methods)

```
->withDetectors(array $detectors): self
->filter(callable $callback): self
->withPlaceholderGenerator(PlaceholderGeneratorInterface $generator): self
->withEncryptor(EncryptorInterface $encryptor): self
->encrypt(?string $key = null): self  // Convenience method for OpenSslEncryptor
```

### Lifecycle Callbacks

[](#lifecycle-callbacks-1)

```
->beforeCloak(callable $callback): self
->afterCloak(callable $callback): self
->beforeUncloak(callable $callback): self
->afterUncloak(callable $callback): self
```

### Core Methods

[](#core-methods)

```
->cloak(string $text, ?array $detectors = null): string
->uncloak(string $text): string
```

Edge Cases
----------

[](#edge-cases)

- **Same value appears multiple times**: Reuses the same placeholder
- **No detections found**: Returns original text unchanged
- **Missing cache on uncloak**: Leaves placeholder in place
- **Empty input**: Returns empty string
- **Overlapping patterns**: All patterns are processed independently

Testing
-------

[](#testing)

```
./vendor/bin/pest
```

Static Analysis
---------------

[](#static-analysis)

```
./vendor/bin/phpstan analyse
```

License
-------

[](#license)

MIT

###  Health Score

35

—

LowBetter than 80% of packages

Maintenance70

Regular maintenance activity

Popularity13

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity42

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~1 days

Total

4

Last Release

171d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/990cfbdeab8ba6fd8c12d682db3e9723e22350bb8b377d91d692927f3e195241?d=identicon)[christopherarter](/maintainers/christopherarter)

---

Top Contributors

[![christopherarter](https://avatars.githubusercontent.com/u/12786153?v=4)](https://github.com/christopherarter "christopherarter (12 commits)")

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/dynamik-dev-cloak-php/health.svg)

```
[![Health](https://phpackages.com/badges/dynamik-dev-cloak-php/health.svg)](https://phpackages.com/packages/dynamik-dev-cloak-php)
```

###  Alternatives

[odolbeau/phone-number-bundle

Integrates libphonenumber into your Symfony application

24910.3M11](/packages/odolbeau-phone-number-bundle)[verbb/formie

The most user-friendly forms plugin for Craft.

101372.9k40](/packages/verbb-formie)[log1x/acf-phone-number

A real ACF phone number field.

12072.5k](/packages/log1x-acf-phone-number)[serendipity_hq/component-value-objects

A set of value objects to manage simple and composite values

20558.6k4](/packages/serendipity-hq-component-value-objects)[rynpsc/craft-phone-number

International phone number field.

2265.9k](/packages/rynpsc-craft-phone-number)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
