Im vorherigen Beitrag Zend: TableGateway und das RowGatewayFeature haben wir mittels TableGateway eine einfache Datenbank Abfrage durchgeführt. In einem zweiten Schritt haben wir das RowGatewayFeature hinzu genommen, damit wir die Save und Delete Operationen auf den Objekten anwenden können.

Durch die Verwendung des RowGatewayFeatures haben wir zwar nun Objekte, welche eine Entity repräsentieren und sich um ihre Persistenz kümmern können, jedoch keine fachliche Logik besitzen können und demnach auch keine Entität, im eigentlichen Sinne, darstellen.
In diesem Beitrag versuchen wir, die Vorteile des RowGateways (Persistenz) mit denen einer Entity (fachliche Logik) zu verbinden. Das Schlüsselwort hierbei ist Hydration.

Schauen wir uns folgenden Ausschnitt einmal genau an:

use Zend\Db\ResultSet\HydratingResultSet;
use Zend\Stdlib\Hydrator\Reflection;

$output = array();
$adapter = $this->getServiceLocator()->get('Zend\Db\Adapter\Adapter');
$customerTable = new TableGateway("customer", $adapter, new RowGatewayFeature('id'));
$rows = $customerTable->select();

$customers = new HydratingResultSet(new Reflection(), new \Customer\Entity\Customer());
$customers->initialize($rows->toArray());

Als erstes fällt uns wahrscheinlich die HydratingResultSet und die Reflection Klasse auf. Diese stammen aus dem Namespace Zend\Db\ResultSet\HydratingResultSet sowie Zend\Stdlib\Hydrator\Reflection.
Die Klasse \Customer\Entity\Customer ist eine von uns selbst angelegte Klasse im Verzeichnis \module\CRM\Customer\Entity\Customer.php.
Hier eine kleine abgespeckte Customer Klasse, die uns als Entity dient:

namespace Customer\Entity;

class Customer {
    
    public $id;
    public $name;
    public $city;

    [...]
}

Was passiert nun? Unser Result ($rows) welches wir aus dem TableGateway zurück erhalten ist ein RowGateway Objekt, welches sich um seine eigene Persistenz kümmern kann.
Allerdings hat dieses Objekt keine fachliche Logik, ist also keine Entität. Wir möchten zur weiteren Verwendung von Customers in unserer Anwendung eine eigene Customer Klasse besitzen, welche wir mit weiteren Attributen und Methoden ausstatten können (sprich: die fachliche Logik).

Genau dabei kommt das Hydrating ins Spiel. Die HydratingResultSet Klasse bekommt bei der Initialisierung unter anderem eine Instanz unserer Entity (Customer Klasse) übergeben. (Interessanter Hinweis! Die Customer Instanz wird intern von der HydratingResultSet Klasse geklont!).
Die Art und Weise wie die Zuordnung der Daten aus dem RowGateway in die Entity übernommen werden, bestimmt der erste Parameter – in diesem Fall via Reflection.

Nun haben wir den Vorteil, unsere Daten in einer Entität zu haben, allerdings haben wir bei diesem Schritt die Vorteile des RowGateways verloren (nämlich seine Persistenz Funktionalität!).
Wie können wir nun beides miteinander verbinden?

Wir benötigen zuerst einen sogenannten Mapper. Diesen legen wir im Namespace Customer\Mapper (also im Verzeichnis module\CRM\Customer\Mapper\Customer.php) an. Diese Mapper Klasse leiten wir vom TableGateway ab. (Wer aufmerksam liest, erkennt das man sich hierfür auch eine clevere Basis Klasse bauen könnte).

Unser Klasse sieht nun wie folgt aus:

namespace Customer\Mapper;

use Zend\Db\TableGateway\TableGateway;
use Zend\Db\TableGateway\Feature\RowGatewayFeature;
use Zend\Stdlib\Hydrator\Reflection;
use Zend\Db\ResultSet\HydratingResultSet;

class Customer extends TableGateway {

    protected $tableName = "customer";
    protected $idCol = "id";
    protected $entityPrototype = null;
    protected $hydrator;
    
    public function __construct($adapter) {
        parent::__construct($this->tableName, $adapter, new RowGatewayFeature($this->idCol));
        
        $this->entityPrototype = new \Customer\Entity\Customer();
        $this->hydrator = new Reflection();
    }
    
    public function hydrate($rows) {
        $customers = new HydratingResultSet($this->hydrator, $this->entityPrototype);
        return $customers->initialize($rows->toArray());
    }
    
    public function getAll() {
        return $this->hydrate($this->select());
    }
    
    public function insert($customer) {
        return parent::insert($this->hydrator->extract($customer));
    }
    
    public function updateEntity($customer) {
        return parent::update($this->hydrator->extract($customer), $this->idCol ."=". $customer->id);
    }
}

Tipp: Wenn die Tabellenspalten und die Attribute der Entity Klasse nicht übereinstimmen, muss hier ein eigener Hydrator eingesetzt werden.

Nun können wir den Code im Controller in folgendes Ändern:

// Mapper erstellen (besser über den ServiceManager)
$customerTable = new \Customer\Mapper\Customer($adapter);
$customers = $customerTable->getAll(); // Alle Customers holen

$customer = $customers->current();
// $customer ist nun eine Entität mit fachlicher Logik
$customer->city = "Hamburg";
// Hier kommt der Vorteil der Persistenz ins Spiel...
$customerTable->updateEntity($customer);