Kontakt
Telefon +49 2161 17 58 83
Mobil +49 179 72 66 112
E-Mail info@ulrich-borchers.de

Nachrüstung eines Audit Log für PHP-Anwendungen

Ein Audit Log ist eine spezielle Form der Protokollierung, bei der Daten und Abläufe erfasst werden, die sicherheitsrelevant oder anderweitig sensibel sind. Typische Daten und Vorgänge stehen beispielsweise im Zusammenhang mit Benutzeraccounts, persönlichen Informationen, Vertragsdaten und Zahlungsvorgängen.

Da man lückenlose und den wirklich stattgefundenen Vorgängen entsprechende Daten erwartet, implementiert man ein Audit Log möglichst außerhalb der eigentlichen Anwendung, um ohne Beeinflussung deren tatsächliches Verhalten zu erfassen. Implementiert man das Audit Log dagegen innerhalb einer Anwendung, deren Verhalten protokolliert wird, besteht die Gefahr, dass die Protokollierung falsch oder unvollständig ist und dass man dieses System stört. Zudem ist eine Anpassung der Programmierung vielleicht nicht oder zumindest nicht ohne Risiken oder großen Aufwand möglich. Und schließlich kann man nicht sicherstellen, dass die innerhalb der Anwendung protokollierten Daten die "Welt" außerhalb immer und lückenlos erreichen: So könnte es vorkommen, dass zwar Informationen protokolliert werden, die Verbindung nach außen jedoch gestört ist, so dass Abläufe und Daten aufgezeichnet werden, die letztendlich so gar nicht vorkamen.

Idealerweise laufen alle "auditierten" Transaktionen über eine zentrale Schnittstelle, die dann mit einem "Audit Logging" versehen werden kann. Eine solche zentrale Schnittstelle kann beispielsweise eine API oder eine Datenbank sein. Voraussetzung ist jedoch, dass wirklich ALLES von Relevanz über diesen zentralen "Kanal" abgewickelt wird und dass hier alle Daten verfügbar sind. Außerdem muss der zentrale Kanal selbst immer verfügbar sein und absolut zuverlässig funktionieren, damit die lückenlose Protokollierung sichergestellt ist.

Man muss zudem sicherstellen, dass die Protokollierung eines Datensatzes gleichbedeutend damit ist, dass eine Transaktion mit ihren Daten genau so tatsächlich stattgefunden hat. Es dürfen keine relevanten Daten am Audit Log vorbei laufen, da man ansonsten nicht die benötigte Vollständigkeit erreicht, was jedoch für ein Audit Log sensibler Daten und Abläufe elementar ist, denn: Ein Audit Log soll die lückenlose Rekonstruktion von Abläufen und der beteiligten Daten ermöglichen und somit beweiskräftig und verlässlich sein.

Wenn in einer bestehenden Anwendung und Systemlandschaft jedoch kein zentraler Datenkanal vorhanden ist oder die zu überwachende Anwendung diesen nicht konsequent - sprich ausschließlich - benutzt, kann auf diese, eigentlich optimale Weise, ein Audit Log nicht installiert werden. Es stellt sich dann die Frage nach Alternativen.

Um dennoch nicht in den überwachten Code eingreifen zu müssen, führt die Lösungssuche für eine PHP-Anwendung in Richtung einer Extension, welche das Audit Log transparent neben dem ausgeführten Code schreibt. Eine für diesen Zweck speziell entwickelte PHP-Extension könnte direkt bei der Verarbeitung des ausgeführten Bytecodes ansetzen. Jedoch ist das mit nicht unerheblichem Aufwand und Know-How verbunden.

Falls die beiden genannten Ansätze (zentraler Kanal und PHP-Extension) nicht verwendet werden können und man also gezwungen oder gewillt ist, ein Audit Log direkt in eine (zu entwickelnde oder bereits vorhandene) PHP-Anwendung einzubauen, sei hier einen Ansatz beschrieben, der dies durch Verwendung und Erweiterung des Observer-Patterns mit geringem Aufwand ermöglicht.

Entscheidend für die Tauglichkeit einer direkt implementierten Lösung ist es, dass der bereits vorhandene Code nicht verändert werden muss, dass er nicht in seiner Flexibilität eingeschränkt wird und dass die zu protollierenden Abläufe nicht durch Seiteneffekte gestört werden. Es soll außerdem kein zusätzlicher Aufwand in Form von Anpassungen entstehen, sondern die Protokollierung relevanter Daten muss lediglich hinzugefügt werden können, ohne das bestehende System darüber hinaus verändern zu müssen oder anderweitig zu stören.

Vorab sei also darauf hingewiesen, dass die vorgestellte Lösung nicht hinreichend ist, um strengen Anforderungen an hoch-sicherheits- oder besonders finanz-relevante Systeme gerecht zu werden, da das Audit Log hier direkt in die Anwendung integriert wird. Setzt man es jedoch konsequent an allen relevanten Stellen ein, entsteht dennoch ein qualitativ hochwertiges Log, das mit anderen Lösungsansätzen gut konkurrieren kann und das in jedem Fall eine hohe Vollständigkeit der erfassten Daten bietet. Und es handelt sich um die vielleicht einzig praktikable Lösung innerhalb einer heterogenen Systemlandschaft für eine gewachsene Applikation, in welcher ein Audit Log, so wie es idealerweise praktiziert wird, aufgrund der Gegebenheiten nicht möglich ist.

In einer solchen Situation bietet der vorgestellte Ansatz die Möglichkeit, ein Audit Log in eine vorhandene Anwendung einzubauen - mit geringst möglichem Zusatzaufwand unter minimalem Eingriff in den vorhandenen Code. Die Robustheit der Lösung steht und fällt mit der Berücksichtigung dreier Dinge:

1. Hochverfügbares Schreiben protokollierter Daten

2. Konsistenz, das heißt inhaltliche Korrektheit durch Sicherstellung, dass im Fehlerfall (Error-Handler, Exception-Handler u.ä.) Daten, die von der Anwendung (ggf. teilweise) bereits protokolliert wurden:a) trotzdem (noch) geschrieben werden, wenn sie im speziellen Fehlerfall aufgezeichnet werden müssen.b) NICHT geschrieben werden, wenn dies im speziellen Fehlerfall inhaltlich falsch wäre.

3. Mitprotokollierung von Fehlerzuständen nebst relevanter Umgebungsinformationen.

Kommen wir zur hier vorstellten Lösung: Zunächst als Beispiel zwei fachliche Klassen, um daran zu zeigen, dass die Anwendung dieses Audit Log-Patterns einfach umzusetzen ist und auf vorhandenen Code aufsetzen kann, ohne diesen zu verändern. Einzige Voraussetzung: Der zu "auditierende" Code ist objektorientiert. Es ist dann lediglich zu unterscheiden, ob eine vorhandene Klasse bereits eine Basisklasse hat oder nicht. Für beide Fälle ist die Verwendung denkbar einfach.

Nehmen wir eine Klasse Customer, die es in der zu erweiternden Anwendung gibt:

class Customer
{
    protected $id;

    public function __construct($id = false)
    {
        $this->id = $id;
    }

    public function getId()
    {
        return $this->id;
    }

    public function register($name, $email)
    {
        // Neuer Kunde wird registriert

        return $this;
    }
}

Customer existiert bereits bevor das Audit Log hinzugefügt wird. Da Customer keine Basisklasse hat, kann von der Klasse AuditableEntity abgeleitet werden:

class Customer extends AuditableEntity
{
    protected $id;

    public function __construct($id = false)
    {
        $this->id = $id;
    }

    public function getId()
    {
        return $this->id;
    }

    public function register($name, $email)
    {
        // Neuer Kunde wird registriert

        // Hinzufügen von Audit-Daten:
        $this->setAuditValue('register_data', ['id' => $this->id, 'name' => $name, 'email' => $email], new AuditContext(__METHOD__, __LINE__));

        return $this;
    }
}

Der Eingriff beschränkt sich auf die Ableitung von AuditableEntity und den Einsatz von setAuditValue() dort, wo Daten protokolliert werden. Sonstige Änderungen am Code sind nicht erforderlich.

Für den Fall, dass eine Klasse bereits eine Basisklasse hat, benutzt man ein Interface und einen Trait.

Die Klasse CCPayment, so wie sie vor Einführung des Audit Log bereits vorhanden ist:

abstract class Payment { }

class CCPayment extends Payment
{
    public function executePayment(Customer $customer)
    {
        // Ausführen einer Zahlung

        return $this;
    }
}

Erweiterung um die Prokollierung von Daten im Audit Log:

class CCPayment extends Payment implements Auditable
{
    use AuditableTrait;

    public function executePayment(Customer $customer)
    {
        // Ausführen einer Zahlung

        // Hinzufügen von Audit-Daten:
        $this->setAuditValue('payment_data', ['customer_id' => $customer->getId(), 'credit_card_number' => 'XXX'], new AuditContext(__METHOD__, __LINE__));

        return $this;
    }
}

Auch hier ist der Eingriff minimal: Implementierung des Interface Auditable, Benutzung des Trait AuditableTrait, der die benötigte Funktionalität bereitstellt, die (identisch und wiederverwendet) in der Klasse AuditableEntity, von der hier nicht abgeleitet werden kann, existiert und schließlich wie oben gesehen die Verwendung der Methode setAuditValue() für die eigentliche Protokollierung der Daten. Diese zweite Variante mit Interface und Trait sollte man nach Möglichkeit immer anwenden, damit die Klasse immer noch von einer anderen abgeleitet werden kann. Durch Verwendung des Interface in Kombination mit dem Trait bleibt die fachliche Klassenhierarchie unbeeinflusst, was zu bevorzugen ist.

Die gezeigten Erweiterungen sind die einzig notwendigen Eingriffe in vorhandenen Code.

Schauen wir uns zunächst die Klasse AuditContext an, von der wie oben zu sehen bei jeder Protokollierung eine Instanz gespeichert wird:

class AuditContext
{
    public $method;
    public $line;
    public $time;

    public function __construct($method, $line, callable $time = null)
    {
        $this->time = is_callable($time) ? $time() : time();

        $this->method = $method;

        $this->line = $line;
    }
}

Damit werden die Stellen im Quellcode, an der ein einzelner Datensatz protokolliert wurde, sowie der zugehörige Zeitpunkt erfasst.

Die protokollierten Daten eines zusammenhängenden Vorgangs werden intern in einer Instanz der Klasse AuditData, die nachfolgend gezeigt wird, abgelegt. Es handelt sich dabei um einen Datencontainer, der die protokollierten Daten einer Instanz einer "auditierten" Klasse speichert.

Wenn also eine Klasse wie beschrieben "auditiert" wird, verwaltet eine Instanz davon intern eine zugehörige Instanz von AuditData, worin die Protokolldaten abgelegt werden:

class AuditData
{
    private $data = [];

    private $defaultEncoder;

    final public function __construct(callable $defaultEncoder = null)
    {
        if (false == is_callable($defaultEncoder)) {

            $defaultEncoder = function(array $data) {

                return json_encode($data);
            };
        }

        $this->defaultEncoder = $defaultEncoder;

        $this->init();
    }

    protected function init() { }

    final public function setValue($key, $value, AuditContext $context = null)
    {
        $this->data[$key] = ['value' => $value, 'context' => $context];

        return $this;
    }

    public static function createEmpty()
    {
        return new self();
    }

    public function isEmpty()
    {
        return count($this->data) == 0 ? true : false;
    }

    final public function getData($encoded = false, callable $encoder = null)
    {
        if (false == $encoded) {

            return $this->data;
        }

        if (false == is_callable($encoder)) {

            $encoder = $this->defaultEncoder;
        }
        
        return $encoder($this->data);
    }

    final public function getByKey($key)
    {
        if (!isset($this->data[$key])) {

            return false;
        }

        return $this->data[$key];
    }
}

Beim Wegschreiben der in Instanzen von AuditData protokollierten Daten wird auf deren Methode getData() zugegriffen, womit die Protokolldaten abgerufen werden. Die dabei optional verwendbare Kodierung der Daten richtet sich nach dem protokollierten Format und kann dem Container von außen in Form eines Callbacks gegeben werden. Damit kann der Container in die Lage versetzt werden, die Daten im jeweils benötigten Format aufbereitet auszuliefern. Das Wegschreiben der Daten werden wir weiter unten betrachten und sehen, wie dieser Mechanismus arbeitet.

Zunächst jedoch das Interface Auditable, das bei Verwendung des Trait direkt oder indirekt durch Ableitung von AuditableEntity nimplementiert wird:

interface Auditable
{
    public function attachAuditObserver(AuditObserver $observer);
    public function detachAuditObserver(AuditObserver $observer);
    public function notifyAuditObservers();
    public function isAuditObserverAttached();
    public function setAuditValue($key, $value, AuditContext $context = null);
    public function getAuditData();
}

Auditierte Klassen sind die Subjekte des hier angewandten Observer-Patterns, und können ihre Beobachter benachrichtigen. Die Protokollierung wird als Observer implementiert. Man kann es so umsetzen, dass die auditierten Klassen ihre Observer selbst benachrichtigen, wenn sie Daten protokolliert haben, die weggeschrieben werden sollen.

Auch kann man im Gegensatz dazu das Wegschreiben der Protokolldaten an beliebiger Stelle außerhalb zu einem definierten Zeitpunkt des Kontrollflusses auslösen. Als weitere Alternative dazu kann auch eine vollautomatische Benachrichtigung der Observer implementiert werden, was sich mithilfe eines Containers, der Referenzen auf die auditierten Subjekte hält und die Protokollierung gesammelt auslöst, realisieren lässt: Der Vorteil hiervon besteht darin, dass das Wegschreiben der Protokolldaten an keiner Stelle im Code explizit ausgelöst werden muss. Nachteilig kann es sein, dass der genaue Zeitpunkt des Wegschreibens nicht kontrollierbar ist, wenn dabei auf Vollautomatisierung gesetzt wird.

Durch die Verwendung des Observer-Patterns ist es möglich, die Protokollierung flexibel zu implementieren, ohne dass zu starre Vorgaben hierfür existieren, da eine Observer-Implementierung bei einer Nachricht selbst bestimmt, was passiert: Es gibt lediglich ein "Audit-Subjekt", das eine Nachricht ausgelöst hat. Außerdem können mehrere Observer-Instanzen oder Protokollierungsvarianten implementiert werden. Darüber hinaus werden unterschiedliche Benachrichtigungskanäle ermöglicht.

Das Interface Auditable enthält neben den Methoden eines Observer-Subjekts auch die Methoden eines "auditierten" Subjekts, beispielsweise, um die Audit-Daten auszulesen. Deshalb ist dieses Pattern eine Erweiterung des Observer-Patterns.

Hier die verwendete Implementierung eines "Audit-Subjekts" (siehe oben: class Customer extends AuditableEntity):
class AuditableEntity implements Auditable
{
    /**
     * @var AuditData
     */
    private $auditData;

    /**
     * @var bool
     */
    private static $autoAttachAuditObserver = false;

    /** 
     * @var AuditObserver[]
     */
    private $observers = [];

    /**
     * @var bool
     */
    private $isAuditObserverAttached = false;

    public static function setAutoAttachAuditObserver($bool)
    {
        self::$autoAttachAuditObserver = (bool)$bool;
    }

    public function isAuditObserverAttached()
    {
        return $this->isAuditObserverAttached;
    }

    public function attachAuditObserver(AuditObserver $observer)
    {
        if (in_array($observer, $this->observers)) {

            //already attached, nothing to do
            return $this;
        }

        $this->observers[] = $observer;

        $this->isAuditObserverAttached = true;

        return $this;
    }

    public function detachAuditObserver(AuditObserver $observer)
    {
        $this->observers = array_diff($this->observers, [$observer]);

        if (count($this->observers) == 0) {

            $this->isAuditObserverAttached = false;
        }

        return $this;
    }

    public function notifyAuditObservers()
    {
        foreach ($this->observers as $observer) {

            $observer->updateFromAuditable($this);
        }

        // AuditData neu und damit leer erstellen nach Benachrichtigung,
        // damit nichts doppelt protokolliert wird:
        $this->createEmptyAuditData();

        return $this;
    }

    final public function getAuditData()
    {
        if (false == $this->auditData instanceof AuditData) {

            $this->createEmptyAuditData();
        }

        return $this->auditData;
    }

    public function setAuditValue($key, $value, AuditContext $context = null)
    {
        $this->handleAutoAttachAuditObserver();

        $this->getAuditData()->setValue($key, $value, $context);

        return $this;
    }

    private function handleAutoAttachAuditObserver()
    {
        if (false == self::$autoAttachAuditObserver) {

            //nothing to do
            return;
        }

        if (false == $this->isAuditObserverAttached) {

            $this->attachAuditObserver(AuditManager::getInstance());
        }
    }

    private function createEmptyAuditData()
    {
        $this->auditData = AuditData::createEmpty();
    }
}

Damit hat eine auditierte Klasse, die hiervon abgeleitet wurde, sämtliche Observer- und Audit-Funktionalitäten zur Verfügung, und die interne Handhabung der Daten ist fertig nutzbar ausprogrammiert. Aus den protokollierten Klassen der vorhandenen Anwendung wird der Umgang mit den Audit-Daten herausgehalten, vom Vorgang des Protokollierens einmal abgesehen.

Für jene Klassen, die bereits eine Basisklasse haben, existiert, wie schon angedeutet, der Trait AuditableTrait:

trait AuditableTrait 
{
    /**
     * @var AuditableEntity
     */
   protected $auditableData;

   public function isAuditObserverAttached()
   {
        return $this->auditableEntity->isAuditObserverAttached();
   }
   
   public function attachAuditObserver(AuditObserver $observer)
    {
        $this->ensureAuditDataEntityExists();

        $this->auditableEntity->attachAuditObserver($observer);

        return $this;
    }

    public function detachAuditObserver(AuditObserver $observer)
    {
        $this->ensureAuditDataEntityExists();

        $this->auditableEntity->detachAuditObserver($observer);

        return $this;
    }

    public function notifyAuditObservers()
    {
        $this->ensureAuditDataEntityExists();

        $this->auditableEntity->notifyAuditObservers();

        return $this;
    }

    public function getAuditData()
    {
        $this->ensureAuditDataEntityExists();

        return $this->auditableEntity->getAuditData();
    }

    public function setAuditValue($key, $value, AuditContext $context = null)
    {
        $this->ensureAuditDataEntityExists();

        $this->auditableEntity->setAuditValue($key, $value, $context);

        return $this;
    }

    private function ensureAuditDataEntityExists()
    {
        if (is_object($this->auditableEntity)) {

            //nothing to do
            return;
        }

        $this->auditableEntity = new AuditableEntity();
    }
}

Als Beispiel für die Verwendung des Traits haben wir die Klasse CCPayment weiter oben gesehen. Der Trait delegiert lediglich an eine Instanz von AuditableEntity. AuditableEntity hat damit die alleinige Verantwortung für den Umgang mit den Audit-Daten (Single Responsibility, SOLID).

Was jetzt noch fehlt ist die Protokollierung auf technischer Ebene, also das Wegschreiben der erfassten Protokolldaten. Es ist kein Zufall, dass wir hiervon bis jetzt noch nichts gesehen haben, denn dies ist durch das Konzept vollkommen offen gehalten, so dass die Protokollierung frei implementierbar ist, also den Bedürfnissen und der Systemumgebung individuell angepasst werden kann.

Analog zum Interface Auditable, das ja die Schnittstelle eines "Audit-Subjekts" darstellt, gibt es das Interface AuditObserver:

interface AuditObserver
{
    public function updateFromAuditable(Auditable $subject);
}

Eine Klasse, die dieses Interface implementiert, kann Nachrichten eines auditierten Subjekts empfangen und darauf reagieren. Im Wesentlichen wird man hiermit das Wegschreiben der Protokolldaten implementieren, aber natürlich ist mit dem Konzept mehr möglich: So kann man Ereignisse filtern, in bestimmten Fällen Benachrichtigungen verschicken und ähnliche Zusatzfunktionalitäten einbinden.

Die Ausgestaltung der Protokollierung hängt von den Anforderungen und von der Systemumgebung ab. Deshalb nachfolgend lediglich eine angedeutete Implementierung, die sich auf Ausgaben mit var_dump() beschränkt:

class AuditManager implements AuditObserver
{
    protected static $instance;

    final protected function __construct() { }

    final protected function __clone() { }

    public static function getInstance()
    {
        if (false == self::$instance instanceof AuditManager) {

            self::$instance = new self();
        }

        return self::$instance;
    }

    public function updateFromAuditable(Auditable $subject)
    {
        if ($subject->getAuditData()->isEmpty()) {

            // AuditData ohne Daten nicht protokollieren
            return;
        }

        // wegschreiben der Audit-Daten HIER implementieren
        var_dump($subject->getAuditData()->getData(true));

        // ^^^ getData() gibt ein assoziatives Array zurück: Zu jedem Schlüsselwert enthält das Array
        // den zugehörigen Wert ('value') und Kontext ('context'), in dem protokolliert wurde.
    }
}

Tatsächlich würde man hier die Daten in einem Filesystem, in einer Datenbank oder in einem dokumentenbasierten System ablegen und an dieser Stelle die fehlerfreie Speicherung der Daten sicherstellen. Es sind dabei besondere Aufmerksamkeit und Sorgfalt gefragt, sowie die Fähigkeit der Wiederherstellbarkeit von Protokolldaten im Fehlerfall.

Mit anderen Worten: Wenn Daten beispielsweise nicht auf eine vollgelaufene Festplattenpartition oder nicht in ein unerwartet nicht erreichbares NFS geschrieben werden können, muss darüber benachrichtigt werden, die Daten dürfen nicht verloren gehen, und sie müssen zu einem späteren Zeitpunkt, wenn die Störung behoben ist, automatisch "nachgezogen" werden, ohne dass dies in den erfassten Daten einen Unterschied macht. Die einfachste und zuverlässigste Lösung für dieses Problem ist, für die Speicherung der Daten einen ausreichend getesteten Hochverfügbarkeitsansatz zu nutzen.

Wie und wann wird aber die physikalische Protokollierung der Daten ausgelöst? Zunächst einmal existieren irgendwo im System Instanzen der regulären Klassen des "auditierten" Systems. Um bei den verwendeten Beispielklassen zu bleiben:

// Instanz einer bestehenden Klasse, die (lediglich) zusätzlich von AuditableEntity abgeleitet
// wurde und darüber Daten protokolliert. Die Klasse "Customer" gibt es in einer bestehenden
// Lösung bereits. Sie wurde lediglich abgeleitet und um Aufrufe von setAuditValue() ergänzt.
// Hier beispielhaft instanziiert:
$customer = new Customer(uniqid());

// Instanz einer bestehenden Klasse, die zusätzlich das Interface Auditable implementiert und
// die Methoden an einen Helper delegiert. Diese Klasse gibt es ebenfalls irgendwo und wird hier
// auch beispielhaft instanziiert:
$payment = new CCPayment();

Man kann nun entweder manuell eine oder beliebig viele Implementierungen des Interface AuditObserver als Beobachter bei einem auditierten Subjekt registrieren. Hiermit wird das Schreiben der Daten aktiviert. Man kann sich leicht vorstellen, dass der "Ausgabekanal", über den protokolliert wird, austauschbar ist, dass außerdem mehrere Ausgabekanäle verwendet werden können, ebenso wie eine Testimplementierung möglich ist, welche die stattfindende Protokollierung darauf prüft, ob sie sich verhält wie erwartet (Unit Test).

Die Registrierung von Observern sollte nach Möglichkeit an einer zentralen Stelle in der Anwendung passieren:

// Auditierung des Customer aktivieren (registriert AuditManager als Observer):
$customer->attachAuditObserver(AuditManager::getInstance());

// Auditierung von CCPayment  aktivieren (registriert AuditManager als Observer):
$payment->attachAuditObserver(AuditManager::getInstance());
$payment->attachAuditObserver(AuditManager::getInstance());
// ^^^ attachAuditObserver() hat beim zweiten Aufruf keinen Effekt bei setAutoAttachAuditObserver(true),
// da dieselbe Instanz nicht zweimal beim selben Objekt registriert wird. Das zur Demonstration (siehe unten).

Man muss nicht zwingend eine Instanz von AuditManager verwenden. Jede Implementierung des Interface AuditObserver funktioniert.

Alternativ lässt sich auch automatisch die Singleton-Instanz von AuditManager als Observer registrieren, sobald eine auditierte Entität etwas protokolliert:

AuditableEntity::setAutoAttachAuditObserver(true);

Dadurch muss man sich nicht mehr um das Registrieren des Observers bei den Subjekten kümmern, heißt, die manuellen Aufrufe von attachAuditObserver() entfallen. Man kann die Lösung anpassen, falls man nicht automatisch die Singleton-Instanz von AuditManager verwenden möchte und sollte die verwendete Implementierung konfigurierbar machen. Implementiert man dabei mithilfe des Decorator-Patterns einen Container von Observern, erhält man die Möglichkeit, mehrere Observer gleichzeitig zu registrieren.

Was nun noch fehlt, ist die Benachrichtigung der Observer, so dass ein AuditObserver die Daten protokolliert.

Das kann man wiederum entweder manuell lösen zum jeweils optimalen Zeitpunkt:

// CCPayment jetzt mit allen auditierten Daten informieren:
$payment->notifyAuditObservers();

// Dieser doppelte Aufruf demonstriert, dass Audit-Daten nicht doppelt protokolliert werden:
$payment->notifyAuditObservers();

// Customer jetzt mit allen auditierten Daten informieren:
$customer->notifyAuditObservers();

Durch die beispielhaft verwendete Funktion var_dump() passiert folgende Ausgabe:

string(168) "{"register_data":{"value":{"id":"56fe547c5ce3e","name":"John Snow'","email":"j.doe@example.com"},
"context":{"method":"Customer::register","line":210,"time":1459508348}}}"
string(164) "{"payment_data":{"value":{"customer_id":"56fe547c5ce3e","credit_card_number":"XXX"},
"context":{"method":"CCPayment::executePayment","line":261,"time":1459508348}}}"

Oder, wenn man die Benachrichtigung der Observer dagegen nicht manuell regeln möchte, was potenziell fehlerträchtig ist (falscher Zeitpunkt, vergessene Aufrufe), bietet es sich an, die relevanten Subjekte in einem Container zu sammeln und die Protokollierung einmalig zu feuern.

Dies ein Sammelcontainer für auditierte Objekte, wenn man alle Benachrichtigungen gemeinsam feuern möchte:

class AuditNotificationContainer
{
    protected $instances = [];

    public function addInstance(Auditable $instance)
    {
        $this->instances[] = $instance;

        return $this;
    }

    public function notifyAll()
    {
        foreach ($this->instances as $instance) {

            $instance->notifyAuditObservers();
        }

        return $this;
    }
}

Schlussbetrachtung

Zu lösen bleibt bei Verwendung eines solchen Containers und damit generischen Mechanismus, wie man die auditierten Subjekte möglichst komfortabel und automatisiert zum Container hinzufügt und wie, beziehungsweise zu welchem Zeitpunkt man die Protokollierung der gesammelten Daten feuert. Von Interesse ist hier auch die  Reihenfolge der Protokollierung, die genau der Reihenfolge der Ausführung entsprechen muss.

Analog zur Implementierung eines AuditObserver ist an dieser Stelle erhöhte Achtsamkeit bezüglich der Robustheit des Benachrichtigungsmechanismus vonnöten, damit die zuverlässige Ausführung der Protokollierung gewährleistet ist.

Die zuletzt beschriebene Herausforderung möchte ich damit offen lassen, so dass dem interessierten Leser noch Raum für eigene Ideen bleibt.

Zurück

Hands On PHP

Harter Hut

Programmiertes - Jenseits von Prosa.

Zend Certified Engineer
Zend Certified Engineer ZF
Oracle Certified Professional, MySQL 5.6 Developer
Sun Certified Java Programmer (SCJP)
Sun Certified Web Component Developer (SCWCD)
RSS
tl_files/open_clip_art/symbole/rss-icon.png  Abonnieren