erikwang2013 / encryptable
Encryptable Eloquent casts and helpers for PHP 8.2+ with Laravel 10–12 / Webman (Illuminate), Hyperf 2–3, and ThinkPHP 6–8 bridges.
Package info
github.com/erikwang2013/encryptable
Type:composer-plugin
pkg:composer/erikwang2013/encryptable
Requires
- php: ^8.2
- composer-plugin-api: ^2.2
- ext-openssl: *
- psr/container: ^1.1|^2.0
Requires (Dev)
- illuminate/contracts: ^10.0|^11.0|^12.0
- illuminate/database: ^10.0|^11.0|^12.0
- illuminate/support: ^10.0|^11.0|^12.0
- illuminate/validation: ^10.0|^11.0|^12.0
- larastan/larastan: ^2.9|^3.0
- laravel/pint: ^1.13
- orchestra/testbench: ^8.0|^9.0|^10.0
- pestphp/pest: ^2.34|^3.0
- pestphp/pest-plugin-laravel: ^2.4|^3.0
- phpunit/phpunit: ^10.5|^11.0
Suggests
- hyperf/db-connection: Required for Hyperf DB decrypt snippets (Encryption::db()).
- hyperf/framework: Hyperf 2–3: ConfigProvider is registered via composer extra (see docs).
- illuminate/database: Required with illuminate/support and illuminate/validation for Laravel/Webman integration.
- illuminate/support: Required for EncryptableServiceProvider and validation rule macros.
- illuminate/validation: Required for UniqueEncrypted / ExistsEncrypted rules.
- laravel/framework: Laravel 10–12: auto-discovery registers EncryptableServiceProvider.
- topthink/framework: ThinkPHP 6–8: call ThinkphpEncryptable::register($app) during boot.
- workerman/webman: Webman: install suggested Illuminate packages, then register EncryptableServiceProvider.
README
Languages: English (this page) | 简体中文
Encryptable (erikwang2013/encryptable)
On PHP 8.2+, this package helps you anonymize / encrypt sensitive attributes in a query-friendly way: values are encrypted before persistence and decrypted when read through Eloquent (or manual APIs). It can also emit MySQL / PostgreSQL-compatible SQL fragments so you can compare or search against encrypted columns in raw queries. The PHP namespace remains Maize\Encryptable\... for historical compatibility.
This repository evolves from the ideas and behaviour of laravel-encryptable (Maize Tech). For the original design, issues, and releases, refer to that upstream project.
Features
- Eloquent custom cast: use
Encryptable::classin$castsfor transparent encrypt/decrypt on attributes. - PHP-side crypto:
Encryption::php()->encrypt()/decrypt()for CLI, queues, or non-model code paths. - DB expressions:
Encryption::db()->decrypt()returns a SQL snippet (MySQL vs Postgres branches) suitable forwhereRaw-style comparisons against stored ciphertext. - Validation:
UniqueEncrypted,ExistsEncrypted, andRule::uniqueEncrypted()/Rule::existsEncrypted()macros (requiresilluminate/validation). - Multi-runtime bridges: Laravel 10–12, Illuminate-based Webman, Hyperf 2–3, and ThinkPHP 6–8 each register container/config in their own way; without a full container,
Encryption::php()can fall back toENCRYPTION_KEY,ENCRYPTION_CIPHER, and optionalENCRYPTION_PREVIOUS_KEYS(see the Supported frameworks table below). - Composer install hook: this package is a Composer plugin; on
composer require/composer update, it inspectsvendor/composer/installed.php, lock, manifest, and project layout to publish config for the stack in use (see Installation → Composer plugin). - Key rotation (
Encryption::php()only): primary +previous_keys/ENCRYPTION_PREVIOUS_KEYSdecryption ring, optionalrotateToCurrentKey()for gradual re-encryption — full behavior and rollout are documented under Configuration → Key rotation.
Requirements
| Item | Notes |
|---|---|
| PHP | ^8.2 with the openssl extension |
| Databases | Docs and SQL helpers target MySQL and PostgreSQL (driver name is used to pick the dialect for Encryption::db()). |
Packagist: erikwang2013/encryptable (name in composer.json).
Supported frameworks
The table below summarizes expected compatibility, how to wire the package, and which features are Laravel-specific (Eloquent cast, illuminate/validation rules). Pin versions in your own project as needed. Config files are installed automatically by the Composer plugin when allowed (see Installation); you can still copy or vendor:publish manually if you prefer.
| Framework | Version (expected) | Integration | Eloquent $casts (Encryptable) |
Encryption::php() |
Encryption::db() |
UniqueEncrypted / ExistsEncrypted & macros |
|---|---|---|---|---|---|---|
| Laravel | 10.x–12.x (PHP ≥ 8.2) | Package discovery registers EncryptableServiceProvider + config/plugin/erikwang2013/encryptable/app.php (or legacy config/encryptable.php) |
✓ | ✓ | ✓ | ✓ |
| Webman | 1.x / 2.x with Illuminate (database / support / validation) | Register EncryptableServiceProvider + config/plugin/erikwang2013/encryptable/app.php (Composer plugin or manual) |
✓ (when using Eloquent) | ✓ | ✓ | ✓ |
| Hyperf | 2.x / 3.x | extra.hyperf.config merges Bridge\Hyperf\ConfigProvider + config/autoload/plugins/erikwang2013/encryptable.php (or legacy config/autoload/encryptable.php) |
— (no Laravel cast; call Encryption::php() in entities/repos) |
✓ | ✓ (install hyperf/db-connection) |
— (needs Illuminate validation stack) |
| ThinkPHP | 6.x–8.x | ThinkphpEncryptable::register($app) + config/plugin/erikwang2013/encryptable/app.php (or legacy config/encryptable.php) |
— (use accessors/mutators or types with Encryption::php()) |
✓ | ✓ | — |
Legend: ✓ = supported for this stack out of the box; — = not provided; integrate in your own layer.
Installation
composer require erikwang2013/encryptable
Composer plugin (auto-publish config)
This package has "type": "composer-plugin" and registers Maize\Encryptable\Composer\Plugin. After install or update of erikwang2013/encryptable, Composer runs the plugin, which:
- Collects Composer package names (lowercased) from, in order:
vendor/composer/installed.php(andinstalled.jsonif present),composer.lock, then rootcomposer.jsonrequire/require-dev. This matches what is actually installed invendor/, including transitive Webman / Hyperf packages that never appear in your rootcomposer.json. - Adds filesystem hints when needed (e.g. Webman:
support/bootstrap.php,start.php, orwindows.phpplusconfig/; Laravel:artisan+bootstrap/app.phporapp/Http/Kernel.php; Hyperf:bin/hyperf.phporconfig/autoload/server.php; ThinkPHP: executablethinkfile in the project root). - Publishes files only when a supported framework is detected (see table). Existing target files are never overwritten.
- If nothing matches, it prints a skip notice so plain PHP projects are not modified.
| Detected dependency (examples) | Official layout we follow | File we create when missing |
|---|---|---|
laravel/framework or laravel/lumen-framework |
Laravel configuration — PHP under config/ (same physical layout as Webman plugins for this package) |
config/plugin/erikwang2013/encryptable/app.php (merged as config('encryptable.*') via EncryptableServiceProvider) |
workerman/webman / webman/* |
Webman plugins — config/plugin/{vendor}/{name}/app.php |
config/plugin/erikwang2013/encryptable/app.php (config('plugin.erikwang2013.encryptable.app.*')) |
topthink/framework or topthink/think |
ThinkPHP 8 config — project config/; plugin-style path for this package |
config/plugin/erikwang2013/encryptable/app.php (injected as encryptable.* when you call ThinkphpEncryptable::register) |
hyperf/framework or hyperf/hyperf |
Hyperf config — merge PHP files under config/autoload/ (relative path becomes dot keys) |
config/autoload/plugins/erikwang2013/encryptable.php (plugins.erikwang2013.encryptable.*; see HyperfEncryptableConfig) |
Laravel / Lumen / ThinkPHP / Webman share the same config/plugin/erikwang2013/encryptable/app.php template (config/stubs/plugin-app.php). Hyperf uses config/autoload/plugins/erikwang2013/encryptable.php when neither that file nor the legacy config/autoload/encryptable.php exists yet (legacy installs keep working; HyperfEncryptableConfig reads the plugin path first, then encryptable.*). If multiple stacks match (e.g. a monorepo), every applicable file is created when missing.
Composer 2.2+ may block plugins until you allow them. Add this to your application composer.json (once), then run composer require again if needed:
"config": { "allow-plugins": { "erikwang2013/encryptable": true } }
Or approve the prompt Composer shows when requiring the package.
Configuration files (manual copy, optional)
If the plugin is blocked or you manage config in VCS templates yourself, copy the matching template from this package into the path your framework expects, then adjust .env as needed.
| Framework | Source inside vendor (package erikwang2013/encryptable) |
Destination in your project |
|---|---|---|
| Laravel / Lumen / ThinkPHP / Webman | vendor/erikwang2013/encryptable/config/stubs/plugin-app.php |
config/plugin/erikwang2013/encryptable/app.php |
| Laravel (legacy, optional) | vendor/erikwang2013/encryptable/config/encryptable.php |
config/encryptable.php (still merged by EncryptableServiceProvider if present and plugin file is absent) |
| Hyperf (recommended) | vendor/erikwang2013/encryptable/config/stubs/hyperf-plugin-autoload.php |
config/autoload/plugins/erikwang2013/encryptable.php |
| Hyperf (legacy) | vendor/erikwang2013/encryptable/config/stubs/hyperf-autoload-encryptable.php |
config/autoload/encryptable.php (top-level key / cipher → encryptable.*) |
Note:
plugin-app.phpis the shared stub (top-levelkey,cipher). Webman reads it natively asplugin.erikwang2013.encryptable.app.*. Laravel / Lumen merge it intoencryptable.*. ThinkPHP loads it inThinkphpEncryptable::registerintoencryptable.*. Hyperf autoload files are keyed by path: the recommended file maps toplugins.erikwang2013.encryptable.*; the legacy filenameencryptable.phpmaps toencryptable.*.
Shell examples:
# Laravel / Lumen / ThinkPHP / Webman (shared plugin layout) mkdir -p config/plugin/erikwang2013/encryptable cp vendor/erikwang2013/encryptable/config/stubs/plugin-app.php config/plugin/erikwang2013/encryptable/app.php # Hyperf (recommended) mkdir -p config/autoload/plugins/erikwang2013 cp vendor/erikwang2013/encryptable/config/stubs/hyperf-plugin-autoload.php config/autoload/plugins/erikwang2013/encryptable.php
Laravel alternative (vendor:publish publishes the plugin file, optional legacy flat file, and the Hyperf stub path for convenience):
php artisan vendor:publish --provider="Maize\Encryptable\EncryptableServiceProvider" --tag="encryptable-config"
Laravel (remaining steps)
With package auto-discovery enabled, Maize\Encryptable\EncryptableServiceProvider is registered. Ensure config/plugin/erikwang2013/encryptable/app.php exists (Composer plugin or Laravel row / vendor:publish), or keep a legacy config/encryptable.php only.
Webman (with Illuminate)
Install illuminate/database, illuminate/support, illuminate/validation, etc. as needed. Ensure the plugin config exists at config/plugin/erikwang2013/encryptable/app.php (the Composer plugin or vendor:publish creates it). Then register:
Maize\Encryptable\EncryptableServiceProvider
in your plugin/bootstrap code. Runtime reads config('plugin.erikwang2013.encryptable.app.key') and .cipher, per Webman plugin config rules.
Hyperf
- Copy or auto-install config per the Hyperf row (
config/autoload/plugins/erikwang2013/encryptable.php, or legacyconfig/autoload/encryptable.php). Values are read asplugins.erikwang2013.encryptable.key/.cipher, with fallback toencryptable.*. - This package declares
Maize\Encryptable\Bridge\Hyperf\ConfigProviderundercomposer.json→extra.hyperf.configfor Hyperf to merge. - For
Encryption::db(), installhyperf/db-connection.
ThinkPHP
- Prefer
config/plugin/erikwang2013/encryptable/app.php(same stub as Webman/Laravel). Legacyconfig/encryptable.phpstill works if you do not use the plugin path. - During application bootstrap (e.g. service registration), call:
\Maize\Encryptable\Bridge\ThinkPHP\ThinkphpEncryptable::register($app);
Configuration
After copying or publishing config/plugin/erikwang2013/encryptable/app.php, legacy config/encryptable.php, or Hyperf’s config/autoload/plugins/erikwang2013/encryptable.php / config/autoload/encryptable.php, the main keys are:
| Key | Description |
|---|---|
key |
Primary secret key, from ENCRYPTION_KEY. New ciphertext is always produced with this key. |
cipher |
Cipher id, default aes-128-ecb, from ENCRYPTION_CIPHER; keep aligned with DB defaults and any existing data contract. All keys in the ring must use this cipher. |
previous_keys |
Retired keys still tried for decrypt after the primary fails (same cipher). From ENCRYPTION_PREVIOUS_KEYS (comma-separated or JSON array) or the previous_keys config entry. |
Without Laravel / bindings, Encryption::php() can still read ENCRYPTION_KEY, ENCRYPTION_CIPHER, and ENCRYPTION_PREVIOUS_KEYS via EnvEncryptableConfig.
Key rotation (elegant zero-downtime)
You can change the primary encryption key without taking the app offline for a big-bang re-encrypt: new data is sealed with the new primary; existing rows keep decrypting as long as the key that produced them still appears in the configured ring (primary or previous_keys).
Behavior summary (what this package implements)
| Area | Behavior |
|---|---|
| Config contract | EncryptableConfigContract::getPreviousKeys(): array — retired keys used only after the primary getKey() fails to produce valid plaintext. Implemented for Laravel (encryptable.previous_keys), Webman plugin app.php, Hyperf (plugins.* / encryptable.*), ThinkPHP (encryptable.previous_keys), and EnvEncryptableConfig (ENCRYPTION_PREVIOUS_KEYS). |
| Parsing | Maize\Encryptable\Support\PreviousKeysParser turns ENCRYPTION_PREVIOUS_KEYS or config into a list<string>: comma-separated values, or a JSON array string (e.g. ["k1","k2"]), or an already-loaded PHP array. |
| Key ring | Encrypter::getDecryptionKeyRing() builds [primary, …previous] with empty strings and duplicate keys removed. All keys in the ring must share the same cipher. |
| Encrypt | PHPEncrypter::encrypt() always uses the primary key only (getKey()). |
Decrypt & isEncrypted() |
For each ring key, OpenSSL decrypt is tried; success requires decrypted payload to start with the internal dirty prefix (crypt:), so random garbage from a wrong key is not treated as valid. Same logic powers Eloquent Encryptable casts via Encryption::php(). |
| Re-encrypt API | PHPEncrypter::rotateToCurrentKey(?string $payload, bool $serialize = true) decrypts with the ring then re-encrypts with the current primary. Encryption::php()->rotateToCurrentKey(...) delegates to it. Non-encrypted payloads are returned unchanged; null stays null. |
| Out of scope | Encryption::db() / DBEncrypter — SQL snippets use the primary key only. previous_keys is ignored there. Rotate DB-native encrypted columns with an app-side or migration pipeline (read → decrypt in PHP if needed → write), not via this ring. |
Recommended rollout (operations)
- Set the new secret as
ENCRYPTION_KEY(primary). Move the old primary intoENCRYPTION_PREVIOUS_KEYS(orprevious_keysin config). Prefer listing the most recently retired key first among several retirees. - Deploy — reads succeed because decrypt walks the ring until a key yields valid package plaintext.
- Optionally run a background job that loads each encrypted field and calls
Encryption::php()->rotateToCurrentKey($ciphertext)(with the same$serializeflag your data used) so ciphertext gradually moves to the new primary; then traffic no longer depends on old keys. - Remove an entry from
previous_keysonly after you are sure no row still encrypted with that key remains (or you accept those rows becoming unreadable).
Configuration surface
| Source | Keys |
|---|---|
| Environment | ENCRYPTION_KEY, ENCRYPTION_CIPHER, ENCRYPTION_PREVIOUS_KEYS |
Laravel / merged encryptable |
key, cipher, previous_keys (see config/encryptable.php or published plugin app.php) |
Webman plugin app.php |
key, cipher, previous_keys (same shape; Webman reads plugin.*.app.*) |
| Hyperf autoload stubs | key, cipher, previous_keys under plugins.erikwang2013.encryptable.* or legacy encryptable.* |
Multiple previous keys (how to configure)
Decrypt order is always: current primary ENCRYPTION_KEY first, then previous_keys in list order (put the key you retired most recently before older ones, so the common “last rotation” case is tried early).
1 — Environment variable (several retirees)
- Comma-separated (no spaces required, but trims are applied):
ENCRYPTION_PREVIOUS_KEYS=oldKeyOne16bytes!!,olderKeyTwo16byte,ancientKeyThree16b
- JSON array (one line in
.env; use this if a key might contain a comma):
ENCRYPTION_PREVIOUS_KEYS=["oldKeyOne16bytes!!","olderKeyTwo16byte","ancientKeyThree16b"]
2 — Laravel / ThinkPHP merged config (encryptable.previous_keys)
PHP array (same order semantics as above). Each string must match the same length OpenSSL expects for your cipher (e.g. aes-128-ecb typically uses a 16-byte secret string).
'previous_keys' => [ 'oldKeyOne16bytes!!', 'olderKeyTwo16byte', 'ancientKeyThree16b', ],
Or reuse the parser from env in config/encryptable.php / plugin app.php (already the default): PreviousKeysParser::parse(env('ENCRYPTION_PREVIOUS_KEYS')).
3 — Webman plugin app.php / Hyperf autoload
Use the same previous_keys key as in the stubs: either a PHP array as above, or PreviousKeysParser::parse(env('ENCRYPTION_PREVIOUS_KEYS')).
Usage
1. Model cast (Laravel / Webman with Eloquent)
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Maize\Encryptable\Encryptable; class User extends Model { protected $fillable = ['name', 'email']; protected $casts = [ 'name' => Encryptable::class, 'email' => Encryptable::class, ]; }
Attributes are encrypted/decrypted automatically on read/write.
2. Manual encrypt/decrypt (PHP)
use Maize\Encryptable\Encryption; $plain = 'value to protect'; $cipher = Encryption::php()->encrypt($plain); $restored = Encryption::php()->decrypt($cipher);
3. SQL decrypt expression (DB)
use Maize\Encryptable\Encryption; // Fragment usable in SELECT / WHERE (syntax differs for MySQL vs Postgres) $expr = Encryption::db()->decrypt($encryptedPayloadFromDb);
DBEncrypter::encrypt()throws “not supported”; this path targets SQL-side decrypt/compare scenarios.
4. Custom validation rules
use Illuminate\Support\Facades\Validator; use Illuminate\Validation\Rule; use Maize\Encryptable\Rules\ExistsEncrypted; use Maize\Encryptable\Rules\UniqueEncrypted; Validator::make($data, [ 'email' => [ 'required', 'email', new ExistsEncrypted('users'), // or Rule::existsEncrypted('users') ], ]); Validator::make($data, [ 'email' => [ 'required', 'email', new UniqueEncrypted('users'), // or Rule::uniqueEncrypted('users') ], ]);
Composer / dependencies
- Runtime
require: onlyphp,ext-openssl, andpsr/container, so Hyperf projects are not forced to pull all of Illuminate. - Laravel / Webman (Illuminate): satisfy
EncryptableServiceProvider, casts, and rules vialaravel/frameworkor the suggestedilluminate/*packages (seecomposer.json→suggest).
Development
composer install composer test # Pest composer analyse # PHPStan (requires local config) composer format # Laravel Pint
References & credits
- Upstream reference: maize-tech/laravel-encryptable — canonical design, issues, and community discussion for the original Laravel package.
- This fork extends multi-framework bridges, Composer metadata, and config/container resolution; compare
CHANGELOGand config when migrating from upstream.
License
MIT. See LICENSE.md.