This example demonstrates the main Audit features in one small application:
- auditing an Invoice and its Lines
- requested changes
- reactive changes
- redacting a sensitive field
- ignoring a sensitive field
- linking reactive audit actions
- displaying the audit log
The example uses the following data model:
Invoice
|
+-- Lines
Each line has a price and quantity. Its total is calculated automatically, and the invoice total is updated whenever a line changes.
The application tables can be created with:
create table invoice (
id int not null primary key auto_increment,
ref varchar(255),
total decimal(8,2),
paypal_id varchar(255),
paypal_token varchar(255)
);
create table line (
id int not null primary key auto_increment,
invoice_id int,
item varchar(255),
price decimal(8,2),
qty int,
total decimal(8,2)
);Create the Audit table separately using the audit_log.sql schema supplied by the package.
class Invoice extends \Atk4\Data\Model
{
public $table = 'invoice';
protected function init(): void
{
parent::init();
$this->hasMany('Lines', [
'model' => [Line::class],
]);
$this->addField('ref', [
'type' => 'string',
]);
$this->addField('total', [
'type' => 'atk4_money',
'default' => 0.00,
]);
$this->addField('paypal_id', [
'type' => 'string',
]);
$this->addField('paypal_token', [
'type' => 'string',
]);
}
public function adjustTotal($change): void
{
$this->save([
'total' => $this->get('total') + $change,
]);
}
}class Line extends \Atk4\Data\Model
{
public $table = 'line';
protected function init(): void
{
parent::init();
$this->hasOne('invoice_id', [
'model' => [Invoice::class],
]);
$this->addField('item', [
'type' => 'string',
]);
$this->addField('price', [
'type' => 'atk4_money',
'default' => 0.00,
]);
$this->addField('qty', [
'type' => 'integer',
'default' => 0,
]);
$this->addField('total', [
'type' => 'atk4_money',
'default' => 0.00,
]);
$this->addHook('beforeSave', function ($m) {
$m->set(
'total',
$m->get('price') * $m->get('qty')
);
});
$this->addHook('afterSave', function ($m) {
if ($m->isDirty('total')) {
$change = $m->get('total')
- $m->getDirtyRef()['total'];
$m->ref('invoice_id')->adjustTotal($change);
}
});
}
}When a line is saved:
beforeSavecalculates its total.afterSavecalculates the difference.- The related Invoice is updated.
This gives us a simple example of reactive auditing.
Create one controller for the persistence.
The PayPal policy applies only to Invoice, so attach it specifically to that model:
$audit = new \Atk4\Audit\AuditController($db);
$invoicePolicy = (new \Atk4\Audit\AuditPolicy())
->redactField('paypal_id')
->ignoreField('paypal_token');
$audit->setModelPolicy(
Invoice::class,
$invoicePolicy
);
$audit->observePersistence($db);The policy means:
paypal_id -> audited, but values are [REDACTED]
paypal_token -> completely ignored
No policy is applied to Line, so its fields are audited normally.
All models added to the persistence afterwards are automatically observed.
$invoice = new Invoice($db);
$invoice->save([
'ref' => 'inv1',
'paypal_id' => 'PAYPAL-12345',
'paypal_token' => 'very-secret-token',
]);The create operation is audited.
Conceptually, its requested diff contains:
{
"ref": [null, "inv1"],
"paypal_id": [null, "[REDACTED]"]
}paypal_token is absent because it is ignored.
The actual PayPal ID and token are never stored in the audit diff.
$invoice->ref('Lines')->insert([
'item' => 'Chair',
'price' => 2.50,
'qty' => 3,
]);
$invoice->ref('Lines')->insert([
'item' => 'Desk',
'price' => 10.20,
'qty' => 1,
]);The resulting invoice total is:
17.70
Audit records the line creation and the reactive invoice update.
Now change the quantity of the first line:
$line = $invoice->ref('Lines')->load(1);
$line->set('qty', 4);
$line->save();The requested change is:
{
"qty": [3, 4]
}The line total is recalculated from:
7.50 -> 10.00
so the line audit entry contains a reactive change similar to:
{
"total": [7.5, 10]
}The parent invoice is then updated:
17.70 -> 20.20
That update gets its own audit entry, linked to the original line operation.
The resulting structure is conceptually:
Audit #10
Line #1
requested:
qty 3 -> 4
reactive:
total 7.50 -> 10.00
|
+-- Audit #11
Invoice #1
requested:
total 17.70 -> 20.20
This relationship is recorded using initiator_audit_log_id.
The same invoice can now change its PayPal information:
$invoice->set('paypal_id', 'PAYPAL-67890');
$invoice->set('paypal_token', 'another-secret-token');
$invoice->save();The audit records the change to paypal_id, but only as:
{
"paypal_id": [
"[REDACTED]",
"[REDACTED]"
]
}The paypal_token change is not present.
This demonstrates the difference between redaction and ignoring a field:
redactField()
The change is visible, the values are hidden.
ignoreField()
The field is not present in the audit.
A global audit page can use AuditGrid:
use Atk4\Audit\Model\AuditLog;
use Atk4\Audit\View\AuditGrid;
$logs = new AuditLog($db);
AuditGrid::addTo($app)
->setModel($logs);For the history of one invoice:
AuditGrid::addTo($app)
->setModel($invoice->ref('AuditLog'));Load an audit entity and pass it to AuditDetail:
use Atk4\Audit\Model\AuditLog;
use Atk4\Audit\View\AuditDetail;
$log = (new AuditLog($db))->load($auditLogId);
AuditDetail::addTo($app)
->setEntity($log);The detail view allows the user to inspect the requested and reactive changes and navigate between related audit actions.
The essential Audit setup for this example is therefore just:
$audit = new \Atk4\Audit\AuditController($db);
$invoicePolicy = (new \Atk4\Audit\AuditPolicy())
->redactField('paypal_id')
->ignoreField('paypal_token');
$audit->setModelPolicy(
Invoice::class,
$invoicePolicy
);
$audit->observePersistence($db);After that, normal Invoice and Line operations are automatically audited, with the special PayPal rules applied only to Invoice.