bitWorking

Zend Framework abstract Model


 27. Juli 2012  PHP  


Da das Zend Framework von Hause aus keine Modelklasse mitliefert, gibt es viele Diskussionen im Netz welche die beste Herangehensweise ist, ein Model/Mapper zu erstellen.
Ein Ansatz den ich gesehen habe, ist den Mapper direkt von der Zend_Db_Table_Abstract Klasse abzuleiten und das Model von der Klasse Zend_Db_Table_Row_Abstract. Die Modelklasse kann dann als rowClass im Mapper angeben werden. Ein Beispiel findet sich in dieser Diskussion: http://stackoverflow.com/questions/294875/models-in-the-zend-framework
Dort steht auch warum diese doch sehr verlockende Möglichkeit nicht besonders gut ist, nämlich weil ein Model nicht auf eine Tabelle beschränkt sein soll, sondern theoretisch auch mehrere Tabellen oder zusätzliche Felder beihalten könnte.

In der Zend Framework Dokumentation wird vorgeschlagen ein Model aus Gettern und Settern zu erstellen und einen Mapper, der mit einem vom Zend_Db_Table_Abstract abgeleiteten Objekt „arbeitet“. Diesen Ansatz finde ich auch am saubersten, da man so ein Model für die reine Datenhaltung hat und den Mapper austauschen könnte, falls man die Persistenz nicht über eine Datenbank sondern z.B. über einen Webservice oder das Dateisystem regeln möchte. Siehe Zend Doku: http://framework.zend.com/manual/de/learning.quickstart.create-model.html

Ich möchte euch nun meinen Ansatz zeigen, der im Wesentlichen auf dem Vorschlag von Zend aufbaut, aber zusätzlich noch Validatoren, Filter und Datentypen mit einbezieht und eine abstrakte Modelklasse bereitstellt.

So nun wollen wir aber auch mal was Code sehen.


abstract class BitWorking_Abstract_Model
{
    protected $_fields = array();
    protected $_fieldValues = array();

    protected abstract function _init();

    public function __construct(array $values = null)
    {
        $this->_init();

        if (is_array($values)) {
            $this->setValues($values);
        }
    }

    public function __set($name, $value)
    {
        if (array_key_exists($name, $this->_fields)) {

            $type = $this->_fields[$name]->getType();

            if (null !== $value) {

                // type casting
                if ($type === BitWorking_ModelFieldType::TYPE_INT) {
                    $value = (int) $value;
                }
                else if ($type === BitWorking_ModelFieldType::TYPE_FLOAT) {
                    $value = (float) $value;
                }
                else if ($type === BitWorking_ModelFieldType::TYPE_STRING) {
                    $value = (string) $value;
                }
                else if ($type === BitWorking_ModelFieldType::TYPE_BOOL) {
                    $value = (bool) $value;
                }
            }

            $this->_fieldValues[$name] = $value;
        }
    }

    public function setValues(array $values)
    {
        foreach ($values as $key => $value) {
            $this->$key = $value;
        }
        return $this;
    }

    public function __get($name)
    {
        if (array_key_exists($name, $this->_fieldValues)) {
            return $this->_fieldValues[$name];
        }
        return null;
    }

    public function getData()
    {
        $data = array();
        $fields = $this->getFieldKeys();
        foreach ($fields as $key) {
            if (array_key_exists($key, $this->_fieldValues)) {
                $data[$key] = $this->_fieldValues[$key];
            }
            else {
                $data[$key] = null;
            }
        }
        return $data;
    }

    public function validate()
    {
        $validators = array();
        $filters = array();

        $data = $this->getData();

        foreach ($this->_fields as $key => $value) {
            $validators[$key] = $value->getValidators();
            $filters[$key] = $value->getFilters();
        }

        $input = new Zend_Filter_Input($filters, $validators, $data);
        return new BitWorking_ValidateResult($input, $this);
    }

}

Die Eigenschaften können über die magischen __get/__set Methoden gelesen/geschrieben werden.
Das einzig blöde daran ist, dass der Editor kein Autocomplete anzeigen kann.
Im Setter findet der Typecast statt, so das die Werte immer dem richtigen Datentyp entsprechen.
Der $_fields Array muss in der _init() Methode der ableitenden Klasse gefüllt werden. Er besteht aus
ModelField-Objekten in denen der Datentyp, die Validatoren und Filter gespeichert sind.


class BitWorking_ModelField
{
    protected $_type;
    protected $_validators = array();
    protected $_filters = array();

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

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

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

    public function __construct($type, $validators = array(), $filters = array())
    {
        $this->_type = $type;
        $this->_validators = $validators;
        $this->_filters = $filters;
    }

    public static function create($type, $validators = array(), $filters = array())
    {
        $modelField = new BitWorking_ModelField($type, $validators, $filters);
        return $modelField;
    }

}

Als letztes bleibt noch die „BitWorking_ValidateResult“ Klasse. Hier wird die isValid Methode vom Zend_Filter_Input aufgerufen, bei Erfolg die gefilterten Werte ins Model geschrieben und die Fehlernachrichten bereitgestellt.


class BitWorking_ValidateResult
{
    protected $_isValid;
    protected $_filterInput;
    protected $_model;

    public function __construct(Zend_Filter_Input $filterInput, BitWorking_Abstract_Model $model)
    {
        $this->_filterInput = $filterInput;
        $this->_isValid = $filterInput->isValid();
        $this->_model = $model;

        if ($this->_isValid) {
            $this->_setData();
        }
    }

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

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

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

    protected function _setData()
    {
        $this->_model->setValues($this->_filterInput->getEscaped());
    }

}

Ein Beispiel einer Modelklasse sieht dann so aus:


class BitWorking_Model_Test extends BitWorking_Abstract_Model
{
    protected function _init()
    {
        $this->_fields = array(
            'id' => BitWorking_ModelField::create(
                BitWorking_ModelFieldType::TYPE_INT,
                array(
                    new Zend_Validate_Int(),
                    'allowEmpty' => true
                )
            ),
            'name' => BitWorking_ModelField::create(
                BitWorking_ModelFieldType::TYPE_STRING,
                array(
                    new Zend_Validate_StringLength(array('min' => 0, 'max' => 255)),
                    'presence' => 'required'
                ),
                array(
                    new Zend_Filter_StringTrim(),
                    new Zend_Filter_StripTags()
                )
            ),

        );

    }

}

// Model benutzen

$test = new BitWorking_Model_Test();
$test->id = 1;
$test->name = 'Stefan';

$validateResult = $test->validate();

if ($validateResult->isValid()) {
    echo 'Alle Werte sind gültig. Model kann in die Datenbank geschrieben werden';
}
else {
    Zend_Debug::dump($validateResult->getMessages());
}

Im nächsten Beitrag möchte ich euch meinen Mapper vorstellen, der mit diesem Model zusammenarbeitet und die meisten Anwendungsfälle abdeckt. Dadurch muss nicht für jedes Model ein eigener Mapper entwickelt werden. Darüber hinaus habe ich eine Formklasse abgeleitet von Zend_Form entwickelt, die mehrere Models entgegennehmen kann und die Formularfelder dynamisch um die Validatoren und Filter aus den Models erweitert.

Schreibt mir doch bitte, was ihr von meinem Ansatz haltet. Ich bin auf eure Meinungen gespannt.



Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.