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 ..
.. 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:
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“ :
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:
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:
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:
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:
Und hier die „finale“ Version von Anti_Record_Abstract:
Anti_Record_User:
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
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 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 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($telephone, 1);
}
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;
}
}
?>
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
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($callback, false)) {
// $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);
}
}
// ..
}
?>
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($string, ENT_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>
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($callback, false)) {
// @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($callback, false)) {
$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;
}
}
?>
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($telephone, 1);
}
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;
}
}
?>
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
Kommentar