WICHTIG: Diese Anleitung richtet sich an fortgeschrittene Entwickler und soll Ihnen ein neues Hilfsmittel erklären. Es handelt sich um kein fertiges Modul!!!
Hey,
ich arbeite zur Zeit bei purpleleaves.de als Chef-Entwickler.
Wir benutzen modified eCommerce Shopsoftware.
Da wir ständig Custom-Lösungen benötigen bin ich also recht tief in xtc:m eingestiegen.
Was mich anfangs sehr gestört hat, war die fehlende Objektorientierung und vor allem die fehlende objektgesteuerte Datenbankabwicklung. Da in xtc:m bereits ein templatingsystem vorhanden ist kann man sagen, dass mir das M aus MVC (
http://de.wikipedia.org/wiki/Model_View_Controller) gefehlt hat.
Ich arbeite bei mir bereits live mit dem System, das ich euch nun vorstelle.
Vielleicht ist es ja genau das, was xtc:m einen Schritt weiter bringen kann.
Ursprünglich von Kevin Skoglung auf Lynda.com´s "PHP - BEYOND THE BASICS" (absolut empfehlenswert -
http://www.lynda.com/PHP-tutorials/php-with-OOP-beyond-the-basics/653-2.html) präsentiert, habe ich es an die Eigenarten von xtc:m angepasst. Ich werde nicht wirklich erklären wie es funktioniert, bzw. was genau welche Funktion macht. Solltet ihr euch mehr damit befassen wollen idealerweise obig angepriesenen Online-Video-Kurs anschauen. Den gibt es ebenfalls auf diverse dubiosen Webseiten kostenlos.
Das framework ging vor allem davon aus, dass eine Klasse immer eine "id" hat, die immer automatisch gefüllt wird. Das ist in xtc:m leider selten der Fall, oft heißt sie anders, z.b. "reviews_id", aber manchmal wird diese id sogar "per hand" gesetzt.
Diesen Teil habe ich angepasst, sodass diese Fälle abgedeckt worden sein sollten.
Ich bin nun auf eure Reaktion gespannt, ob ihr es ebenfalls so genial wie ich finde, und ob und wie man das framework nun komplett integriert. Ich habe bisher nur einige wenigen Klassen erstellt. Es gibt noch viel zu tun.
Interessant ist an dieser Stelle auch Late Static Binding. Die Klassenfunktionen lassen sich von der "database" Klasse bisher (also pre 5.3) nicht vererben. D.h. die DB-Funktionen sind in jeder Klasse erneut enthalten. Dort wird zwischen "own methods" und "common database methods" unterschieden. Zukünftig (mit lsb) sollten die nur noch in der database-klasse sein.
Außerdem gibt es eine Session-Klasse, die aber bisher von mir nur minimal genutzt wird (is_logged_in()) sowie die customers_id des eingeloggten users lässt sich abfragen.
Hier eine Beispiel Klasse customers.php
<?php// If it's going to need the database, then it's // probably smart to require it before we start.require_once(LIB_PATH
.DS
.'database.php');class Customer
extends DatabaseObject
{ protected static
$table_name="customers"; protected static
$prim_key="customers_id"; protected static
$db_fields = array( 'customers_id', 'customers_cid', 'customers_vat_id', 'customers_vat_id_status', 'customers_warning', 'customers_status', 'customers_gender', 'customers_firstname', 'customers_lastname', 'customers_dob', 'customers_email_address', 'customers_default_address_id', 'customers_telephone', 'customers_fax', 'customers_password', 'customers_newsletter', 'customers_newsletter_mode', 'member_flag', 'delete_user', 'account_type', 'password_request_key', 'payment_unallowed', 'shipping_unallowed', 'refferers_id', 'customers_date_added', 'customers_last_modified'); public $customers_id; public $customers_cid; public $customers_vat_id; public $customers_vat_id_status; public $customers_warning; public $customers_status; public $customers_gender; public $customers_firstname; public $customers_lastname; public $customers_dob; public $customers_email_address; public $customers_default_adress_id; public $customers_telephone; public $customers_fax; public $customers_password; public $customers_newsletter; public $customers_newsletter_mode; public $member_flag; public $delete_user; public $account_type; public $password_request_key; public $payment_unallowed; public $shipping_unallowed; public $refferers_id; public $customers_date_added; public $customers_last_modified; // own methods public function is_potential
() { return Kundengruppe
::find_by_customers_id($this->customers_id); } public static
function find_by_email
($email) { $result_array = self::find_by_sql("SELECT * FROM ".self::$table_name."
WHERE customers_email_address='{$email}' LIMIT 1"); return !empty($result_array) ?
array_shift($result_array) : false; } public function login
() { $email = $this->customers_email_address; $password = $this->customers_password; if($customer = Customer
::find_by_email($email)) { $check_customer = xtc_db_fetch_array
($check_customer_query); if (SESSION_RECREATE
== 'True') { xtc_session_recreate
(); } # TODO Länder CHECK in OOP
#$check_country_query = xtc_db_query("select entry_country_id, entry_zone_id from ".TABLE_ADDRESS_BOOK." where customers_id = '".(int) $check_customer['customers_id']."' and address_book_id = '".$check_customer['customers_default_address_id']."'");
#$check_country = xtc_db_fetch_array($check_country_query);
$_SESSION['customer_gender'] = $this->customers_gender; $_SESSION['customer_first_name'] = $this->customers_firstname; $_SESSION['customer_last_name'] = $this->customers_lastname; $_SESSION['customer_id'] = $this->customers_id; $_SESSION['customer_vat_id'] = $this->customers_vat_id; $_SESSION['customer_default_address_id'] = $this->customers_default_address_id; $_SESSION['customer_country_id'] = $this->entry_country_id; $_SESSION['customer_zone_id'] = $this->entry_zone_id; $datetime = date('Y-m-d H:i:s', time()); # TODO Customers_info in OOP
xtc_db_query
("update ".TABLE_CUSTOMERS_INFO
." SET customers_info_date_of_last_logon = now(), customers_info_number_of_logons = customers_info_number_of_logons+1 WHERE customers_info_id = '".(int
) $this->customers_id."'"); xtc_write_user_info
((int
) $this->customers_id); // restore cart contents $_SESSION['cart']->restore_contents(); if (is_object($econda)) $econda->_loginUser
(); if (isset($_SESSION['REFERER']) && !empty($_SESSION['REFERER'])) { xtc_redirect
(xtc_href_link
($_SESSION['REFERER'])); } elseif ($_SESSION['cart']->count_contents() > 0) { xtc_redirect
(xtc_href_link
(FILENAME_SHOPPING_CART
),'NONSSL'); } else { xtc_redirect
(xtc_href_link
(FILENAME_DEFAULT
),'NONSSL'); } #}
} else { # FEHLER
} } // Common Database Methods public static
function find_all
() { return self::find_by_sql("SELECT * FROM ".self::$table_name); } public static
function find_by_id
($id=0) { $result_array = self::find_by_sql("SELECT * FROM ".self::$table_name." WHERE " . self::$prim_key . "='{$id}' LIMIT 1"); return !empty($result_array) ?
array_shift($result_array) : false; } public static
function find_by_sql
($sql="") { global $database; $result_set = $database->query($sql); $object_array = array(); while ($row = $database->fetch_array($result_set)) { $object_array[] = self::instantiate($row); } return $object_array; } private static
function instantiate
($record) { // Could check that $record exists and is an array $object = new self; // Simple, long-form approach: // $object->id = $record['id']; // $object->username = $record['username']; // $object->password = $record['password']; // $object->first_name = $record['first_name']; // $object->last_name = $record['last_name']; // More dynamic, short-form approach: foreach($record as $attribute=>$value){ if($object->has_attribute($attribute)) { $object->$attribute = $value; } } return $object; } private function has_attribute
($attribute) { // We don't care about the value, we just want to know if the key exists // Will return true or false return array_key_exists($attribute, $this->attributes()); } protected function attributes
() { // return an array of attribute names and their values $attributes = array(); foreach(self::$db_fields as $field) { if(property_exists
($this, $field)) { $attributes[$field] = $this->$field; } } return $attributes; } protected function sanitized_attributes
() { global $database; $clean_attributes = array(); // sanitize the values before submitting // Note: does not alter the actual value of each attribute foreach($this->attributes() as $key => $value){ $clean_attributes[$key] = $database->escape_value($value); } return $clean_attributes; } public function save
() { // A new record won't have an id yet. // KORREKTUR: Checken, falls id gesetzt, ob auch eintrag zu id existiert. if(isset($this->{self::$prim_key})) { if($xx = self::find_by_id($this->{self::$prim_key})) { return $this->update(); } } return $this->create(); } public function create
() { global $database; // Don't forget your SQL syntax and good habits: // - INSERT INTO table (key, key) VALUES ('value', 'value') // - single-quotes around all values // - escape all values to prevent SQL injection $attributes = $this->sanitized_attributes(); $sql = "INSERT INTO ".self::$table_name." ("; $sql .= join(", ", array_keys($attributes)); $sql .= ") VALUES ('"; $sql .= join("', '", array_values($attributes)); $sql .= "')"; if($database->query($sql)) { $this->{self::$prim_key} = $database->insert_id(); return true; } else { return false; } } public function update
() { global $database; // Don't forget your SQL syntax and good habits: // - UPDATE table SET key='value', key='value' WHERE condition // - single-quotes around all values // - escape all values to prevent SQL injection $attributes = $this->sanitized_attributes(); $attribute_pairs = array(); foreach($attributes as $key => $value) { $attribute_pairs[] = "{$key}='{$value}'"; } $sql = "UPDATE ".self::$table_name." SET "; $sql .= join(", ", $attribute_pairs); $sql .= " WHERE " . self::$prim_key . "=". $database->escape_value($this->{self::$prim_key}); $database->query($sql); return ($database->affected_rows() == 1) ?
true : false; } public function delete
() { global $database; // Don't forget your SQL syntax and good habits: // - DELETE FROM table WHERE condition LIMIT 1 // - escape all values to prevent SQL injection // - use LIMIT 1 $sql = "DELETE FROM ".self::$table_name; $sql .= " WHERE {$this->prim_key}=". $database->escape_value($this->{$this->prim_key}); $sql .= " LIMIT 1"; $database->query($sql); return ($database->affected_rows() == 1) ?
true : false; // NB: After deleting, the instance of User still // exists, even though the database entry does not. // This can be useful, as in: // echo $user->first_name . " was deleted"; // but, for example, we can't call $user->update() // after calling $user->delete(). } } ?> Mit den "// own methods" werdet ihr nichts anfangen können, die kann man sich eben selbst bei Bedarf hinzufügen. z.B. wie oben find_by_email
Nun aber zum interessante Teil: Wie wendet man das framework an?
Zuerst muss man die initialize.php einbinden und editieren.
Diese befindet sich in dem von mir bereitgestellten Ordner "tk-includes".
Einmalig muss man hier nun (wie auch in der "includes/configure.php" von xtc:m) den absoluten pfad zum html-verzeichnis des Webservers eintragen.
Ich empfehle ein "require_once" aus der "includes/application_top.php" heraus (einfach als letztes hinzufügen), damit man jederzeit auf das framework zugreifen kann.
Da das framework auch standalone funktioniert gibt es auch hier eine "config.php", in der man eine Datenbankverbindung herstellen könnte etc., außerdem werden hier die DB-Klassen eingebunden, sowie benötigte Pfade erstellt. Allerdings ist das nicht zwingend per Hand nötig, weil es dafür eine autoloader Funktion(in der "functions.php") gibt.
Kritisch ist allerdings, falls eine Klasse bereits existiert und durch den autoloader neu deklariert werden soll. Deshalb gibt es auch keine "product.php" sondern eine "new_product.php" (weil die Klasse Product manchmal bereits existiert)
Hier ein Anwendungsbeispiel (Demo unter
http://www.purpleleaves.de/public):
Ich benutze das Framework als stand-alone in diesem Fall, da ich mir den Templating-Teil für das Beispiel sparen wollte, ist aber problemlos möglich.
Es wird ein customer (Datenbank customers) ausgewählt und bearbeitet. Beachtet bitte, wie man keine sql-Befehle mehr schreiben muss und mit extrem wenig Code auskommt. Ich habe glaube ich mehr kommentiert als wirklich geschrieben. Der Code müsste nur minimal angepasst werden, damit unbegrenzt viele customer angezeigt werden würden, die ebenfalls bearbeitbar wären.
<?php require_once("../tk-includes/initialize.php");// Simulation. in xtc:m wäre, falls ein benutzer eingeloggt ist, // in der session die variable "customer_id" gesetzt.// die Klasse Session (also das Objekt $session) fängt diese nun id ab und // speichert sie in user_id $_SESSION['customer_id'] = '215'; $session->check_login(); // Falls der Benutzer bearbeitet werden soll - Das Formular abgeschickt wurde if(isset($_POST['action']) and
$_POST['action'] == 'edit_customer') { // Prüfen, ob es zur eingegebenen customers_id bereits einen Customer gibt if($customer = Customer
::find_by_id($_POST['customers_id'])) { // tue nichts, bereits erfolgreich gefunden } else { // falls nicht, erzeuge einen neuen $customer = new Customer
(); } // hier beginnt die Magie! // Da $customer ein array-artiges Objekt ist, kann ich es mit foreach durchlaufen// $key gibt den key, also den Namen der aktuellen Klassenvariablen an, value den entsprechenden Wert. foreach($customer as $key => $value) {// Die ID (wie auch immer sie heißt) soll nicht verändert werden können, überspringe also if($key == Customer
::$prim_key) { continue; }// Prüft in einer Schleife, ob für Post-Variablen mit //name = klassenvariable gesetzt und nicht leer sind if(isset($_POST[$key]) and
$_POST[$key] != '') { // füge diese Post-Variablen nun dem Objekt hinzu. $customer->$key = $_POST[$key]; }// Dieser Befehl übernimmt für euch die Komplette Datenbankabwicklung!// Existiert zum primary-key (der id, wie auch immer sie heißt) bereits ein Objekt, // wird der Datenbankeintrag geupdated// ansonten wird ein neuer erzeugt. $customer->save(); } redirect_to
($_SERVER['PHP_SELF']); }// statischer Aufruf der Klasse Customer// Zurückgegeben wird in diesem Fall ein Array von Objekten// In einer If-Abfrage geklammert wird false zurückgegeben, falls kein Objekt gefunden wird if($customer = Customer
::find_by_id($session->user_id)) { ?> <h2>Eingeloggten Customer bearbeiten</h2>
<form action='
<?php echo $_SERVER['PHP_SELF']; ?>' method='post'>
<table>
<?php $x = 0; foreach($customer as $key => $value) { // zweispaltige darstellung if($x % 2 == 0) { echo "<tr>"; } echo "<td>"; // Aktuelle Klassenvariable echo $key . ":"; echo "</td><td>";// Erzeuge ein Inputfeld dass als name den namen der Klassenvariablen hat,// als Wert deren Wert falls gesetzt echo "<input type='text' name='".$key."' ";// Falls $key = id (wie auch immer sie heißt), dann input feld readonly schalten if($key == Customer
::$prim_key) { echo "readonly='readonly'"; } echo "value='".$value."' />"; echo "</td>"; // zweispaltige darstellung if($x % 2 == 1) { echo "</tr>"; } $x++; } ?> <tr><td colspan='2'><input type='submit' /></td></tr>
</table>
<input type='hidden' name='action' value='edit_customer' />
</form>
<?php } else { echo "Benutzer mit customers_id " . $session->user_id . " nicht vorhanden"; } ?> Ich hoffe der Code ist mit den Kommentaren allen verständlich - spätestens mit der Online-Demo
Das ganze lässt sich problem auch mit dem Templatingsystem verbinden.
Allerdings wird mir die Schreiberei gerade zu viel um noch so ein Beispiel zu produzieren.
Ich habe euch erstmal nur wenige Klasse bereitgestellt. Ich habe schon einige mehr vorbereitet, allerdings noch nicht komplett und getestet. Da viele DB-Tabellen purpleleaves-spezifische(meine Arbeit) Änderungen erfahren haben will ich das lieber noch einfach in Ruhe durchgehen.
Was noch zu tun ist:
- Session->Klasse ausbauen!
- Alle Datenbank Tabellen als Klasse erzeugen
- Late static binding recherchieren und ggf. umsetzen.
Framework unter:
http://purpleleaves.de/public/php-framework.zipIst schon spät und ich recht müde, hoffe ich hab jetzt keine Fehler unnötig eingebaut und freue mich auf euer Feedback und Fragen.
Linkback: https://www.modified-shop.org/forum/index.php?topic=21012.0