Encoded Records
Pop\Db\Record\Encoded extends Record and encodes named columns on the way in, decoding them on the way
back out — a JSON column that reads as an array, a password column that stores a hash, a column encrypted
at rest. Declare which columns get which treatment as properties and extend Record\Encoded instead of
Record.
Everything a record class already does keeps working: a $fillable allowlist, a beforeSave() hook, a
$primaryKeys override, every finder on Records & the ORM. Only the parent changes.
<?php
namespace App\Table;
use Pop\Db\Record\Encoded;
class Users extends Encoded
{
protected array $hashFields = ['password'];
protected array $jsonFields = ['preferences'];
protected string $hashAlgorithm = PASSWORD_BCRYPT;
protected array $hashOptions = ['cost' => 12];
}
Assignment encodes, and reading decodes. Nothing at the call site changes.
use App\Table\Users;
$user = new Users([
'username' => 'testuser',
'email' => 'testuser@test.com',
'password' => 'secret',
'preferences' => ['theme' => 'dark']
]);
$user->save();
The stored row holds a bcrypt hash in password and {"theme":"dark"} in preferences. Read it back and
$user->preferences['theme'] is 'dark' again — toArray() decodes too. getRawValue('preferences')
gives you the stored string instead.
There are five encodings, one property each.
| Property | The column stores | Reading gives you back |
|---|---|---|
$jsonFields |
json_encode() output |
the decoded array |
$phpFields |
serialize() output |
the unserialized value |
$base64Fields |
base64_encode() output |
the decoded string |
$hashFields |
a one-way password hash | the hash itself |
$encryptedFields |
a base64-wrapped iv/value/mac/tag payload |
the decrypted plaintext |
$encryptedFields needs a key. The $key and $cipherMethod properties on the class win, and APP_KEY
and APP_CIPHER_METHOD in $_ENV fill in whichever the class leaves empty. Add both to .env yourself —
the skeleton ships neither — and generate the key as base64: base64_encode(random_bytes(32)).
# generate one with: php -r "echo base64_encode(random_bytes(32));"
APP_CIPHER_METHOD=aes-256-cbc
APP_KEY=<base64 of 32 random bytes>
APP_PREVIOUS_KEYS=<retired base64 keys, comma-separated>
Two different failures wait here and they come from different classes. With either value missing — the
guard is an or, not an and — encoding throws Pop\Db\Record\Exception,
Error: The encryption properties have not been set. With both present but the key not valid base64
for the cipher, it's Pop\Crypt\Encryption\Exception,
Error: Invalid key or unsupported cipher.
APP_PREVIOUS_KEYS holds a comma-separated list of retired keys, so decryption keeps working through a
rotation while new writes use the current one. Every entry is base64 like APP_KEY.
APP_PREVIOUS_KEYS is read alongside APP_KEY, so a class that sets $key itself supplies its own
previous keys too.
Hashed columns are checked with verify(), which takes the column name and the attempted plaintext:
use App\Table\Users;
$user = Users::findOne(['username' => 'testuser']);
if ($user->verify('password', 'secret')) {
// credentials are good
}
New in v7, verify() also upgrades a hash it finds out of date — hashed under a weaker cost or an older
algorithm than the class declares. Verify against a $2y$04$ hash while the class asks for cost 12 and the
stored hash comes out $2y$12$, so a password store migrates itself as people log in.
The third argument turns that off. Pass false when the record is not yours to save, or when you would
rather batch the writes, and verify() records what it found instead of acting on it:
if ($user->verify('password', $attemptedPassword, false)) {
if ($user->needsRehash()) {
$user->rehash('password', $attemptedPassword);
}
}
needsRehash() reports the outdated hash and rehash() acts on it, re-encoding under the current
$hashAlgorithm and $hashOptions and saving. With the default it has already run by the time verify()
returns.
rehash()'s value argument carries #[\SensitiveParameter], so PHP redacts it from stack traces.
See Also#
- Records & the ORM — the record class this extends, and everything it keeps
- Auth Records —
Record\Auth, which builds a login flow on top of this - Encryption — the pop-crypt ciphers
$encryptedFieldsruns through - Configuration — where
APP_KEYand the rest of.envare read - pop-db README — the full
Record\EncodedAPI surface