[1.67.0] - 2026-07-10 — Adhil
Theme: extension seams — four opt-in extension points (independent DB sessions, around-execution
wrappers, write-side row hooks, and blob lifecycle/authorization hooks) that are exact pass-throughs
until an application binds them. No new env vars, no migrations, no default changes.
Added
- Blob lifecycle + authorization seams on the upload pipeline. Two optional contracts let an
application observe and gate the framework's blob endpoints without replacing the controller:Glueful\Uploader\Contracts\BlobCreatedHook::onBlobCreated(string $blobUuid, ?string $uploaderUserUuid)
runs after a blob row is persisted; throwing rejects the upload and the controller compensates —
it deletes the stored object (checked, not assumed) and hard-deletes the blob row via the new
BlobRepository::forceDelete(), falling back to a verifiedstatus='deleted'quarantine so a failed
hook can never leave a servable, unattributed blob.Glueful\Uploader\Contracts\BlobAccessPolicy::authorizeAccess(array $blob, BlobAccessContext $ctx)
runs after the framework's own visibility/auth/signature checks onshow/info/delete/signedUrl,
receiving aBlobAccessContext {action: BlobAction, authenticatedUserUuid, signatureValid}(the
signature is computed once per request and reused). Returningfalseyields a 404.
Both resolve softly from the container (NullBlobCreatedHook/NullBlobAccessPolicypass-through
defaults — the framework binds nothing), so existing applications are byte-for-byte unaffected.
- Deferred thumbnail generation.
FileUploader::generateThumbnailFor()generates an upload's
thumbnail after the fact; the upload controller now defers thumbnail creation until the
BlobCreatedHookhas accepted the blob, so a rejected upload never leaves an orphaned public
thumbnail. Thumbnail failures after acceptance are logged and degrade tothumb_url: null— a
committed upload never 500s on a thumbnail error. - Independent database sessions.
Connection::newPdo()opens a non-pooled PDO using the
connection's resolved configuration for advisory locks and other session-scoped infrastructure. - Around-execution database wrappers.
QueryExecutor::addExecutionWrapper()composes generic
ExecutionWrapperInterfaceimplementations around the actual prepare/execute operation, allowing
extensions to hold locks or other resources for the full statement boundary. The registry is
process-level and resettable throughclearExecutionWrappers(). - Process-level write hooks on
Connection— the write-side counterpart to the existingtable()
read hooks.Connection::addInsertHook(\Closure $hook)registers a
fn(string $table, array $data): arraythat runs, in registration order, over the row of every
QueryBuilderinsert(),insertBatch(), andupsert(), so an extension can transparently stamp or
transform columns (a tenant key,created_by, an encrypted field, …) in one place instead of in every
repository.clearInsertHooks()resets the process-global registry (mirrorsclearTableHooks()) and
applyInsertHooks()is the seam the query builder calls. Batch inserts are hardened around hook
output: a hook that produces a non-uniform column set across the batch, or a list-shaped
(non-associative) row, raises\UnexpectedValueExceptionbefore SQL generation, and each row's key
order is normalized to the first row's so a reordered-but-equal column set cannot misalign positional
binding. When no hook is registered the seam is a pass-through — no behavior change for existing code.