Zend und dynamische Felder für Models

by mlaug on 27. Juli 2010

Immer wieder habe ich das Problem, dass einem Model eine neue Eigenschaft bzw. ein neues Attribut hinzugefügt werden soll. Tabellen, die vorher fein spezifiziert wurden laufen so meist über. Schlimm wird es vorallem, wenn bestimmte Attribute irgendwann wieder obsolet sind. Das Resultat ist zumeist ein großer Haufen Datenmüll, mit dem andere meist wenig anfangen können. Um diesem Problem entgegenzuwirken habe ich mich von der Verwaltung von Option in WordPress inspirieren lassen. Ich empfand es schon immer als sehr beeindruckend mit was für einer einfachen Datenbank Struktur WordPress aufwartet  und dabei so flexibel ist. Dieses Verfahren habe ich daher für Zend im Prototyp implementiert.

Schritt 1: Erstellen des Model

Das Model selber ist sehr primitiv, benötigt es doch nur eine get() und eine set() Methode. Da ich ich diese Option Klasse später an alle meine Models hängen möchte (und diese vielleicht options mit dem selben Namen verwenden), füge ich noch eine Variable $hash hinzu.

/**
 * this stores options for any model or controller
 * @author mlaug
 */
class Default_Option {

    /**
     * hash to identify any options
     * @var string
     */
    protected $_hash = null;

    /**
     * store any protected options in here
     * @var array
     */
    protected $_protected = array();

    /**
     * make use of cache
     * @var Zend_Cache_Backend_Memcached
     */
    protected $_cache = null;

    /**
     * @var Default_Option_Table
     */
    protected $_table = null;

    public function __construct(){
        $cache = Zend_Registry::get('cache');
        $this->_cache = $cache;
    }

    /**
     * set the hash to identify any options
     * @author mlaug
     * @param string $hash
     * @return boolean
     */
    public function setHash($hash){
        if ( is_string($hash) ){
            $this->_hash = $hash;
            return true;
        }
        return false;
    }

    /**
     * get an option field
     * @author mlaug
     * @param string $option
     * @return mixed
     * @todo implement caching
     */
    public function get($option){
        $value = $this->getTable()
                      ->get($this->_hash, $option)
                      ->optionValue;
        return $this->maybeUnserialize($value);
    }

    /**
     * @author mlaug
     * @param mixed $option
     * @param mixed $value
     * @return boolean
     * @todo: implement caching
     */
    public function set($option,$value){
        try{

            if ( !is_string($option) ){
                return false;
            }

            //check if this is an protected option
            $this->protectSpecialOptions($option);
            //maybe we need to serialize this data
            $value = $this->maybeSerialize($value);
            //store value (add or update)
            $this->getTable()
                 ->set($this->_hash, $option, $value);

            return true;
        }
        catch ( Exception $e ){
            return false;
        }

    }

    /**
     * @author mlaug
     * @return Default_Option_Table
     */
    private function getTable(){
        if ( is_null($this->_table) ){
            $this->_table = new Default_Option_Table();
        }
        return $this->_table;
    }

    /**
     * get a serialized string if needed
     * @author mlaug
     * @param mixed $data
     * @return string
     */
    private function maybeSerialize($data) {
        if ( is_array( $data ) || is_object( $data ) ){
            return serialize( $data );
        }

        if ( is_serialized( $data ) ){
            return serialize( $data );
        }

        return $data;
    }

    /**
     * return unserialized string if needed, otherwise
     * just return the original
     * @author mlaug
     * @param string $original
     * @return mixed
     */
    private function maybeUnserialize($original) {
        if ( is_serialized( $original ) ){
            return @unserialize( $original );
        }
        return $original;
    }

    /**
     * @author mlaug
     * @param string $option
     */
    private function protectSpecialOptions($option){
        if ( in_array( $option, $this->_protected ) ){
            throw new Exception('Cannot set protected option');
        }
    }

}

Schritt 2: Erstellen der Tabelle

Die Tabelle speichert neben dem Hash, der zur eindeutigen Identifizierung der Zugehörigkeit benötigt wird, auch den Wert optionValue und den Bezeichner optionName. Als Unique habe ich hier hash und optionName gesetzt, damit eher eine Exception geworfen wird, als das diese beiden Werte doppelt vorkommen. Diese würde ansonsten eine eindeutige Zuordnung zum Model später nicht ermöglichen.

CREATE TABLE `options` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `hash` varchar(255) NOT NULL,
  `optionName` varchar(255) NOT NULL,
  `optionValue` text Default NULL,
  `created` timestamp default current_timestamp,
  UNIQUE(`hash`,`optionName`),
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

Schritt 3: Erstellen der Zend_Db Schnittstelle

/**
 * Table to store options
 *
 * @author mlaug
 */
class Default_Option_Table extends Zend_Db_Table_Abstract {
     /**
     * name of the table
     * @param string
     */
    protected $_name = 'options';

    /**
     * primary key
     * @param string
     */
    protected $_primary = 'id';

    /**
     * @author mlaug
     * @param string $hash
     * @param string $option
     * @return  Zend_Db_Table_Row
     */
    public function get($hash,$option){
        $select = $this->select()
                       ->where('hash=?',$hash)
                       ->where('optionName=?',$option);
        $row = $this->fetchRow($select);
        if ( $row instanceof Zend_Db_Table_Row_Abstract ){
            return $row;
        }
        return null;
    }

    /**
     * add/update an option
     * @author mlaug
     * @param string $hash
     * @param string $option
     * @param mixed $value
     * @return int
     */
    public function set($hash,$option,$value){
        //get old one if available
        $old = $this->get($hash,$option);
        if ( $old instanceof Zend_Db_Table_Row_Abstract ){
            $row = $old;
        }
        else{
            $row = $this->createRow();
        }
        $row->hash = $hash;
        $row->optionName = $option;
        $row->optionValue = $value;
        return $row->save();
    }

}

Diese beiden Klassen lassen sich relativ einfach erklären.

  1. setHash: Hier wird ein einzigartiger String übergeben. In meinen Models verwende ich hier folgende Kombination, wobei getId mir die aktuelle Id des Datensatzes zurück gibt. Diese ist in Zusammenhang mit dem Klassennamen einzigartig (Primary Key)
    md5($this->getId() . __CLASS__)
  2. Die Methoden get und set setzen eine Option in der Datenbank. Das Speichern in der Datenbank in Zusammenhang mit der Tabelle ist denke ich selbsterklärend. Hierbei wird gerade bei der set Funtion darauf geachtet, dass man den Datensatz entweder erstellt oder, falls vorhanden, aktualisiert. Die Funktion get ist also im Model für das Erstellen und das Aktualisieren einer Option zuständig.
  3. Wichtig sind noch die Funktionen, die ich von WordPress “geklaut” habe. Diese überprüfen den übergebenen Wert und serialisieren ihn, wenn nötig. Bei einem get Aufruf wird das ganze wieder rückgängig gemacht, so dass man immer den gesetzten Wert zurück bekommt

Der ganze Code steht in diesem Zip Verzeichnis zum Download zur Verfügung: Archiv

Leave a Comment