Ankündigung

Einklappen
Keine Ankündigung bisher.

Tutorial: PHP/MySQL und OOP

Einklappen
Dieses Thema ist geschlossen.
X
X
  • Filter
  • Zeit
  • Anzeigen
Alles löschen
neue Beiträge

  • Tutorial: PHP/MySQL und OOP

    Hallo,
    ich möchte euch als Tutorial - oder für den Fortgeschritteneren Anregungen - vorstellen, wie bei mir Datensätze aus der Datenbank bei PHP landen. Ich verwende dabei für eine Datenbank-Tabelle letztlich drei PHP-Klassen. Dies wären einmal die Manager-, die RecordSet- und die Record-Klasse.
    Mein Tutorial möchte ich daher auch in drei Teile einteilen ..
    • Record-Klasse
    • RecordSet-Klasse
    • Manager-Klasse


    .. in dem ich heute zunächst auf die Record-Klasse eingehe und konkret als Beispiel eine „User“-Klasse verwende. Das Tutorial wird für die weiteren Klassen später erweitert.


    Record-Klasse:

    Die Record-Klasse dient als einfacher Datenhalter, das heißt sie hat keine Verbindung zur Datenbank, zu Formularen oder weiß sonst sehr viel von ihrer Umwelt. Sie beschäftigt sich lediglich mit den ihr zugedachten Werten. Sie hat daher zunächst folgende Eigenschaft und folgende fünf relevanten Methoden:
    Code:
    private  $_properties = array()
    public function import(array $properties, $merge = false)
    public function export() 
    public function getId()
    protected function _hasProperty($property, $checkNotNull = false)
    protected function _getProperty($property)
    protected function _setProperty($property, $value)
    Diese Methoden sind in einer abstrakten Basisklasse definiert („Record_Abstract“), von der die konkreten Klassen (z.B. „User_Record“) erben.
    Diese Methoden verwalten letztlich nur die Eigenschaft $_properties, welche ein Array mit den (User-)Daten ist. In diesem assoziativen Array sind die Schlüssel-/Werte-Paare untergebracht, um die sich die Record-Klasse kümmert. Definieren wir die abstrakte Klasse ganz konkret, als Klassen-Prefix verwende ich mein Kürzel „Anti“ :

    PHP-Code:
    <?php
    class Anti_Record_Abstract
    {
        private 
    $_properties = array();

        
    /**
         * @param array $properties
         * @param bool $merge [optional]
         * @return null
         */
        
    public function import(array $properties$merge false)
        {
            
    $this->_properties $merge
                               
    array_merge($this->_properties$properties)
                               : 
    $properties;
        } 

        
    /**
         * @return array $properties
         */
        
    public function export()
        {
            return 
    $this->_properties;
        }

        
    /**
         * @return int|null
         */
        
    public function getId()
        {
            return 
    $this->_getProperty("id");
        }

        
    /**
         * @param string $property
         * @param bool $checkNotNull
         * @return bool
         */
        
    protected function _hasProperty($property$checkNotNull false)
        {
            return 
    array_key_exists($property$this->_properties)
                && (!
    $checkNotNull || null !== $this->_properties[$property]);
        }

        
    /**
         * @param string $property
         * @return mixed|null
         */
        
    protected function _getProperty($property)
        {
            return 
    array_key_exists($property$this->_properties)
                 ? 
    $this->_properties[$property]
                 : 
    null;
        }

        
    /**
         * @param string $property
         * @param mixed $value
         * @return null
         */
        
    protected function _setProperty($property$value)
        {
            
    $this->_properties[$property] = $value;
        }
    }
    ?>
    Die import() Methode erlaubt das Importieren von beliebigen Eigenschaften. (Da dies sicherlich ein berechtigter Kritikpunkt ist, werde ich am Ende des Artikels genauer darauf eingehen.) Als zweiten Parameter kann das Zusammenfügen vorhandener Eigenschaften mit den übergebenen Eigenschaften erzwungen werden, beispielsweise um geänderte (z.B. Formular-)Daten mit den aktuellen Eigenschaften zu verschmelzen.
    Die export() Methode ermöglicht es der Aussenwelt, die eigenen Eigenschaften zur Verfügung zu stellen.

    Wenn wir wieder Bezug zu unserem User-Beispiel nehmen, würde unsere Klasse vielleicht so aussehen:

    PHP-Code:
    <?php
    /**
     * @author Christian Reinecke <reinecke@bajoodoo.com>
     * @since 2009-04-17
     */
    class Anti_Record_User extends Anti_Record_Abstract
    {
        
    /**
         * @return string
         */
        
    public function getFirstname()
        {
            return 
    $this->_getProperty("firstname");
        }

        
    /**
         * @return string
         */
        
    public function getLastname()
        {
            return 
    $this->_getProperty("lastname");
        } 

        
    /**
         * @param string $phone
         * @return null
         */
        
    public function setTelephone($phone)
        {
            if (
    preg_match("/^\+(\([0-9]+\))(.+)$/"$phone$match)) {
                
    $this->_setProperty("telephone_country"trim($match[1], "()")); // (\([0-9]+\))
                
    $this->_setProperty("telephone",         "0" $match[2]);       // (.+)
            
    } else {
                
    $this->_setProperty("telephone"$phone);
            }
        }

        
    /**
         * @return string
         */
        
    public function getTelephone()
        {
            
    $telephone $this->_getProperty("telephone");
            if (
    $this->_hasProperty("telephone_country")) {
                
    // add country's telephone code and cut off leading 0 from $telephone
                
    $telephone  "+(" $this->_getProperty("telephone_country")
                            . 
    ")" mb_substr($telephone1);
            }
            return 
    $telephone;
        }

        
    /**
         * @see http://php.net/strtotime
         * @param string $when date time value for strtotime()
         * @return int age
         */
        
    public function getAge($when "today")
        {
            
    $birthday      $this->_getProperty("birthday");
            
    $birthdayStamp strtotime($birthday);
            
    $birthdayStamp date("Ymd"$birthdayStamp);

            
    $whenStamp strtotime($when);
            
    $whenStamp date("Ymd"$whenStamp);

            
    $age floor(($whenStamp $birthdayStamp) / 10000);

            return (int)
    $age;
        }
    }
    ?>
    Wir implementieren die Methoden, die wir öffentlich (public) den Anwendern des Record-Objektes zur Verfügung stellen möchten. Es mag sicherlich eine Reihe Methoden geben, die den Wert von _getProperty($property) einfach nur „durchreichen“, andere wie getAge() sollen aber zeigen, dass auch hier auch mehr möglich ist.

    Denkbar wäre nun eine Anwendung in diesem Stil:

    PHP-Code:
    <?php
    $userData 
    = array(
        
    "firstname" => "Max",
        
    "lastname"  => "Müller",
        
    "telephone" => "+(49)721 12345",
        
    "birthday"  => "1982-09-05");

    $user = new Anti_User_Record();
    $user->import($userData);

    echo 
    $user->getFirstname(), " "$user->getLastname(), ", Telefon: ",
         
    $user->getTelephone(), ", Alter: "$user->getAge();
    ?>
    Code:
    Ausgabe:
    Max Müller, Telefon: +(49)721 12345, Alter: 26
    Klar sein muss, die Record-Klasse, bzw. die Anti_Record_User() Klasse kennt weder eine Datenbank, noch Formulare oder ein Ausgabeformat. In dieser Klasse sollte also niemals HTML- oder SQL-Code zu finden sein, genausowenig wie Vor-Formatierungen dafür (htmlentities()) oder Datums-Formatierungen, denn diese Klasse kümmert sich ausschliesslich um das (fast) neutrale Halten (d.h. Setzen/Set und Wiedergeben/Get) der Daten. Diesen Punkt einzusehen ist wichtig, da die Wiederverwendbarkeit und Flexibilität sonst sofort verloren geht.

    Sicherlich ist in dieser Klasse auch eine Validierung von Benutzerdaten sinnvoll und auch angebracht. Dazu wären weitere, manuell angelegte setter-Methoden (setLastname(), ..) sinnvoll, die dem Namen der ihnen zugeordneten Eigenschaft entsprechen („lastname“ => „setLastname“). So könnten diese beim import() durch einen zusammengebauten Callback verwendet werden, anstatt den Array anstandslos zu übernehmen:

    PHP-Code:
    <?php
    /**
     * @author Christian Reinecke <reinecke@bajoodoo.com>
     * @since 2009-04-17
     */
    class Anti_Record_Abstract
    {
        
    // .. 

        /**
         * @param array $properties
         * @param bool $merge [optional]
         * @return null
         */
        
    public function import(array $properties$merge false)
        {
            if (!
    $merge) {
                
    $this->_properties = array();
            }
            foreach (
    $properties as $property => $value) {
                
    // @example:
                // $property = "firstname"
                
    $callback = array($this"set" ucfirst($property)); 
                
    // $callback = array($this, "setFirstname")
                
    $params   = array($value);
                if (!
    is_callable($callbackfalse)) {
                    
    // $this->setFirstname() does not exist, use fallback
                    
    $callback = array($this"_setProperty");
                    
    // $this->_setProperty("firstname", $value)
                    
    $params   = array($property$value);
                }
                
    call_user_func_array($callback$params);
            }
        }
        
        
    // ..
    }
    ?>
    Wenn vorhanden werden nun also die eigens definierten setter-Methoden verwendet, andernfalls der Fallback auf _setProperty().

    Selbstverständlich sind ähnliche Filter für die Ausgabe über export() im selben Stil denkbar (genauso wie Eigenschaften mit _, auf eine CamelCased-setter/getter-Methode umgedrückt werden könnten). Feel free

    Bei den setter-Methoden – also Dateneingabe – sollte dann darauf geachtet werden, dass die Daten validiert werden (String-Länge (Text), begrenzter Zahlenraum (Zahlen), existierendes Datum (Datum, check_date) etc.), aber ein möglichst maschinenlesbarer Wert erhalten bleibt (normiertes Datums-Format, ..), also wenig Implikation (meine Anwender sind Deutsch, die Ausgabe kommt ins HTML, ..) vorgenommen werden.

    Die getter-Methoden werden oft nur die Eigenschaften durchschleusen und zurückgeben. Interessanter sind dabei getter-Methoden, die die vorhandenen Werte auswerten, wie es getAge() macht. Für die Ausgabesprache der Webseite könnte auch der Ländercode in der Telefonnummer hinzugezogen werden, falls nicht bessere Werte vorliegen.

    Also ein getCountrySuggestion().

    (Sicherlich wäre es in dieser Klasse nicht angebracht, die $_SERVER[ HTTP_ACCEPT_LANGUAGE] hinzu zu ziehen. Bei einer ageleiteten Klasse, die den aktuellen Session-User darstellt [Anti_Record_Session_User] wäre dies schon eher denkbar.)

    In Verbindung mit AJAX könnte man sicherlich einige Predictions für noch offene Eingabefelder vornehmen.
    So sieht das ganze also in Aktion aus:

    PHP-Code:
    <?php
    /**
     * @author Christian Reinecke <reinecke@bajoodoo.com>
     * @since 2009-04-17
     * @warning use UTF-8 encoding without BOM for this file
     * @see http://wikipedia.org/wiki/Byte_Order_Mark
     */

    require_once "./Anti_Record_Abstract.php";
    require_once 
    "./Anti_Record_User.php";

    /**
     * @see http://php.net/nl2br
     * @see http://php.net/htmlentities
     * @param string $string plain-text
     * @return string html-optimized
     */
    function toHtml($string)
    {
        return 
    nl2br(htmlentities($stringENT_COMPAT"utf-8"));
    }

    $userData = array(
        
    "firstname" => "Max",
        
    "lastname"  => "Müller",
        
    "telephone" => "+(49)721 12345",
        
    "birthday"  => "1982-09-05");

    $user = new Anti_Record_User();
    $user->import($userData);

    header("Content-Type: text/html; charset=utf-8");
    ?>
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml">
        <head>
            <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
            <title>User Profile</title>
        </head>
        <body>
            <h1><?php echo toHtml($user->getLastname()) ?><?php echo toHtml($user->getFirstname()) ?></h1>
            <div>
                <dl>
                    <dt>Age</dt>
                    <dd><?php echo toHtml($user->getAge("today")) ?></dd>
                    <dt>Telephone</dt>
                    <dd><?php echo toHtml($user->getTelephone()) ?></dd>
                </dl>
            </div>
        </body>
    </html>
    Und hier die „finale“ Version von Anti_Record_Abstract:

    PHP-Code:
    <?php
    /**
     * @author Christian Reinecke <reinecke@bajoodoo.com>
     * @since 2009-04-17
     */
    class Anti_Record_Abstract
    {
        
    /**
         * @var array
         */
        
    private $_properties = array();

        
    /**
         * @param array $properties
         * @param bool $merge [optional]
         * @return null
         */
        
    public function import(array $properties$merge false)
        {
            if (!
    $merge) {
                
    $this->_properties = array();
            }
            foreach (
    $properties as $property => $value) {
                
    // @example:
                // $property = "firstname"
                // =>
                // $callback = array($this, "setFirstname")
                // =>
                // $this->setFirstname()
                
    $callback = array($this"set" ucfirst($property));
                
    $params   = array($value);
                if (!
    is_callable($callbackfalse)) {
                    
    // @example:
                    // $this->setFirstname() does not exist, use fallback
                    // =>
                    // $this->_setProperty("firstname", $value)
                    
    $callback = array($this"_setProperty");
                    
    $params   = array($property$value);
                }
                
    call_user_func_array($callback$params);
            }
        }

        
    /**
         * @return array $properties
         */
        
    public function export()
        {
            
    $properties = array();
            foreach (
    $this->_properties as $property => $value) {
                
    $callback = array($this"get" ucfirst($property));
                
    $params   = array();
                if (!
    is_callable($callbackfalse)) {
                    
    $callback = array($this"_getProperty");
                    
    $params   = array($property$value);
                }
                
    $properties[$property] = call_user_func_array($callback$params);
            }
            return 
    $properties;
        }

        
    /**
         * @return int|null
         */
        
    public function getId()
        {
            return 
    $this->_getProperty("id");
        }

        
    /**
         * @param string $property
         * @param bool $checkNotNull
         * @return bool
         */
        
    protected function _hasProperty($property$checkNotNull false)
        {
            return 
    array_key_exists($property$this->_properties)
                && (!
    $checkNotNull || null !== $this->_properties[$property]);
        }

        
    /**
         * @param string $property
         * @return mixed|null
         */
        
    protected function _getProperty($property)
        {
            return 
    array_key_exists($property$this->_properties)
                 ? 
    $this->_properties[$property]
                 : 
    null;
        }

        
    /**
         * @param string $property
         * @param mixed $value
         * @return null
         */
        
    protected function _setProperty($property$value)
        {
            
    $this->_properties[$property] = $value;
        }
    }
    ?>
    Anti_Record_User:

    PHP-Code:
    <?php
    /**
     * @author Christian Reinecke <reinecke@bajoodoo.com>
     * @since 2009-04-17
     */
    class Anti_Record_User extends Anti_Record_Abstract
    {
        
    /**
         * @return string
         */
        
    public function getFirstname()
        {
            return 
    $this->_getProperty("firstname");
        }

        
    /**
         * @return string
         */
        
    public function getLastname()
        {
            return 
    $this->_getProperty("lastname");
        }

        
    /**
         * @param string $phone
         * @return null
         */
        
    public function setTelephone($phone)
        {
            if (
    preg_match("/^\+(\([0-9]+\))(.+)$/"$phone$match)) {
                
    $this->_setProperty("telephone_country"trim($match[1], "()")); // (\([0-9]+\))
                
    $this->_setProperty("telephone",         "0" $match[2]);       // (.+)
            
    } else {
                
    $this->_setProperty("telephone"$phone);
            }
        }

        
    /**
         * @return string
         */
        
    public function getTelephone()
        {
            
    $telephone $this->_getProperty("telephone");
            if (
    $this->_hasProperty("telephone_country")) {
                
    // add country's telephone code and cut off leading 0 from $telephone
                
    $telephone  "+(" $this->_getProperty("telephone_country")
                            . 
    ")" mb_substr($telephone1);
            }
            return 
    $telephone;
        }

        
    /**
         * @see [url=http://php.net/strtotime]PHP: strtotime - Manual[/url]
         * @param string $when date time value for strtotime()
         * @return int age
         */
        
    public function getAge($when "today")
        {
            
    $birthday      $this->_getProperty("birthday");
            
    $birthdayStamp strtotime($birthday);
            
    $birthdayStamp date("Ymd"$birthdayStamp);

            
    $whenStamp strtotime($when);
            
    $whenStamp date("Ymd"$whenStamp);

            
    $age floor(($whenStamp $birthdayStamp) / 10000);

            return (int)
    $age;
        }
    }
    ?>
    Fazit:

    Was haben wir nun mit dieser Klasse gewonnen? Wir haben gewonnen, dass diese Klasse unser zentraler Zugriffspunkt auf alle Benutzer („User“) der Anwendung geworden ist. Erweitern wir diese Klasse um Eigenschaften oder Methoden, steht sofort jedem Skript die Erweiterung zur Verfügung. Weiterhin ist die Klasse gut geeignet für Unit-Tests, da sie unabhängig ihrer Quelle funktioniert. Über die import() und setter-Methoden können Werte nun von Unit-Tests, der Datenbank, einem Formular oder einer CSV-Datei stammen. Das Filtern von (Eingabe-)Werten kann hier an zentraler Stelle vorgenommen werden.

    Der wichtigste Punkt ist jedoch, dass man sich um den Datenzugriff keine Gedanken mehr machen muss. Sobald ein Record-Objekt vorhanden ist, hat man Zugriff auf alle benötigten Daten und den Komfort aller Schnittstellen, die dieses Objekt als Parameter akzeptieren.

    Im nächsten Tutorial möchte ich auf RecordSet-Objekte eingehen, die Listen solcher (materialisierten) Objekte verwalten, auf die sich Sortierung und Filterungen anwenden lassen und die sich wie gewöhnliche Arrays mittels Interfaces mit foreach() oder while() durchlaufen lassen.

    Falls zu diesem Tutorial Fragen, Anregungen, Kritik oder Verbesserungsvorschläge aufkommen, fände ich es ne feine Sache, wenn man darüber hier diskutieren würde, weil ich selbst erst vor kurzem so programmiere, und die Idee dafür durch "mal hier mal da abgucken" zustande kam und sicherlich auch noch offene Fragen im Raum stehen. Zum Beispiel die Referenz zu anderen Record-Objekten. Hierzu bin ich übrigens, nach dem mir PHP den Speicher leergefressen hatte, da ich Photo-Objekte mit ihren File-Objekten verknüpft hatte auf einen Callback-Mechanismus gekommen, den ich aber erst am Ende der Tutorials ansprechen möchte, da sie etwasa ungewöhnlich ist, aber überraschend einfach und gut funktioniert. Sicherlich ist es aber auch schwer ohne die restlichen Tutorials hier einen Gesamteindruck zu posten. Vielleicht lässt sich der ein oder andere ja doch zu einem Kommentar hinreißen.

    Gruß,
    c
    "Mein Name ist Lohse, ich kaufe hier ein."


  • #2
    RecordSet-Klasse:

    Hallo,

    im zweiten Tutorial-Teil möchte ich auf RecordSet-Objekte eingehen, die nun als Liste von den im ersten Tutorial angesprochenen Record-Objekten fungieren. Natürlich könnte man statt solcher RecordSet-Objekten auch einfache Arrays verwenden, der geringe Overhead solcher Objekte bringt allerdings einige kleine Vorteile mit, auf die ich nurnoch ungerne verzichte.
    Dies wären wieder einmal Schnittstellen-Sicherheit, das heißt eine Methode oder Funktion, die eine Liste von Record-Objekten möchte, soll dies nicht in einer zusätzlichen Prüfung testen müssen, sondern durch Angabe der RecordSet-Klasse sofort sichergehen können, dass es sich um eine Liste dieser Record-Objekte handelt.

    Weiterhin können auf diese RecordSet-Objekte Sortierung, Filter aber vielleicht wichtiger Gruppierungen vorgenommen und Meta-Informationen entnommen werden.
    Technisch wird wie beim Record-Objekt zunächst eine Unterteilung in abstrakte Basis-Klasse („RecordSet“) und konkrete RecordSet-Implementierung vorgenommen. Hier zunächst die abstrakte Basis-Klasse, wieder mit meinem Prefix „Anti“:

    PHP-Code:
    <?php
    /**
     * @author Christian Reinecke <reinecke@bajoodoo.com>
     * @since 2009-04-27
     */
    abstract class Anti_RecordSet_Abstract implements CountableIterator
    {
        
    /**
         * @var array
         */
        
    protected $_records = array();

        
    /**
         * @return string record class name
         */
        
    abstract protected function _getRecordClassName();

        
    /**
         * @see Countable
         * @return int
         */
        
    public function count()
        {
            return 
    count($this->_records);
        }

        
    /**
         * @see Iterator
         * @return Anti_Record_Abstract|null
         */
        
    public function current()
        {
            return 
    current($this->_records);
        }

        
    /**
         * @see Iterator
         * @return Anti_Record_Abstract|null
         */
        
    public function next()
        {
            return 
    next($this->_records);
        }

        
    /**
         * @see Iterator
         * @return int
         */
        
    public function key()
        {
            return 
    key($this->_records);
        }

        
    /**
         * @see Iterator
         * @return bool
         */
        
    public function valid()
        {
            return (bool)
    $this->current();
        }

        
    /**
         * @see Iterator
         * @return Anti_Record_Abstract|false
         */
        
    public function rewind()
        {
            return 
    reset($this->_records);
        }

        
    /**
         * @throws InvalidArgumentException
         * @see [url=http://php.net/manual/en/class.invalidargumentexception.php]PHP: InvalidArgumentException - Manual[/url]
         * @param Anti_Record_Abstract $record
         * @return null
         */
        
    public function add(Anti_Record_Abstract $record)
        {
            
    $this->_checkRecord($record);
            
    $this->_records[] = $record;
        }

        
    /**
         * @throws InvalidArgumentException
         * @see [url=http://php.net/manual/en/class.invalidargumentexception.php]PHP: InvalidArgumentException - Manual[/url]
         * @param Anti_Record_Abstract $record
         * @return bool
         */
        
    public function isFirst(Anti_Record_Abstract $record)
        {
            
    $this->_checkRecord($record);
            return !empty(
    $this->_records) && $this->_records[0] === $record;
        }

        
    /**
         * @throws InvalidArgumentException
         * @see [url=http://php.net/manual/en/class.invalidargumentexception.php]PHP: InvalidArgumentException - Manual[/url]
         * @param Anti_Record_Abstract $record
         * @return bool
         */
        
    public function isLast(Anti_Record_Abstract $record)
        {
            
    $this->_checkRecord($record);
            return !empty(
    $this->_records) && $this->_records[count($this->_records)-1] === $record;
        } 

        
    /**
         * @return array
         */
        
    public function toArray()
        {
            return 
    $this->_records;
        }

        
    /**
         * @throws InvalidArgumentException
         * @see [url=http://php.net/manual/en/class.invalidargumentexception.php]PHP: InvalidArgumentException - Manual[/url]
         * @param Anti_Record_Abstract $record
         * @return null
         */
        
    protected function _checkRecord(Anti_Record_Abstract $record)
        {
            
    $className $this->_getRecordClassName();
            if (!
    $record instanceof $className) {
                throw new 
    InvalidArgumentException("given record is not an instance of $className");
            }
        }
    }
    ?>
    Zunächst implementiert unsere Klasse mit „implements“ zwei PHP-interne Interfaces, denen wir mit konkret implementierten Methoden entsprechen müssen.

    Das Countable Interface macht unsere Objekt zählbar, das heißt wir können darauf count() aufrufen, und bekommen wie bei einem Array die Anzahl der Elemente. Ein direkter Aufruf von $recordset->count() ist natürlich auch möglich.

    Das Iterator Interface verlangt von uns Methoden, um auf den Elementen (Records) zu iterieren. Iterieren heißt, dass ein interner (uns verborgener) Zeiger auf eines der Record-Objekte zeigt. Mit next(), current(), rewind() können wir diesen Zeiger verschieben. Dieses Interface ist nötig, damit unser Array mit foreach() durchlaufen werden kann.

    Weiterhin existiert die Methode add(), mit der ein neues Element hinzugefügt werden kann. Dabei kann in der abstrakten Deklaration natürlich nicht auf ein konkretes Record-Objekt hin getestet werden, sondern nur auf sein abstraktes Pendant, also „Anti_Record_Abstract“. Den expliziten Test, ob das Record-Objekt auch zum RecordSet passt, übernimmt unsere geschützte Methode _checkRecord(). Über die Deklaration der ebenfalls geschützten Methode _getRecordClassName() als abstrakt stellen wir sicher, dass jede konkrete Ableitung von Anti_RecordSet_Abstract (z.B. Anti_Message_RecordSet) diese Methode implementiert, andernfalls wirft PHP bereits beim Laden der Klasse einen Fehler. Die Methode _checkRecord() testet nun also, ob das Objekt eine Instanz der Klasse ist, die über _getRecordClassName() zurückgegeben wird.

    Das Methoden-Prefix check ist wie get, set, is und has bei mir übrigens reserviert für eine bestimmte Tätigkeit. check*()-Methoden haben keinen Rückgabewert, sondern werfen im Fehlerfall nur eine Exception. In diesem Fall eine InvalidArgumentException(), diese ist Teil der SPL-Standard-Bibliothek von PHP und leitet von der LogicException ab, welche wiederum von der Standard-Exception ableitet. Lange Rede kurzer Sinn: Wir können diese Exception ganz normal mit catch (Exception $exception) oder spezieller mit catch (InvalidArgumentException $exception) fangen.

    Die beiden Methoden isFirst() und isLast() sind übrigens bereits Helfer für die Ausgabe, beispielsweise um CSS-Klassen für das erste oder letzte Element zuweisen zu können, ohne sich selbst um das Zählen kümmern zu müssen. Allgemeine und wiederkehrende Aufgaben sollen ja schließlich dorthin wo sie hingehören, nämlich in das Objekt, das die Liste verwaltet. Das weiß es ja schließlich am Besten.

    Zum Abschluss darf natürlich die konkrete Implementierung von Anti_Message_RecordSet nicht fehlen:

    PHP-Code:
    <?php
    class Anti_Message_RecordSet extends Anti_RecordSet_Abstract
    {
        protected function 
    _getRecordClassName()
        {
            return 
    "Anti_Message_Record";
        }
    }
    ?>
    Wofür nun das ganze? Zunächst mal war der Hauptgrund für ein Array-Objekt und gegen den reinen Array die Schnittstelle. Ich finde es sehr wichtig, saubere Schnittstellen zu definieren, Fehler also so früh wie möglich zu erkennen. Ein Punkt der auch im Design-by-Contract angesprochen wird: Vorbedingung, Zwischenbedingung, Nachbedingung. Dass einer Methode korrekte Parameter übergeben werden sollte ist sicherlich die Vorbedingung, um korrekt arbeiten zu können. Weiterhin haben mich Kleinigkeiten gestört, wie das angedeutete Durchlaufen von Schleifen und dem Testen auf bestimmte Positionsmerkmale im Array. Vorher sah es so aus:

    PHP-Code:
    <?php
    foreach ($messages as $i => $message) {
        
    $class = ($i == 0)                    ? " first" "";
        
    $class = ($i == count($messages) - 1) ? " last"  "";
        
    $class ltrim($class);
        
        echo 
    '<li class="' $class '">' $message["text"] . '</li>';
    }
    ?>
    Und was wenn jetzt jemand zwischendurch mal ein Element löscht und $i garkeine Position mehr darstellt? Oder $message[„text“] nicht existiert?
    Bei der OOP Variante mit RecordSet und Record ist mir beides egal, ich bekomme weder eine falsche Positionsangabe und dadurch eine verhunzte Ausgabe im Browser, noch einen Undefined-Index Hinweis von PHP. Die Validierung wurde ja schon bei der Erzeugung der entsprechenden Objekt-Instanz durchgeführt. Der Fehler wird wenn also geworfen, wo er entsteht.
    Kommen wir aber zu einem interessanteren Feature, den Operationen auf diese Mengen (Record-Objekten). Möglich sind zusätzliche Sortierungen oder Filter, die aber hauptsächlich im Teil der noch zu besprechenden Manager-Klasse liegen würden. Bei Nachrichten könnte eine Gruppierung der Nachrichten gewünscht sein. Wie man es aus Thunderbird und Konsorten kennt. „Nachrichten heute“, „letzte Woche“, „älter“. Da dies eine Operation auf einer Menge materialisierter Mengen ist (mit materialisiert meine ich konkrete Record-Instanzen, die auch zur Anzeige in diesem Skriptdurchlauf bestimmt sind).

    Definieren wir also zunächst einmal eine Gruppen-Konfiguration:

    PHP-Code:
    <?php
    $dateTimeGroupDefinitions 
    = array(
        array(
            
    "title" => "today",
            
    "start" => strtotime(date("Y-m-d 00:00:00")),
            
    "end"   => strtotime(date("Y-m-d 23:59:59"))
        ),
        array(
            
    "title" => "last seven days",
            
    "start" => strtotime(date("Y-m-d 00:00:00"strtotime("-8 day"))),
            
    "end"   => strtotime(date("Y-m-d 23:59:59"strtotime("yesterday")))
        ),
        array(
            
    "title" => "älter",
            
    "start" => 0,
            
    "end"   => strtotime(date("Y-m-d 23:59:59"strtotime("-9 day")))
        )
    );
    ?>
    Wenn ihr die Stirn runzelt, weil man bei der Array-Definition ja sicher einen Fehler macht ist das richtig Jedes Array, das ein bestimmtes mehrdimensionales Format hat und voraussetzt, gehört genau genommen in ein Objekt, das die Konsistenz sicherstellt. Da ich nicht abschweifen möchte, belasse ich es bei diesem Array.

    Diese Definition übergeben wir also einer Methode der Klasse Anti_Message_RecordSet und bekommen zurück Arrays von RecordSet-Objekten. Wenn wir das ganze jetzt verwursten, kommen wir zu einer Anti_Message_RecordSet-Klasse, die ihre Nachrichten auch noch zeitlich gruppieren kann und also wie folgt implementiert ist:

    PHP-Code:
    <?php
    class Anti_Message_RecordSet extends Anti_RecordSet_Abstract
    {
        protected 
    $_title;

        public function 
    getDateTimeGroups(array $dateTimeGroupDefinitions$discardEmptyGroups true)
        {
            
    $groups   = array();
            
    $messages $this->_records;
            foreach (
    $dateTimeGroupDefinitions as $dateTimeGroupDefinition) {
                
    $group = new self();
                
    $group->setTitle($dateTimeGroupDefinition["title"]);

                foreach (
    $messages as $i => $message) {
                    
    $append true;
                    
    $edited $message->getCreated();

                    if (
    $dateTimeGroupDefinition["start"] !== null) {
                        
    $append $dateTimeGroupDefinition["start"] <= $edited->format("U");
                    }
                    if (
    $append && $dateTimeGroupDefinition["end"] !== null) {
                        
    $append $edited->format("U") <= $dateTimeGroupDefinition["end"];
                    }
                    if (
    $append) {
                        
    $group->add($message);
                        unset(
    $messages[$i]); // make sure this message is added only once
                    
    }
                }

                if (
    count($group) > || !$discardEmptyGroups) {
                    
    $groups[] = $group;
                }
            }
            if (!empty(
    $messages)) {
                
    // the definition is corrupt and did not match for every message, which is bad because if we ignore
                // this, some messages will get lost
                
    throw new InvalidArgumentException("invalid datetime group definition, not every message matched");
            }
            return 
    $groups;
        }

        public function 
    setTitle($title)
        {
            
    $this->_title $title;
        }

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

        protected function 
    _getRecordClassName()
        {
            return 
    "Anti_Message_Record";
        }
    }
    ?>
    Der Unsicherheitsfaktor dieses Konstruktes ist wie schon angesprochen der Array. Arrays sind übrigens fast immer die Schwachpunkte von schlechten Skripten, die nachher keiner versteht. Es wird also Zeit wirklich OOP anzuwenden. Wenn auch wie gesagt hier aus Gründen der Übersicht nicht in letzter Konsequenz.

    Folgendes Beispiel zeigt das ganze in Aktion:

    PHP-Code:
    <?php
    /**
     * @author Christian Reinecke <reinecke@bajoodoo.com>
     * @since 2009-04-17
     * @warning use UTF-8 encoding without BOM for this file
     * @see http://wikipedia.org/wiki/Byte_Order_Mark
     */
    require_once "./init.php";

    /**
     * @see http://php.net/nl2br
     * @see http://php.net/htmlentities
     * @param string $string plain-text
     * @return string html-optimized
     */
    function toHtml($string)
    {
        return 
    nl2br(htmlentities($stringENT_COMPAT"utf-8"));
    }

    $messagesData = array(
        array(
            
    "subject" => "title1",
            
    "text"    => "text1",
            
    "created" => strtotime("today")
        ),
        array(
            
    "subject" => "title2 <script>alert(1)</script>",
            
    "text"    => "text2",
            
    "created" => strtotime("last Monday")
        ),
        array(
            
    "subject" => "title3",
            
    "text"    => "text3 <span style='font-size:55px'>test test</span>",
            
    "created" => strtotime("last Friday")
        ),
        array(
            
    "subject" => "title4",
            
    "text"    => "text4",
            
    "created" => strtotime("-37 day")
        )
    );

    $messages = new Anti_Message_RecordSet();
    foreach (
    $messagesData as $messageData) {
        
    $message = new Anti_Message_Record();
        
    $message->import($messageData);
        
    $messages->add($message);
    }

    $dateTimeGroupDefinitions = array(
        array(
            
    "title" => "today",
            
    "start" => strtotime(date("Y-m-d 00:00:00")),
            
    "end"   => strtotime(date("Y-m-d 23:59:59"))
        ),
        array(
            
    "title" => "last seven days",
            
    "start" => strtotime(date("Y-m-d 00:00:00"strtotime("-8 day"))),
            
    "end"   => strtotime(date("Y-m-d 23:59:59"strtotime("yesterday")))
        ),
        array(
            
    "title" => "älter",
            
    "start" => 0,
            
    "end"   => strtotime(date("Y-m-d 23:59:59"strtotime("-9 day")))
        )
    );

    header("Content-Type: text/html; charset=utf-8");
    ?>
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml">
        <head>
            <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
            <title>Posteingang</title>
        </head>
        <body>
            <h1>Posteingang</h1>
            <div>
    <?php if (count($messages) > 0): ?>
                <p>Sie haben insgesamt <?php echo count($messages?> Nachricht(en).</p>
                <ul>
    <?php     foreach ($messages->getDateTimeGroups($dateTimeGroupDefinitions) as $messages): ?>
                    <li>
                        <?php echo toHtml($messages->getTitle()) ?>
                        <ul>
    <?php         foreach ($messages as $message): ?>
                            <li>
                                <div class="title"><?php echo toHtml($message->getSubject()) ?></div>
                                <div class="message"><?php echo toHtml($message->getText()) ?></div>
                                <div class="created"><?php echo $message->getCreated()->format("d.m.Y, H:i:s"?></div>
                            </li>
    <?php         endforeach ?>
                        </ul>
                    </li>
    <?php     endforeach ?>
                </ul>
    <?php else: ?>
                <p>Keine Nachrichten.</p>
    <?php endif ?>
            </div>
        </body>
    </html>
    (init.php stellt lediglich das Einbinden der Klassen sicher und setzt die Standard-Zeitzone für PHP auf „Europe/Berlin“; meinen schlechten Stil $messages in der Schleife zu überschreiben muss nicht übernommen werden )

    Als Ausgabe bekommen wir unser gruppiertes Ergebnis:

    Code:
    Posteingang
    
    Sie haben insgesamt 4 Nachricht(en).
    
        * today
              o title1
                text1
                27.04.2009, 00:00:00
        * last seven days
              o title2 <script>alert(1)</script>
                text2
                20.04.2009, 00:00:00
              o title3
                text3 <span style='font-size:55px'>test test</span>
                24.04.2009, 00:00:00
        * älter
              o title4
                text4
                21.03.2009, 00:16:19
    Fazit:
    Ein RecordSet-Objekt hat wenig Overhead, zugegeben keinen erheblichen Mehrwert, reiht sich aber konsequent ins OOP ein und kann in der ein oder anderen Lage sicherlich der richtige Ort für das Handling von Record-Listen sein.

    Gleiches wie bisher: Fragen und Kritik fände ich ne feine Sache.

    Danke fürs Lesen,
    Gruß,
    c
    "Mein Name ist Lohse, ich kaufe hier ein."

    Kommentar


    • #3
      Sehr gutes Tutorital, habs gelesen und gleich verstanden. Möglicherweise werde ich dein RecordSet auch verwenden.

      Danke
      Mfg Tomtaz
      "Es soll jetzt diese Erfindung geben.... Kugel oder so heißt die. Ist so eine Art Suchmaschine..."

      Kommentar


      • #4
        Huch, ich hatte gehofft, die Managerklasse käme gleich noch. Hat sich gerade so gut gelesen. Dickes Lob an die Schreibweise, inklusive des vorhergesagten Stirnrunzelns.

        Ein paar kleine Anmerkungen/Kritiken
        Records
        • Callback-Fallback
          Hier sollte man nachdenken, ob man nicht lieber mit Exception/Abbruch oder zumindest einer Notice reagiert. Setter und Getter zu definieren ist zwar nervig, aber so fehlt einerseits das konkrete Interface und zweites wird es dann hakelig, wenn es um Datenanbindung an die DB und die konkrete Zuordnung Property-Feld geht.
          Alternativ könnte man drüber nachdenken,
          1. im Setter gültige "Felder" zu definieren
          2. Im Prop-Array über die assoz. Schlüssel gültige "Felder" zu definieren (ähnlich wie im Getter) - alle setzbaren Felder mit NULL initialisieren
          3. diese Definition irgendwie über den DB Manager zu regeln (was dann natürlich das Objekt davon abhängig macht).
          4. ergo: vielleicht eine allgemeingültige Initialisierungsmethode einzuführen, über die gültige Properties angegeben werden.
        • Du sprichst Validierung im Setter an, wobei ich mich frage, ob das aus Sicht des Prinzips Zusicherungen überhaupt Belang der Datenhaltung ist. Diskussionsansätze:
          1. Das Record weiß ja nicht, dass bspw. ein angebundenes DB Feld nur max. 120 Zeichen verträgt
          2. Muss es das denn auch wissen? Ist für die korrekte Angabe denn nicht die Eingabe und deren Validierung zuständig und für die korrekte Speicherung die Datenbankschicht? Denn:
          3. Schließlich könnte die Initialisierung des Records ja auch aus einer Session vorgenommern werden, die bereits validierte Daten enthält (bspw. nach einem mehrstufigen Eingabeprozess wie einem Assistenten)

        RecordSet
        • Die Gruppierung wäre eigentlich ein schönes Beispiel für das Decorator-Pattern. Zumal man dann noch einen echten Mehrwert hätte (Sortierbarkeit innerhalb der Gruppe).
        --

        „Emoticons machen einen Beitrag etwas freundlicher. Deine wirken zwar fachlich richtig sein, aber meist ziemlich uninteressant.
        Wenn man nur Text sieht, haben viele junge Entwickler keine interesse, diese stumpfen Texte zu lesen.“


        --

        Kommentar


        • #5
          Zitat von nikosch Beitrag anzeigen
          Hier sollte man nachdenken, ob man nicht lieber mit Exception/Abbruch oder zumindest einer Notice reagiert. Setter und Getter zu definieren ist zwar nervig, aber so fehlt einerseits das konkrete Interface und zweites wird es dann hakelig, wenn es um Datenanbindung an die DB und die konkrete Zuordnung Property-Feld geht.
          Jein, ich hatte auch eine Initialisierung im Sinn, aber wenn man die Datenbank umstellt und plötzlich die Anwendung Exceptions wirft finde ich das keine schöne Lösung. Das Problem hatten wir hier in der Firma, als ein neues Datenbank-Feld für den User hinzukam. Der User ist so ziemlich das erste, was in der Anwendung initialisiert wird, wenn dann schon Exceptions auftreten ist die Anwendung tot. Deshalb hab ich in meinen neueren Entwürfen davon abgesehen.


          • Du sprichst Validierung im Setter an, wobei ich mich frage, ob das aus Sicht des Prinzips Zusicherungen überhaupt Belang der Datenhaltung ist. Diskussionsansätze:
            1. Das Record weiß ja nicht, dass bspw. ein angebundenes DB Feld nur max. 120 Zeichen verträgt
            2. Muss es das denn auch wissen? Ist für die korrekte Angabe denn nicht die Eingabe und deren Validierung zuständig und für die korrekte Speicherung die Datenbankschicht? Denn:
            3. Schließlich könnte die Initialisierung des Records ja auch aus einer Session vorgenommern werden, die bereits validierte Daten enthält (bspw. nach einem mehrstufigen Eingabeprozess wie einem Assistenten)
          Guter Punkt. Darüber muss ich nochmal nachdenken.


          RecordSet
          • Die Gruppierung wäre eigentlich ein schönes Beispiel für das Decorator-Pattern. Zumal man dann noch einen echten Mehrwert hätte (Sortierbarkeit innerhalb der Gruppe).
          Zugegeben, außer der Messaging-Gruppierung fallen mir nicht mehr viele Anwendungsbeispiele ein. Aber mir reicht der kleine Vorteil das ganze in einem Objekt zu kapseln schon aus.
          "Mein Name ist Lohse, ich kaufe hier ein."

          Kommentar


          • #6
            Jein, ich hatte auch eine Initialisierung im Sinn, aber wenn man die Datenbank umstellt und plötzlich die Anwendung Exceptions wirft finde ich das keine schöne Lösung.
            Naja gut, auf dem Livesystem wäre das nicht schön. In der Entwicklung ist mir sowas im allgemeinen lieber, also wenn irgendwelche Seiteneffekte auftreten, nach denen man sich dämlich sucht und sich nach 3 Tagen an die Stirn schlägt - Mensch hast ja neulich ein Feld hinzugefügt. Nicht dass ich jetzt ein Beispiel hätte, aber den Effekt kennt man zur Genüge.

            Auf der anderen Seite verletzte alles bis auf die Strukturinitialisierung seitens der DB dann auch wieder das DRY-Prinzip. Wenn man weiterhin kein zusätzliches Domänenobjekt einführt, wird es sich i.A. kaum vermeiden lassen, dass das Objekt mehr Daten als die zugehörige Datenbankstruktur enthält.
            --

            „Emoticons machen einen Beitrag etwas freundlicher. Deine wirken zwar fachlich richtig sein, aber meist ziemlich uninteressant.
            Wenn man nur Text sieht, haben viele junge Entwickler keine interesse, diese stumpfen Texte zu lesen.“


            --

            Kommentar


            • #7
              Durch das Akzeptieren von unbekannten Eigenschaften treten aber keine Seiteneffekte auf.
              "Mein Name ist Lohse, ich kaufe hier ein."

              Kommentar


              • #8
                Alles nur eine Frage der Phantasie:
                PHP-Code:
                public function export()
                    {
                        return 
                $this->_properties;
                    } 


                Fällt mir gerade noch auf: getId() benutzt pauschal die 'id'-Property. Matchst Du das später auf ein etwaig anderslautendes DB-Feld oder benutzt Du eine entsprechende DB-Namenspolicy?
                --

                „Emoticons machen einen Beitrag etwas freundlicher. Deine wirken zwar fachlich richtig sein, aber meist ziemlich uninteressant.
                Wenn man nur Text sieht, haben viele junge Entwickler keine interesse, diese stumpfen Texte zu lesen.“


                --

                Kommentar


                • #9
                  OK das ist mein Namensschema. Im schlimmsten Fall bekommst du durch _getProperty("id") ein NULL zurück. Vielleicht kann man dafür eine protected Variable einführen, die bei Bedarf vom konkreten Record überschrieben werden kann.
                  "Mein Name ist Lohse, ich kaufe hier ein."

                  Kommentar


                  • #10
                    Der Manager ist nicht vergessen, habe leider wenig Zeit im Moment.
                    "Mein Name ist Lohse, ich kaufe hier ein."

                    Kommentar


                    • #11
                      Verdienter Schelte für's Thread pushen zum Trotz - Ehre wem Ehre gebührt:

                      Nice! Sehr gut zu lesen, verständlich selbst noch nach einer schlaflosen Nacht und natürlich anregender Code.
                      Hoffen wir mal, dass du irgendwann wieder ein wenig Zeit findest - Dein Ansatz bei der Manager-implementierung wird sicherlich nicht nur mich interessieren.

                      Kommentar


                      • #12
                        Zugegebenermassen hab ich die Implementierung mittlerweile etwas abgeaendert. Am Manager war ich kuerzlich dran, allerdings bin ich noch am ueberlegen, ob ich Code poste, den ich konkret benutze, oder ob ich poste, wie man es meiner Meinung nach eigentlich (besser) machen sollte. Auch beim Filtern der Manager-Selektion hab ich mich selbst noch nicht fuer eine Implementierung entschieden. Wuerde hier das ungern als Best-Practice (sowieso nur meine Meinung) posten, wenn ich davon selbst noch nicht voll ueberzeugt bin. Und bei der Hitze faellts gleich 3x schwerer sich 2-3 Stunden hinzusetzen und das runterzuschreiben. Aber es wird kommen .. irgendwann!

                        PS: Das ganze schreit uebrigens foermlich nach einem Codegenerator, ich hab sogar ueberraschenderweise innerhalb von kuerzester Zeit (knapp ne Stunde) einen kleinen brauchbaren geschrieben, um eine Datenbank in Modelle/Record-Klassen zu ueberfuehren. Aber wie immer nix serienreif. Den wuerde ich dann als Tutorial (falscher Begriff, vielmehr Erfahrungsbericht) Punkt 4 schonmal vorankuendigen (fuer naechstes Jahr )
                        "Mein Name ist Lohse, ich kaufe hier ein."

                        Kommentar

                        Lädt...
                        X