BackupService
in package
Database-agnostic backup and restore service.
Exports all application tables via direct database reads as JSON, compresses with gzip, and encrypts with AES-256-GCM. Restore reverses the process.
Table of Contents
Constants
- CIPHER : mixed = 'aes-256-gcm'
- EXCLUDED_TABLES : mixed = ['queued_jobs', 'queue_processes', 'backups', '...
- Tables excluded from backup because they contain transient runtime state.
- FORMAT_VERSION : mixed = 2
- HEADER_PREFIX_LENGTH : mixed = 1048576
- IV_LENGTH : mixed = 12
- OPERATIONAL_SCHEMA_TABLES : mixed = ['backups', 'queued_jobs', 'queue_processes', '...
- Runtime tables whose rows are excluded but schemas must be preserved.
- PBKDF2_ALGO : mixed = 'sha256'
- PBKDF2_ITERATIONS : mixed = 100000
- SALT_LENGTH : mixed = 16
- TAG_LENGTH : mixed = 16
Properties
- $connectionName : string
Methods
- __construct() : mixed
- decryptToLogicalArchive() : string
- Decrypt a passphrase-protected backup into its logical archive.
- encryptLogicalArchive() : string
- Protect a managed logical archive with a temporary tenant restore credential.
- export() : array{data: string, meta: array}
- Export all application tables to an encrypted backup file.
- exportLogicalArchive() : array{data: string, meta: array{table_count: int, row_count: int, size_bytes: int}}
- Export all application tables as a gzip-compressed JSON logical archive.
- getMigrationFingerprint() : array<string, array<int, array{version: string, migration_name: string}>>
- Snapshot migration state for fingerprinting a backup.
-
import()
: array{table_count: int, row_count: int, constraints_not_valid?: int, payload_upgrade?: array
, post_restore?: array } - Import (restore) from an encrypted backup.
-
importLogicalArchive()
: array{table_count: int, row_count: int, constraints_not_valid?: int, payload_upgrade?: array
, post_restore?: array } - Import a gzip-compressed JSON logical archive.
- validateImportHeader() : void
- Validate that an encrypted backup starts with the expected JSON metadata.
- validateImportPayload() : void
- Validate that an encrypted backup can be opened before starting restore side effects.
- validateLogicalArchive() : void
- Validate a gzip-compressed JSON logical archive without restoring it.
- buildRestoreTableMap() : array<string, array<int, array<string, mixed>>>
- Build restore table map for the current schema.
- clearApplicationCachesForRestore() : void
- Clear Cake cache pools around restore so schema and runtime state match DB.
- connection() : Connection
- Return the configured backup target connection.
-
decodeLogicalPayload()
: array{meta: array
, schema: array , tables: array } - decrypt() : string
- Decrypt data encrypted by encrypt().
- deriveKey() : string
- Derive a 256-bit key from a passphrase using PBKDF2.
- describeFingerprintDiff() : string
- Build a short human-readable diff between backup and current migration fingerprints for error messages.
- dropPostgresForeignKeys() : array<int, array{table: string, name: string, definition: string}>
- Drop FK constraints for Postgres tables and return definitions for later re-add.
- encrypt() : string
- Encrypt data with AES-256-GCM using a PBKDF2-derived key.
-
ensureOperationalSchemaTables()
: array{meta: array
, schema: array , tables: array } - Add operational table definitions that older backup manifests omitted.
- filterRowsToCurrentColumns() : array<int, array<string, mixed>>
- Drop columns that existed in the backup environment but no longer exist in the current schema.
- getInsertBatchSize() : int
- Get insert batch size.
- importPayloadRows() : array{table_count: int, row_count: int}
- insertBatchRows() : void
- isExcludedTable() : bool
- Return true when the table should never appear in a backup payload.
- isIntegerLikeType() : bool
- Check if integer like type.
- isNumericColumnType() : bool
- Check if numeric column type.
- normalizeBooleanForPostgres() : mixed
- Normalize Postgres boolean column values from mixed JSON payload forms.
- normalizeColumnScalarForMysql() : mixed
- Coerce scalar values into DB-safe representations for MySQL inserts.
- normalizeComplexValueForMysql() : array{converted: bool, value: mixed}
- Coerce array/object values for MySQL inserts.
- normalizeEmptyStringForMysql() : array{converted: bool, value: mixed}
- Coerce empty-string values for MySQL numeric/boolean/temporal columns.
- normalizeNullForMysql() : array{converted: bool, value: mixed}
- Coerce nulls for non-nullable MySQL columns.
- normalizeRowForInsert() : array<string, mixed>
- Normalize row values for MySQL inserts, coercing temporal values from ISO-8601 JSON forms to DB-friendly SQL literal formats.
- normalizeTemporalValueForMysql() : string|null
- Convert ISO-8601 temporal strings to MySQL-compatible temporal formats.
- readGzipPrefix() : string
- Decompress only the requested prefix without expanding the full archive.
- reportProgress() : void
- reportSchemaMismatch() : void
- resetPostgresSequences() : void
- Reset Postgres sequences so new rows follow restored primary keys.
- restorePostgresForeignKeys() : int
- Re-add previously dropped Postgres FK constraints.
- validateLogicalArchiveHeader() : void
- Validate the bounded header of a compressed logical archive.
Constants
CIPHER
private
mixed
CIPHER
= 'aes-256-gcm'
EXCLUDED_TABLES
Tables excluded from backup because they contain transient runtime state.
private
mixed
EXCLUDED_TABLES
= ['queued_jobs', 'queue_processes', 'backups', 'sessions']
Queue state, backup metadata, and session tokens should not cross environments. Phinx migration logs are intentionally preserved so a restored database keeps accurate main/plugin migration history.
FORMAT_VERSION
private
mixed
FORMAT_VERSION
= 2
HEADER_PREFIX_LENGTH
private
mixed
HEADER_PREFIX_LENGTH
= 1048576
IV_LENGTH
private
mixed
IV_LENGTH
= 12
OPERATIONAL_SCHEMA_TABLES
Runtime tables whose rows are excluded but schemas must be preserved.
private
mixed
OPERATIONAL_SCHEMA_TABLES
= ['backups', 'queued_jobs', 'queue_processes', 'sessions']
PBKDF2_ALGO
private
mixed
PBKDF2_ALGO
= 'sha256'
PBKDF2_ITERATIONS
private
mixed
PBKDF2_ITERATIONS
= 100000
SALT_LENGTH
private
mixed
SALT_LENGTH
= 16
TAG_LENGTH
private
mixed
TAG_LENGTH
= 16
Properties
$connectionName read-only
private
string
$connectionName
= 'default'
Methods
__construct()
public
__construct([string $connectionName = 'default' ]) : mixed
Parameters
- $connectionName : string = 'default'
-
CakePHP connection name to back up or restore.
decryptToLogicalArchive()
Decrypt a passphrase-protected backup into its logical archive.
public
decryptToLogicalArchive(string $encryptedData, string $encryptionKey) : string
Used by the platform legacy-import path to convert a .kmpbackup
(upstream/self-service format) into a managed envelope-encrypted backup.
Parameters
- $encryptedData : string
- $encryptionKey : string
Return values
stringencryptLogicalArchive()
Protect a managed logical archive with a temporary tenant restore credential.
public
encryptLogicalArchive(string $compressedData, string $encryptionKey) : string
Parameters
- $compressedData : string
- $encryptionKey : string
Return values
stringexport()
Export all application tables to an encrypted backup file.
public
export(string $encryptionKey) : array{data: string, meta: array}
Parameters
- $encryptionKey : string
-
User-provided encryption key
Return values
array{data: string, meta: array} —Encrypted bytes and metadata
exportLogicalArchive()
Export all application tables as a gzip-compressed JSON logical archive.
public
exportLogicalArchive() : array{data: string, meta: array{table_count: int, row_count: int, size_bytes: int}}
Encryption is intentionally left to the caller so managed platform backups can use per-backup envelope keys without double encryption.
Return values
array{data: string, meta: array{table_count: int, row_count: int, size_bytes: int}}getMigrationFingerprint()
Snapshot migration state for fingerprinting a backup.
public
getMigrationFingerprint() : array<string, array<int, array{version: string, migration_name: string}>>
Returns a map of phinxlog-table-name → array of {version, migration_name} rows sorted by version. The structure stays stable across engines so the fingerprint can be compared between the bake environment (e.g. MySQL) and the restore environment (e.g. Postgres).
Return values
array<string, array<int, array{version: string, migration_name: string}>>import()
Import (restore) from an encrypted backup.
public
import(string $encryptedData, string $encryptionKey[, callable(array<string, mixed>): void|null $progressReporter = null ][, callable|array{ignoreSchemaMismatch?: bool}|null $options = [] ][, callable(): void|null $migrationRunner = null ]) : array{table_count: int, row_count: int, constraints_not_valid?: int, payload_upgrade?: array, post_restore?: array}
Parameters
- $encryptedData : string
-
Raw encrypted backup bytes
- $encryptionKey : string
-
User-provided encryption key
- $progressReporter : callable(array<string, mixed>): void|null = null
-
Restore progress callback
- $options : callable|array{ignoreSchemaMismatch?: bool}|null = []
-
Restore options, or migration runner for BC
- $migrationRunner : callable(): void|null = null
-
Post-import migration callback
Return values
array{table_count: int, row_count: int, constraints_not_valid?: int, payload_upgrade?: arrayImport statistics
importLogicalArchive()
Import a gzip-compressed JSON logical archive.
public
importLogicalArchive(string $compressedData[, callable(array<string, mixed>): void|null $progressReporter = null ][, callable|array{ignoreSchemaMismatch?: bool}|null $options = [] ][, callable(): void|null $migrationRunner = null ]) : array{table_count: int, row_count: int, constraints_not_valid?: int, payload_upgrade?: array, post_restore?: array}
Parameters
- $compressedData : string
-
Gzip-compressed versioned JSON payload
- $progressReporter : callable(array<string, mixed>): void|null = null
-
Restore progress callback
- $options : callable|array{ignoreSchemaMismatch?: bool}|null = []
-
Restore options, or migration runner for BC
- $migrationRunner : callable(): void|null = null
-
Post-import migration callback
Return values
array{table_count: int, row_count: int, constraints_not_valid?: int, payload_upgrade?: arrayvalidateImportHeader()
Validate that an encrypted backup starts with the expected JSON metadata.
public
validateImportHeader(string $encryptedData, string $encryptionKey) : void
This intentionally avoids full JSON decoding in web requests; the queued restore task performs full validation before making restore changes.
Parameters
- $encryptedData : string
- $encryptionKey : string
validateImportPayload()
Validate that an encrypted backup can be opened before starting restore side effects.
public
validateImportPayload(string $encryptedData, string $encryptionKey) : void
This decrypts, decompresses, and validates the payload structure only; it does not reset schema, write rows, queue work, or mutate restore status.
Parameters
- $encryptedData : string
- $encryptionKey : string
validateLogicalArchive()
Validate a gzip-compressed JSON logical archive without restoring it.
public
validateLogicalArchive(string $compressedData) : void
Parameters
- $compressedData : string
buildRestoreTableMap()
Build restore table map for the current schema.
private
buildRestoreTableMap(array<string, array<int, array<string, mixed>>> $payloadTables, array<int, string> $currentTables) : array<string, array<int, array<string, mixed>>>
A backup is a full logical snapshot. When an older backup is restored into a newer schema, tables introduced after the backup was taken must be emptied so stale current-environment data cannot survive the restore. Tables that no longer exist in the current schema are left in the decoded payload for compatibility migrators, but they are not inserted directly.
Parameters
- $payloadTables : array<string, array<int, array<string, mixed>>>
- $currentTables : array<int, string>
Return values
array<string, array<int, array<string, mixed>>>clearApplicationCachesForRestore()
Clear Cake cache pools around restore so schema and runtime state match DB.
private
clearApplicationCachesForRestore() : void
connection()
Return the configured backup target connection.
private
connection() : Connection
Return values
ConnectiondecodeLogicalPayload()
private
decodeLogicalPayload(string $compressedData[, callable(array<string, mixed>): void|null $progressReporter = null ]) : array{meta: array, schema: array, tables: array}
Parameters
- $compressedData : string
- $progressReporter : callable(array<string, mixed>): void|null = null
Return values
array{meta: arraydecrypt()
Decrypt data encrypted by encrypt().
private
decrypt(string $data, string $passphrase) : string
Parameters
- $data : string
- $passphrase : string
Return values
stringderiveKey()
Derive a 256-bit key from a passphrase using PBKDF2.
private
deriveKey(string $passphrase, string $salt) : string
Parameters
- $passphrase : string
- $salt : string
Return values
stringdescribeFingerprintDiff()
Build a short human-readable diff between backup and current migration fingerprints for error messages.
private
describeFingerprintDiff(array<string, array<int, array{version: string, migration_name: string}>> $backup, array<string, array<int, array{version: string, migration_name: string}>> $current) : string
Parameters
- $backup : array<string, array<int, array{version: string, migration_name: string}>>
- $current : array<string, array<int, array{version: string, migration_name: string}>>
Return values
stringdropPostgresForeignKeys()
Drop FK constraints for Postgres tables and return definitions for later re-add.
private
dropPostgresForeignKeys(mixed $connection, mixed $driver, array<int, string> $tableNames) : array<int, array{table: string, name: string, definition: string}>
Parameters
- $connection : mixed
- $driver : mixed
- $tableNames : array<int, string>
Return values
array<int, array{table: string, name: string, definition: string}>encrypt()
Encrypt data with AES-256-GCM using a PBKDF2-derived key.
private
encrypt(string $data, string $passphrase) : string
Output format: salt(16) + iv(12) + tag(16) + ciphertext
Parameters
- $data : string
- $passphrase : string
Return values
stringensureOperationalSchemaTables()
Add operational table definitions that older backup manifests omitted.
private
ensureOperationalSchemaTables(array{meta: array, schema: array, tables: array} $payload) : array{meta: array, schema: array, tables: array}
The rows remain excluded from the restore payload, but preserving table shapes keeps runtime services usable after schema reset.
Parameters
-
$payload
: array{meta: array
, schema: array , tables: array }
Return values
array{meta: arrayfilterRowsToCurrentColumns()
Drop columns that existed in the backup environment but no longer exist in the current schema.
private
filterRowsToCurrentColumns(array<int, array<string, mixed>> $rows, TableSchemaInterface $tableSchema) : array<int, array<string, mixed>>
Parameters
- $rows : array<int, array<string, mixed>>
- $tableSchema : TableSchemaInterface
Return values
array<int, array<string, mixed>>getInsertBatchSize()
Get insert batch size.
private
getInsertBatchSize(int $columnCount, bool $isPostgres) : int
Parameters
- $columnCount : int
- $isPostgres : bool
Return values
intimportPayloadRows()
private
importPayloadRows(array{schema: array, tables: array} $payload[, callable(array<string, mixed>): void|null $progressReporter = null ]) : array{table_count: int, row_count: int}
Parameters
-
$payload
: array{schema: array
, tables: array } - $progressReporter : callable(array<string, mixed>): void|null = null
Return values
array{table_count: int, row_count: int}insertBatchRows()
private
insertBatchRows(mixed $connection, mixed $driver, string $quotedTable, array<int, string> $columns, array<int, array<string, mixed>> $batch, TableSchemaInterface $tableSchema, bool $isPostgres) : void
Parameters
- $connection : mixed
- $driver : mixed
- $quotedTable : string
- $columns : array<int, string>
- $batch : array<int, array<string, mixed>>
- $tableSchema : TableSchemaInterface
- $isPostgres : bool
isExcludedTable()
Return true when the table should never appear in a backup payload.
private
static isExcludedTable(string $tableName) : bool
Parameters
- $tableName : string
Return values
boolisIntegerLikeType()
Check if integer like type.
private
isIntegerLikeType(string $columnType) : bool
Parameters
- $columnType : string
Return values
boolisNumericColumnType()
Check if numeric column type.
private
isNumericColumnType(string $columnType) : bool
Parameters
- $columnType : string
Return values
boolnormalizeBooleanForPostgres()
Normalize Postgres boolean column values from mixed JSON payload forms.
private
normalizeBooleanForPostgres(TableSchemaInterface $tableSchema, string $column, mixed $value) : mixed
Parameters
- $tableSchema : TableSchemaInterface
- $column : string
- $value : mixed
normalizeColumnScalarForMysql()
Coerce scalar values into DB-safe representations for MySQL inserts.
private
normalizeColumnScalarForMysql(mixed $value, string $columnType) : mixed
Parameters
- $value : mixed
- $columnType : string
normalizeComplexValueForMysql()
Coerce array/object values for MySQL inserts.
private
normalizeComplexValueForMysql(mixed $value, string $columnType) : array{converted: bool, value: mixed}
Parameters
- $value : mixed
- $columnType : string
Return values
array{converted: bool, value: mixed}normalizeEmptyStringForMysql()
Coerce empty-string values for MySQL numeric/boolean/temporal columns.
private
normalizeEmptyStringForMysql(TableSchemaInterface $tableSchema, string $column, string $columnType) : array{converted: bool, value: mixed}
Parameters
- $tableSchema : TableSchemaInterface
- $column : string
- $columnType : string
Return values
array{converted: bool, value: mixed}normalizeNullForMysql()
Coerce nulls for non-nullable MySQL columns.
private
normalizeNullForMysql(TableSchemaInterface $tableSchema, string $column, string $columnType) : array{converted: bool, value: mixed}
Parameters
- $tableSchema : TableSchemaInterface
- $column : string
- $columnType : string
Return values
array{converted: bool, value: mixed}normalizeRowForInsert()
Normalize row values for MySQL inserts, coercing temporal values from ISO-8601 JSON forms to DB-friendly SQL literal formats.
private
normalizeRowForInsert(array<string, mixed> $row, array<int, string> $columns, TableSchemaInterface $tableSchema, bool $isPostgres) : array<string, mixed>
Parameters
- $row : array<string, mixed>
- $columns : array<int, string>
- $tableSchema : TableSchemaInterface
- $isPostgres : bool
Return values
array<string, mixed>normalizeTemporalValueForMysql()
Convert ISO-8601 temporal strings to MySQL-compatible temporal formats.
private
normalizeTemporalValueForMysql(string $value, string $columnType) : string|null
Parameters
- $value : string
- $columnType : string
Return values
string|nullreadGzipPrefix()
Decompress only the requested prefix without expanding the full archive.
private
readGzipPrefix(string $compressedData, int $maxLength) : string
Parameters
- $compressedData : string
- $maxLength : int
Return values
stringreportProgress()
private
reportProgress(callable(array<string, mixed>): void|null $progressReporter, string $phase, string $message[, array<string, mixed> $context = [] ]) : void
Parameters
- $progressReporter : callable(array<string, mixed>): void|null
- $phase : string
- $message : string
- $context : array<string, mixed> = []
reportSchemaMismatch()
private
reportSchemaMismatch(array<string, mixed> $payload, bool $ignoreSchemaMismatch[, callable(array<string, mixed>): void|null $progressReporter = null ]) : void
Parameters
- $payload : array<string, mixed>
- $ignoreSchemaMismatch : bool
- $progressReporter : callable(array<string, mixed>): void|null = null
resetPostgresSequences()
Reset Postgres sequences so new rows follow restored primary keys.
private
resetPostgresSequences(Connection $connection, mixed $schemaCollection, mixed $driver, array<int, string> $tableNames) : void
Parameters
- $connection : Connection
- $schemaCollection : mixed
- $driver : mixed
- $tableNames : array<int, string>
restorePostgresForeignKeys()
Re-add previously dropped Postgres FK constraints.
private
restorePostgresForeignKeys(mixed $connection, mixed $driver, array<int, array{table: string, name: string, definition: string}> $droppedForeignKeys) : int
Parameters
- $connection : mixed
- $driver : mixed
- $droppedForeignKeys : array<int, array{table: string, name: string, definition: string}>
Return values
intvalidateLogicalArchiveHeader()
Validate the bounded header of a compressed logical archive.
private
validateLogicalArchiveHeader(string $compressedData) : void
Parameters
- $compressedData : string