Pop PHP
Database

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.

PHPapp/src/Table/Users.php
<?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.

PHP
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)).

BASH
# 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:

PHP
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:

PHP
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 RecordsRecord\Auth, which builds a login flow on top of this
  • Encryption — the pop-crypt ciphers $encryptedFields runs through
  • Configuration — where APP_KEY and the rest of .env are read
  • pop-db README — the full Record\Encoded API surface