Mit CakePHP in 30 Minuten Tags zu einer Tabelle hinzufügen
Im Web 2.0 geht nichts mehr ohne Tags. Und ohne ein Rapid Development Framework kann das ganz schön haarig werden. Zum Glück springt CakePHP mit leuchtenden Augen (oder waren es die Augen des Entwicklers?) in die Bresche und begeistert mit sehr einfachen Umsetzung.
Mit der folgenden Anleitung bekommt ein beliebiges Model die Möglichkeit Tags zu speichern, inklusive Has and belongs to many Beziehung.
Anforderungen
Alle Einträge der Datenbank Posts (z.B.) sollen mit einer beliebigen Anzahl Tags beschrieben werden können. Die Tags werden in einer eigenen Datenbank gespeichert. Die Relationen zu den Einträgen wird in einer weiteren Datenbank gespeichert.
Zur komfortablen Eingabe sollen die Tags mit Komma getrennt eingegeben und bearbeitet werden. In der Ausgabe sollen alle Tags als Array zur Verfügung stehen.
Die Datenbanken
Eine Datenbank ist bereits vorhanden welche alle Posts speichert.
Die Tags bekommen eine eigene Datenbank mit den Spalten id (primary, auto_increment) und name.
Um CakePHP die Magie zu ermöglichen muss die dritte Tabelle, zum speichern der Relationen, tags_posts heißen. Sie beinhaltet die Spalten tag_id und post_id.
Das Model
Das Model der Tags ist denkbar einfach:
<?
class Tag extends AppModel {
[tab]var $name = 'Tag';
[tab]
[tab]$hasAndBelongsToMany = array('Post');
}
?>
Die Schönheit ist in der letzten Zeile: $hasAndBelongsToMany = array('Post'); hier wird dem Model erklärt das es zum Model Post gehört und dementsprechend wird die Datenbank tags_posts erwartet.
Das Model Post kriegt die gleiche Informationen, in umgekehrte Richtung:
$hasAndBelongsToMany = array('Tag');
Speichern von Tags
Jetzt wird es spannend! Die Tags sollen kommaspeariert eingegeben werden und beim speichern automatisch auseinander geschnitten werden. Dazu wird im View der Funktionen add und edit der Posts ein neues Element angelegt. Ich nenne es temp_tags.
echo $form->input('temp_tags', array(
'label' => 'Tags (mit Komma trennen)',
));
Die Verarbeitung des Feldes erfolgt im Model von Post und zwar in der Funktion beforeSave(). Diese wird ausgeführt bevor ein Datensatz gespeichert wird.
function beforeSave() {
// hier gehts ab!
}
Als erstes wird überprüft ob das Feld temp_tags überhaupt existiert. Ist dies der Fall wird in der Variable $tags ein Array gespeichert, aufgeteilt nach Komma. Anschließend wird die Variable temp_tags aus dem Datensatz gelöscht:
if(isset($this->data[$this->name]['temp_tags'])) {
$tags = explode(",", $this->data[$this->name]['temp_tags']);
unset($this->data[$this->name]['temp_tags']);
Ich habe auf die Daten über $this->data[$this->name] zugegriffen. Das ermöglicht die Kopie der gesamten Funktion in das nächste Projekt ohne sich noch einmal Gedanken über die Namen machen zu müssen.
Das Array $tags wird jetzt Element für Element untersucht. Dabei sollen keine Leerzeichen stören (trim).
foreach($tags as $tag) {
$tag = trim($tag);
}
Der erste Schritt besteht darin herauszufinden ob der Tag schon vorhanden ist und wenn ja, für die neue Relation die ID herauszubekommen.
$id = $this->Tag->findByName($tag);
Wenn das nicht geklappt hat wird das Model Tag darauf vorbereitet einen neuen Eintrag zu speichern (create) und muss es dann auch sofort tun.
if(!$id['Tag']['id']) {
$this->Tag->create();
$this->Tag->save(array('name' => $tag));
$id['Tag']['id'] = $this->Tag->id;
}
In Normalfall steht jetzt aufjedenfall in der Variable $id['Tag']['id'] die ID des Tags. Entweder weil es ausgelesen wurde, oder weil ein neuer Eintrag erzeugt wurde.
Das wird jetzt dem Model mitgeteilt
$this->data['Tag']['Tag'][] = $id['Tag']['id'];
Fehlt nur noch die ggf. definierte Elternmethode und eine positive Rückmeldung.
parent::beforeSave();
return true;
Fertig!
}
Das Auslesen im View geschieht von alleine durch die habtm Beziehung. Eine reine Freude dieser PHP Kuchen. Nicht vergessen die Daten zu überprüfen und zu sichern, wenn dieser Schnippsel auf einer öffentlichen Seite zum Einsatz kommt!
Hier noch mal der gesamte Quelltext der Funktion beforeSave():
function beforeSave() { //Kommaseparierte Liste von Tags als Tags speichern if(isset($this->data[$this->name]['temp_tags'])) { $tags = explode(",", $this->data[$this->name]['temp_tags']); unset($this->data[$this->name]['temp_tags']); foreach($tags as $tag) { $tag = trim($tag); //ID vorhanden oder nicht? $id = $this->Tag->findByName($tag); if(!$id['Tag']['id']) { //Tag neu anlegen $this->Tag->create(); $this->Tag->save(array('name' => $tag)); $id['Tag']['id'] = $this->Tag->id; } $this->data['Tag']['Tag'][] = $id['Tag']['id']; } } parent::beforeSave(); return true; }
kleine korrektur noch:
du solltest nicht nur trim() verwenden, sondern danach noch auf !empty() prüfen
wenn jemand “ss,ff, ,dfdf” etc postet, würde der 3. tag ein “leerer Tag” sein, was ja wenig sinn macht
aber sonst super!
oh, und hab ich was verpasst? oder kapier ichs nich?
wie läufts denn mit dem “Entfernen” von tags? geht das nicht mehr über edit? muss man dann manuell delete des joint table triggern?
weil ich hier keine differenzbildung sehe im code
on edit:
$before (db)
$after (post)
if (!in_array(after)) && in_array(before)) -> delete
Hi Mark,
danke für den Hinweis! Habe mich schon über das leere Tag gewundert. Absolut richtig. Ich werde es gleich hinzufügen.
Das Löschen funktioniert über Edit tadellos, habe es gerade ausprobiert. Cake prüft anscheinend beim aktualisieren die angegebenen Verbindungen und entfernt sie ggf.
Das ist magisch!
tatsächlich? unglaublich…muss ich das doch gleich ma nach-basteln^^
Viel Spaß! Ich habe die Ursache gefunden. unique muss auf true gesetzt sein:
“unique: If true (default value) cake will first delete existing relationship records in the foreign keys table before inserting new ones, when updating a record. So existing associations need to be passed again when updating. ”
http://book.cakephp.org/view/83/hasAndBelongsToMany-HABTM
Klasse Beitrag, habe so einiges für mich entdeckt!
Danke und schöne Grüße
Alex
Danke für diesen Artikel! Finde die Infos hier eigentlich sehr wichtig.Die werden mir natürlich sehr helfen.
Danke für das Lob!! Ich habe auch noch eine Anmerkung:
Statt der Funktion explode() ist es besser die Cake-interne Funktion tokenize zu benutzen:
String::tokenize($ein_string);
Hier gibts mehr Infos: http://book.cakephp.org/view/574/tokenize (auch sehr praktisch für CSV Dateien)
nice
didnt know about tokenize() yet – just changed my explode() to it
thanks!
Soweit alles super, aber du solltest besser von “Tabellen” anstatt von “Datenbanken” sprechen das verwirrt einen Newbee nicht so ganz… auch wenn man für den Zweck bestimmt separate Datenbanken verwenden kann ist bei dir wohl die Verwendung von Tabellen gemeint (tag_id , post_id).