PHP Pear: DB_DataObject

Caution: PLZ READ

While cleaning up my harddisc I found an old (german) article I started to write about PHP Pear’s DB_DataObject. As it is me I didn’t really finish the article but I came quite far therefore I decided to publish it on my blog now. Sry, I can’t give support anymore and I don’t know if all that is written isn’t deprecated now. But I know that when I was forced to use PHP DataObject was a good sollution for accessing databases on php.

Plz consider, I know the format of the document is a catastrophy but as long as I don’t know that this might be still useful I won’t change that! Keep on rocking! And good luck! :D

That’s what it’s all about:

http://pear.php.net/package/DB_DataObject/redirected

————————————————————————————

DB-Mapper und SQL-Builder:DataObject

Nie wieder Wrapper-Klassen selber schreiben! Der Erlöser ist in Form des PEAR-Pakets DB_DataObject auf die PHP-Welt gekommen. Das auf PEAR:DB aufsetzende Paket generiert anhand einer Datenbank für jede Tabelle die entsprechende Wrapper-Klasse, mit der sämtliche Tabellendaten ausgelesen und manipuliert werden können, ohne auch nur eine Zeile SQL schreiben zu müssen.

In professionel eingesetzten PHP-Applikationen ist es üblich, Geschäfts- und Präsentationslogik zu trennen. Üblicherweise schreibt man für Tabellen der Datenbank Wrapper-Klassen, die Datensätze dieser Tabelle repräsentieren. Die Wrapper-Klassen haben letztendlich alle die gleich Struktur: Hole, Lösche, Modifiziere, löschen Datensatz sowie füge einen neuen ein. Die Zeiten dieser lästigen Sisyphos-Arbeit sind spätestens seit dem Erscheinen des DB_DataObject Moduls gezählt. DataObject erstellt anhand der Datenbank die Wrapper-Klassen automatisch, die es ermöglicht aus die Datensätze zuzugreifen. Dazu ermöglicht das Modul durch den eingebauten SQL-Builder Datenbank-Zugriffe, ohne auch nur eine Zeile SQL schreiben zu müssen. Durch die Arbeit mit Klassen ist dies um ein vielfaches Intuitiver und das lästige Parsen des Resultset erledigt DataObject für sie. Trigger simulieren, Update, Select, Fetching, joins. RAW-Queries

Schwer zu glauben, dass der Albtraum ein Ende hat? Hier nun ein Fahrplan, wie man das Grundgerüst seiner Geschäftslogik mit Hilfe von PEAR:DataObject erstellen lassen kann:

  1. Datenbank einrichten

Für unser Beispiel legen wir in Mysql eine kleine Shop-DB namens db_shop an, die drei Tabellen,

user, artikel, bestellung, bestellung_artikel beinhaltet:

--
-- Tabellenstruktur für Tabelle `artikel`
--
CREATE TABLE `artikel` (
`artikel_id` int(11) NOT NULL default '0',
`bezeichnung` varchar(100) collate latin1_general_ci NOT NULL default '',
`preis` float NOT NULL default '0',
`anzahl_in_lager` int(11) NOT NULL default '0',
PRIMARY KEY  (`artikel_id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 COLLATE=latin1_general_ci;
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `bestellung`
--
CREATE TABLE `bestellung` (
`bestellung_id` int(11) NOT NULL default '0',
`user_id` int(11) NOT NULL default '0',
`datum` date NOT NULL default '0000-00-00',
PRIMARY KEY  (`bestellung_id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 COLLATE=latin1_general_ci;
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `bestellung_artikel`
--
CREATE TABLE `bestellung_artikel` (
`bestellung_id` int(11) NOT NULL default '0',
`artikel_id` int(11) NOT NULL default '0'
) ENGINE=MyISAM DEFAULT CHARSET=latin1 COLLATE=latin1_general_ci;
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `user`
--
CREATE TABLE `user` (
`userid` int(11) NOT NULL default '0',
`name` varchar(80) collate latin1_general_ci NOT NULL default '',
`email` varchar(100) collate latin1_general_ci NOT NULL default '',
PRIMARY KEY  (`userid`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 COLLATE=latin1_general_ci;
  1. Datenbank-Informationen 	in system.ini verfügbar machen:
im ROOT-Verzeichnis eine Datei anlegen, die folgende minmalkonfiguration bzgl der Datenbank  beinhaltet:
[DB_DataObject]
database=dsn  <db-string>://<username-db>:<passwort-db>@<host>/<datenbank>
schema_location = pfad, wo interne Schema-Datei angelegt werden soll
class_location = pfad, wo wrapper-klassen gespeichert werden sollen
class_prefix = PrefixTabellenname
In unserem Fall:
[DB_DataObject]
database        = mysql://root:@localhost/db_shop
schema_location = f:\workspace\dataobject\db\db_shop
class_location  = f:\workspace\dataobject\db\db_shop
class_prefix    = DB_
  1. createTables.php aufrufen

php f:\xampp\php\pear\DB\DataObject\createTables.php system.ini aufrufen.

Durch diesen Aufruf wird mit Hilfe der in system.ini bereitgestellten Daten auf die Datenbank zugegriffen und anhand der vorhandenen Tabellen, die entsprechenden Wrapper-Klassen generiert und in das geforderte Verzeichnis db_shop geschoben.

<abbildung 1>
abb 1: So sollte es aussehen

In dem Verzeichnis findet man auch das interne Datenbank-Schema, auf das allerdings an dieser Stelle nicht eingegangen wird.

Um auf die Daten via DataObject zugreifen zu können, muss man nun nur noch eine Configurations-Datei-Schreiben die jeweils per require_once in das PHP-Skript eingebunden werden muss.

<?php
require_once "DB/DataObject.php";
$config = parse_ini_file('system.ini',TRUE);
foreach($config as $class=>$values) {
$options = &PEAR::getStaticProperty($class,'options');
$options = $values;
}
?>

Dort wird wie schon bei createTables.php die system.ini Datei an DataObject

weitergeleitet, damit sämtliche Daten für den Zugriff auf die Datenbank bereitstehen

Damit ist der erste Schritt geschafft! Wir haben die Wrapper-Klassen erstellt, und können mit Hilfe der Configurations-Datei auf die Tabellen zugreifen.

Um z.B. aus unserem Shop-Beispiel einen bestimmten Artikel zu erhalten, reicht folgender 3-Zeiler:

1:require_once "db_config.php";
2: $artikel=DB_DataObject::factory("Artikel");
3: $artikel->get("artikel_id",1);
Zeile 1: Datenbank-Zugriff konfigurieren
Zeile 2: Tabelle Artikel ansprechen
Zeile 3: Datensatz mit artikel_id = 1 abrufen

Da wir in diesem speziellen Fall artikel_id der Primary Key ist, reicht auch

der Aufruf: $artikel->get(1);

In diesem Fall hätten wir unseren Artikel in $artikel gespeichert und könnten auf die einzelnen Daten zugreifen. Hier sind zwei Arten möglich:

echo "$artikel->artikel_id";
direkt auf das Attribut zugreifen
echo $artikel->getartikel_id;
die spezielle methode benutzen

Jetzt wo wir wissen, wie man auf die Daten eines Datensatzes zugreifen kann, wollen wir diese natürlich auch manipulieren. Stellen Sie sich vor der Preis des Artikels hätte sich geändert:

1: $artikel->preis=6.0;
2: $artikel->update();
Zeile 1: neuen preis setzen
Zeile 2: Datensatz modifizieren
Sollte allerdings ein neuer Artikel in Sortiment aufgenommen werden:
1: $artikel = DB_DataObject::factory("artikel")
2: $artikel->beschreibung="Fussball";
3: $artikel->preis=10.95
4: $artikel->anzahl_in_lager=10;
5: $artikel->insert();
Zeile 1: auf artikel-tabelle zugreifen
Zeile 2-4: Daten setzen
Zeile 5: Daten schreiben

Man kann ist allerdings nicht auf den primary key beschränkt, um Datensätze zu suchen. Man kann jegliche Werte setzen:

1: $artikel = DB_DataObject:factory("artikel")
2: $artikel->anzahl_in_lage=5;
3: $anz_num = $artikel->find();

Zeile1: Tabelle artikel auswählen

Zeile2: Query-Daten setzen

Zeile3: Query ausführen. Die vorher gesehene get Methode ist eine Zusammenfassung von Zeile 2 und 3. Sobald man allerdings mehr als eine Spalte setzen will, muss man mit find() arbeiten. Diese Art der Abfrage lässt es auch zu, dass mehrere Ergebnisse auftreten können. Die Anzahl der Resultierenden Datensätze wird von find() geliefert.

Über:
4: $result = new Array();
5: while ($artikel->fetch())
5: { $result[] = $artikel->clone();}

Zeile 5: mit fetch() wird der nächste (bzw erste) Datensatz in $atrikel geladen!

sobald kein Datensatz mehr vorhanden ist, wird false geliefert

Jetzt wo Sie mittlerweile ein Gefühl für das Umgehen mit DataObject bekommen haben, hier nun ein Beispiel, dass weitere Funktionalitäten beinhalet:

$artikel=DB_DataObject::factory("artikel");
// alle Artikel, von denen mehr als 10 im Lager sind
$artikel->whereAdd("anzahl_in_lager>=10");
$artikel->whereAdd("anzahl_in_lager>=10","or");
// offeset 1 maximal 3 Ergebnisse
$artikel->limit(1,3);
// select count aufrufen
$count = $artikel->count();
// sortieren nach beschreibung absteigend und dann anzahl in lager aufsteigen! asc ist default
$artikel->orderBy("bezeichnung asc");
$anz_lines = $artikel->find();
while ($artikel->fetch())
{
 echo "$artikel->bezeichnung<br>";
}

Mit Hilfe der whereAdd-Methode kann man das Query weiter einschränken. Standard-mäßig werden alle Teile mit AND verbunden! “OR” kann man angeben!

Mit Limit kann man die Anzahl der Ergebnisse beschränken! Wenn man 2 Argumente angibt, ist der erste Offset der zweite Anzahl.

Mit count() kann man abfragen wie viele ergebnisse das query liefert.

Mit orderBy(column “asc|desc]”) kann man die Sortierung setzen.

Mit find() wird das query ausgeführt.

Mit fetch() werden die einzelnen Datensätze abgerufen.

$artikel=DB_DataObject::factory(”artikel”);

$artikel->groupBy("anzahl_in_lager");
$artikel->selectAdd("count(*) as grp_count");
$artikel->find();
Zeile2: Gruppierungen sind auch möglich
Zeile3: Jegliche Art von Zusatzspalte kann mit selectAdd() hinzugefügt werden

Bisher haben wir uns lediglich innerhalb einer Tabelle bewegt. Wie sieht das allerdings mit Beziehungen zu anderen Tabellen und Joins aus? Auch dafür gibt es eine Lösung.

Man muss eine Datei database_links.ini anlegen, in denen beschrieben ist, wie die Tabellen miteinander verknüpft sind.(welche fremdschlüssel zeigen aus andere Tabellen) In unserem Fall würde diese Datei folgendermaßen aussehen:

[bestellung]
user_id = user:user_id
[bestellung_artikel]
bestellung_id = bestellung:bestellung_id
artikel_id = artikel:artikel_id

Sollten fremdschlüssel in der tabelle enthalten sein, kann man mit der Methode getLinks() eine Verknüpfung zwischen beiden Tabellen schaffen:

$bestellung = DB_DataObject::factory("bestellung");
$bestellung->get("bestellung_id",1000);
// automatisch verbindung über foreignkeys zu den entsprechenden Tabellen
// schaffen. über $local->_foreignkey kann ma auf die entsprechende Tabelle   //zugreifen
$bestellung->getLinks();
echo "bestellung-Datum:$bestellung->datum<br>";
echo "{$bestellung->_user_id->name} {$bestellung->_user_id->email}<br>";

getLinks lädt allerdings alle fremd-tabellen! wenn man nur eine bestimmte benötigt kann man, dies auch mit getLink(”foreign_key”) bewerkstelligen. Mit der gleichen Methode kann man auch manuell, ohne links.ini foreign-tables nachladen

getLink(localkey,foreigntable,foreignkey)
Wenn eine einfache Verbindung nich mehr reicht, muss man auf Joins zurückgreifen.

Dafür gibt es die Methode joinAdd()

Dazu nehme man 2 Tabellen-Instanzen, setze die gewünschte Kriterien und verbinde diese mit joinAdd(). JoinAdd vrbindet dann anhand der in der links.ini angegebenen beziehungen die tabellen via join. In dem Beispiel unten zu sehen: Ausgabe aller Bestellungen in denen der Artikel mit der ID 1000 auftaucht! Dazu soll der Artikel-Name abrufbar sein,…(siehe selectAdd())

$artikel = DB_DataObject::factory("artikel");
$artikel->whereAdd("artikel_id<=1000");
$best_art = DB_DataObject::factory("bestellung_artikel");
$best_art->joinAdd($artikel);
$bestellung = DB_DataObject::factory("bestellung");
$bestellung->joinAdd($best_art);
$bestellung->selectAdd("artikel.bezeichnung as art_name");
$bestellung->find();
while ($bestellung->fetch())
{
 $bestellung->getLinks();
 echo "Besteller: {$bestellung->_user_id->name} Bestellung-Datum:$bestellung->datum $bestellung->art_name<br>";
}

Jetzt das wichtigste schlechthin. Will man in die Wrapper nun Geschäftslogik integrieren kann man dies indem man einfach seine Methoden unterhalb des ###autogen ende einfügt. Dort kann man auch Methoden zum Zugriff auf die Tabellen-Attribute überschreiben!!

Wenn sich die Tabellen struktur ändern sollte, kann man mit createTables die Wrapper auf den neusten Stand bringen, ohne die Geschäftslogik zu löschen.

Wer den SQL-Builder nicht nutzen möchte, kann queries auch für die Methode query(”..”) abschicken! dies kann mitunter bei komplizierten verstrikungen sinnvoll sein, so dass die Übersicht gewährt bleibt!

das wars
Trigger emulieren, Methoden überschreiben
function delete()
{ if ($GLOBALS['user']->checkRight(MAY_DELETE_HERE)) {
$this->cleanUpStuff();
return parent::delete();
}
return false;
}

Hier zwei kleine Beispiele:

$user = DataObject::factory("user");
$user->get("1100"); // primary key
$user = DataObject::factory("user");
$user->vorname="Thomas";
$user->find(); // searches all user with vorname Thomas