Search by

yahyaerturan / audit-codeigniter4

yahyaerturan

CodeIgniter 4 adapter for the yahyaerturan/audit immutable audit-trail library.

Package info

github.com/yahyaerturan/audit-codeigniter4

pkg:composer/yahyaerturan/audit-codeigniter4

Statistics

Installs: 3

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

2.0.0 2026-09-08 11:14 UTC

This package is auto-updated.

Last update: 2026-09-08 12:21:01 UTC


README

CodeIgniter 4 integration for the framework-neutral yahyaerturan/audit immutable audit-trail library. The adapter holds all CI4 coupling so the core package stays free of any framework dependency.

  • Runs audit SQL on your application's own CI4 connection for a configured database group — no hidden second PDO handle, so an append can join your surrounding transaction.
  • Auto-discovers its services, config class, Spark commands, and an opt-in request filter.
  • Ships safe defaults: anonymous/system actor, no tenancy, request IP/user-agent capture off, and no silent error swallowing.

Requirements

Requirement Version
PHP ^8.5
CodeIgniter codeigniter4/framework ^4.7
Core library yahyaerturan/audit ^2.0 (installed automatically)
Extensions ext-json
Database SQLite3 or PostgreSQL (see Supported databases)

The PHP floor is deliberately narrower than CodeIgniter's own: CI4 4.7 supports PHP 8.2+, whereas this adapter and the core library require PHP 8.5. The constraints still intersect cleanly (^8.5 ∩ ^8.2>=8.5 <9.0), but an application still running PHP 8.3 or 8.4 cannot install 2.0.0. For that case 1.0.0 remains published on the ^8.3 floor, and Composer selects it automatically when the platform does not satisfy ^8.5.

Installation

composer require yahyaerturan/audit-codeigniter4

On PHP 8.3 or 8.4, pin the last release built for that floor (it pulls the core at ^1.0):

composer require yahyaerturan/audit-codeigniter4:^1.0

The package is discovered automatically through Composer's PSR-4 map: CI4 finds its Config\Services, Config\Registrar, and Commands without any manual registration, as long as module discovery stays enabled (the framework default).

Configuration

All knobs live on YahyaErturan\Audit\CodeIgniter4\Config\Audit. They are intentionally coarse and scalar — richer behavior (actor resolution, tenancy, redaction) is customized by overriding a service, not by growing this class.

Property Type Default Purpose
databaseGroup string default CI4 database group whose connection the audit store wraps. Use your app's group to couple writes to its transaction, or a dedicated group for independent persistence.
captureRequestMethod bool true Capture the request.method metadata key on HTTP requests.
captureRequestPath bool true Capture request.path (query string excluded) on HTTP requests.
captureIpAddress bool false Capture request.ip. Off by default for privacy.
captureUserAgent bool false Capture request.user_agent. Off by default for privacy.
acceptIncomingCorrelationId bool false Trust a validated upstream correlation header. Read only by the opt-in filter; never trusted implicitly.
incomingCorrelationHeader string X-Correlation-ID Header read for an incoming correlation ID when the above is true.
metadataMaxBytes int 65536 Maximum serialized metadata bytes (mirrors the core default).
changesMaxBytes int 262144 Maximum serialized changes bytes (mirrors the core default).

Because the class extends CodeIgniter\Config\BaseConfig, override values through .env (short prefix audit) or an application Registrar — you do not edit the package file.

.env

# Point the audit store at a dedicated group defined in app/Config/Database.php
audit.databaseGroup = auditing
audit.captureIpAddress = true

Registrar (app/Config/Registrar.php)

<?php

namespace Config;

class Registrar
{
    public static function Audit(): array
    {
        return [
            'databaseGroup'    => 'auditing',
            'captureIpAddress' => true,
        ];
    }
}

Create the schema

Two Spark commands manage the audit tables on the configured databaseGroup:

php spark audit:schema:migrate   # apply pending migrations (idempotent)
php spark audit:schema:status    # report current/latest/applied/pending versions
  • audit:schema:migrate is idempotent, prints the database group and platform, then either reports the schema is up to date or applies the pending migrations. Exit code 0 on success, 1 on failure.
  • audit:schema:status prints the group, platform, current/latest versions, applied and pending migrations, and whether the schema is up to date. Exit code 0, or 1 if the status cannot be read.

Recording an entry

Resolve the recorder with service('yahyaAuditRecorder') and submit an immutable AuditRecord:

<?php

use YahyaErturan\Audit\Domain\Actor;
use YahyaErturan\Audit\Domain\AuditRecord;
use YahyaErturan\Audit\Domain\Change;
use YahyaErturan\Audit\Domain\Changes;
use YahyaErturan\Audit\Domain\Subject;

$entry = service('yahyaAuditRecorder')->record(
    AuditRecord::for('invoice.paid')
        ->by(Actor::identified('user', '42', 'Ada Lovelace'))
        ->on(Subject::identified('invoice', 'INV-123', 'March invoice'))
        ->withChanges(Changes::from([
            'status'  => Change::replaced('open', 'paid'),
            'paid_at' => Change::added('2026-09-06T12:00:00+00:00'),
        ]))
        ->withMetadata(['channel' => 'stripe', 'amount_cents' => 5000])
);

$id = $entry->id()->toString(); // canonical UUID string

The recorder fills the actor, tenant, correlation ID, request ID, and request metadata from the current request context, normalizes and redacts changes/metadata, assigns a UUIDv7 id and a recorded-at timestamp, appends exactly once, and returns the immutable AuditEntry. It retains no per-request state, so sharing it across sequential requests in a long-running worker cannot leak one request's actor/tenant/trace into the next.

AuditRecord is a fluent, immutable builder — every method returns a new instance. Available methods: for(), by(), on(), withChanges(), withMetadata(), withTenantId(), withCorrelationId(), withCausationId(), withRequestId(), occurredAt().

Reading and querying

Resolve the reader with service('yahyaAuditReader'):

<?php

use YahyaErturan\Audit\Domain\AuditId;
use YahyaErturan\Audit\Query\AuditQuery;

$reader = service('yahyaAuditReader');

// By exact id
$entry = $reader->find(AuditId::fromString('01890b1e-7c3a-7a10-9f3d-2b6f0a5c1d42'));

// Paged search — keyset pagination, canonical order occurred_at DESC, id DESC
$page = $reader->search(
    AuditQuery::all()
        ->forTenant('acme')
        ->forEvent('invoice.paid')
        ->byActor('user', '42')
        ->limit(50)
);

foreach ($page->entries() as $row) {
    // $row is an AuditEntry
}

if ($page->hasMore()) {
    $next = $reader->search(
        AuditQuery::all()->forTenant('acme')->after($page->nextCursor())
    );
}

There is deliberately no count() and no offset pagination: totals are expensive on very large audit tables and encourage offset UX. AuditPage exposes entries(), nextCursor(), and hasMore(); pass the opaque cursor back through after() unchanged.

AuditQuery modifiers: forTenant() / withoutTenant() / anyTenant(), forEvent() / eventPrefix(), byActor(), forSubject(), withCorrelationId() / withCausationId() / withRequestId(), since() / until(), forIds(), limit() (1–500, default 100), after().

Transaction semantics

The store never owns the business transaction. append() never begins, commits, or rolls back on its own — it executes its insert on the connection it is given. Point databaseGroup at the same group your application uses and the audit append becomes part of your transaction:

<?php

$db = db_connect(); // same group as Config\Audit::$databaseGroup ('default')

$db->transStart();

$invoices->markPaid($invoiceId);          // your business mutation
service('yahyaAuditRecorder')->record(   // the audit fact
    \YahyaErturan\Audit\Domain\AuditRecord::for('invoice.paid')
        ->on(\YahyaErturan\Audit\Domain\Subject::identified('invoice', (string) $invoiceId))
);

$db->transComplete();

// The audit row exists if and only if the business mutation committed.

Use a dedicated group when the audit record must survive even if the business transaction rolls back (independent persistence). Both the rollback and the commit case are covered by the adapter's integration tests.

Actor and tenant resolution

The adapter never depends on Shield or any authentication library. Out of the box:

  • Actor — a CLI request resolves to Actor::system('cli'); an HTTP (or unresolved) request resolves to Actor::anonymous(). Attributing an action to a real principal requires an application-supplied resolver.
  • TenantNullTenantResolver infers no tenant, so records are stored unscoped.

Attribute entries to real users/tenants by overriding the matching service in your application's app/Config/Services.php (CI4 discovers it with application-namespace precedence):

<?php

namespace Config;

use CodeIgniter\Config\BaseService;
use YahyaErturan\Audit\CodeIgniter4\ActorResolver;

class Services extends BaseService
{
    public static function yahyaAuditActorResolver(bool $getShared = true): ActorResolver
    {
        return new \App\Audit\ShieldActorResolver(/* inject whatever you need */);
    }
}

Your resolver implements ActorResolver::resolve(): Actor and receives nothing from the framework automatically — inject your own dependencies (for example Shield's auth()) and return an Actor. For tenancy, override yahyaAuditTenantResolver (returning TenantResolver::resolve(): ?string), or set a tenant per record with ->withTenantId('acme').

Optional request-context filter (audit-context)

Installing the package registers the audit-context filter alias (via its Registrar), but the filter is never global and never runs at boot. Attach it explicitly to routes or groups to stamp each HTTP request with adapter-private trace markers before your application runs:

// app/Config/Filters.php
public array $globals = [
    'before' => ['audit-context'],
];

// or per route group
$routes->group('api', ['filter' => 'audit-context'], static function ($routes) {
    // ...
});

The filter writes two spoof-proof headers — it removes each header before re-setting it, so a client cannot inject or duplicate them:

Header Value
X-Yahya-Audit-Request-Id a fresh UUIDv7 per request
X-Yahya-Audit-Correlation-Id a fresh UUIDv7, or a validated upstream value when acceptIncomingCorrelationId is true and incomingCorrelationHeader is present and valid

The context provider later reads these markers into each recorded entry's requestId / correlationId. The filter records nothing and never mutates the response. Without the filter the recorder still works — those two identifiers are simply null.

Supported databases

CI4 platform Core driver Status
SQLite3 sqlite supported
Postgre postgresql supported
any other (MySQLi, SQLSRV, OCI8, …) rejected at construction with UnsupportedDatabaseDriver

The bridge issues raw SQL with positional ? binds and never uses the query builder, so the group's DBPrefix is not applied to audit statements — the schema owns its table names.

Failure behavior

Nothing is silently swallowed. The adapter translates CI4's two failure modes (a thrown DatabaseException, or query() returning false with a connection error) into core audit exceptions, all of which implement the AuditException marker:

Condition Exception
Unsupported database platform UnsupportedDatabaseDriver
Transaction begin/commit/rollback failed AuditPersistenceFailed
Insert/statement failed AuditPersistenceFailed
Duplicate entry id (unique violation) DuplicateAuditEntryId
Invalid value (oversize metadata/changes, bad ids) InvalidAuditValue / InvalidAuditIdentifier
Invalid query InvalidAuditQuery
Invalid cursor InvalidAuditCursor

Because the store never owns the transaction, a thrown exception leaves the surrounding transaction for your code to roll back.

Services reference

Resolve any service with service('...'). Each is shared and safe to reuse across requests.

Service key Contract Responsibility
yahyaAuditRecorder Auditor record(AuditRecord): AuditEntry
yahyaAuditReader AuditReader find() / search()
yahyaAuditStore AuditStore append-only writer
yahyaAuditSqlConnection SqlConnection wraps the configured group's connection
yahyaAuditSchemaManager AuditSchemaManager schema migrate/status
yahyaAuditContextProvider AuditContextProvider builds the per-request context
yahyaAuditActorResolver ActorResolver override to attribute real users
yahyaAuditTenantResolver TenantResolver override for tenancy
yahyaAuditRequestAccessor RequestAccessor current-request seam

Service names are yahyaAudit*-prefixed because CI4 discovers service methods globally and unprefixed names would collide across packages.

License

MIT — see LICENSE.