Sortierung eines Dropdownwidgets in Symfony ändern (Doctrine)

Frameworks sind toll. Man kann in vielen tausend Zeilen das tun, wozu man früher nur ein paar Statements gebraucht hat… Oder war es umgekehrt? Hier etwas Symfony spezifisches, das mir ein paar gefärbte Haare gekostet hat, weil ich sie mir ausreissen musste.

Das Problem

Es sei gegeben eine dreistufige Hierarchie: Kategorien → Fächer → Kurse. Es sind ganz saubere 1:n-Beziehungen: Jedes Fach gehört zu einer Kategorie und ein Kurs ist genau ein durchgeführtes Fach.

Folgt man dem (wirklich guten) Symfony-Tutorial, so wird an Tag 3 ganz unten gezeigt, wie aus der Tabellendefinition automatisch ein Formular generiert wird. Clevererweise werden für 1:n-Beziehungen Dropdowns generiert.

Wenn ein neuer Kurs generiert wird, so soll man angeben können, was für ein Fach er unterrichtet. Standardmässig werden Fächer wie folgt angezeigt:

  1. Ein Kategorienkürzel („I“ für Informatik, „A“ für Ausdruckstanz (haha), …)
  2. Einer „Nummer“ (normalerweise „01“ bis „15“ oder so)
  3. Dem Namen des Fachs

Das Fach braucht für seine Darstellung also das Kürzel aus dem Kategorien-Model (bin ich der Einzige der bei Models immer grinsen muss?).

Das generierte Dropdown zeigt die Namen der Fächer falsch und alles Kreuz und Rüben durcheinander an. Ha, nun ist guter Rat teuer.

dropdownstart

Lösung

Namen der Fächer

Das geht noch relativ einfach: Jedes Model besitzt eine __toString() Methode, die überschrieben werden kann:

# File: lib/model/doctrine/modelnamefach.class.php
[...]
  public function __toString() {
        return sprintf('%s %s %s', $this->FreifachNgKategorie->getKuerzel(), $this->getNummer(), $this->getName());
  }
[...]

Und tatsächlich:

dropdownbefore

Sortierung

Ich hätte mir eigentlich gewünscht, für jedes Model eine Defaultsortierung angeben zu können. Aber da nicht Weihnachten ist, werden meine Wünsche nicht erfüllt. Da hilft nur Quellcodestudium und glückliches Zusammenpuzzeln der Doctrine Anleitung, dem Symfony-Doctrine Buch und der API-Dokumentation (scrollen bis Doctrine): Beispiele gibts leider fast keine.

Nungut: Das Formular wird automatisch generiert (lib/form/doctrine/base/), darum sollte hier nur wer was ändern, der per Stunde bezahlt wird und das nach jeder Generierung wieder neu tun kann :). Für uns Andere müssen wir das Choice in der Kinderklasse hacken und eine table_method reinbringen, die die Einträge nach unserem Gusto liefert:

# File: lib/form/doctrine/modelnamekurs.class.php
[...]
  public function configure()  {
      $this->setWidget('freifach_id', new sfWidgetFormDoctrineChoice(array('model' => 'FreifachNgFreifach', 'table_method' => 'retrieveFreifachOrderedByCategory')));
  }
[...]

Diese table_method muss auch noch codiert werden. Und das in der Tabellenklasse der Fächer (dochdoch, wenn man sich das überlegt ist es ganz logisch *irres_grinsen*).

# File: lib/model/freifachmodelnameTable.class.php
[...]
    function retrieveFreifachOrderedByCategory() {
        $q=Doctrine_Query::create()->from('FreifachNgFreifach f')->innerJoin('f.FreifachNgKategorie k')->orderby('k.kuerzel, f.nummer, f.name');
        //echo $q->getSql();
        return $q;
    }
[...]

Toll, und dann klappts auch mit dem Dropdown:

dropdownordered

Automatisches und mehrfaches submitten von Formularen

Wirre Gedanken

FormularDieser Beitrag ist dem Titz gewidmet. Dem Titz, der sich aufgeregt hat :pirate-grumble: , obwohl es gar nicht nötig gewesen wäre und der sich nun mit einer gewissen Teilnehmerredundanz anfreunden muss. Hauptsache ist doch, das PHP fliesst und die Variablen bleiben sauber… Und ein Bierchen würde ich auch noch springen lassen :bier: …

Eine wichtige Frage zu Beginn: Wieso sollten wir denn automatisch und mehrfach Formulare submitten wollen? Hmm, um den Titz zu ärgern? Weil wir es können? Weil man manchmal tun muss, was man tun muss? Weil man seine 84 Kinder an einem elektronischen Fussballturnier anmelden will :laola: (hat eigentlich schon jemand bemerkt, dass ich neue Smilies und unglaublich Freude daran habe?)? Oder weil man über einen Wettbewerb gestolpert ist, der ebendies nicht verbietet (also das Mehrfachsubmitten, nicht das Kinderanmelden oder die neuen Smilies) und der kein Captcha hat (vielleicht, weil es der Titz vergessen hat)?

Nachtrag 31.07.2008: Schenken mir doch so unglaublich nette Mitarbeiter einer Versicherung heute morgen am Bahnhof einen Müsliriegel. Und auf diesem eher gesunden Teil hat es, ja rate, oh wissbegieriges Volk, einen Wettbewerb. Ob der Titz wohl am Abend für eine andere Firma weitercoded? Ich werde mich auf jeden Fall während der Zugfahrt mal damit befassen :computer: .

Vorgehen

Erster Schritt: Selenium IDE

Man könnte nun wie wild losprogrammieren, oder aber einen einfacheren Weg wählen. Ein guter Startpunkt für automatisiertes Browsen generell ist die Selenium IDE. Dieses geniale Teil für den Firefox zeichnet wie ein Macrorecorder alles auf, was im Browser gemacht wird. Hat man die Teilnahme beim Wettbewerb einmal so aufgezeichnet, so müssen nur noch die click’s, die das Form absenden, durch clickAndWait’s ersetzt werden, damit vor dem Weiterausfüllen (bei mehrseitigen Formularen) auf die neue Seite gewartet wird. Es empfiehlt sich, eine Abschlussüberprüfung als letzten Schritt hinzuzufügen, um um kontrollieren zu können, ob die Kinder erfolgreich angemeldet wurden (markieren des „Dankeblabla“, dann rechte Maustaste und assertTextPresent).

Ein Wettbewerb über 2 Seiten. Aufgezeichnet und bearbeitet mit der Selenium IDE.

Ein Wettbewerb über 2 Seiten. Aufgezeichnet und bearbeitet mit der Selenium IDE.

Diesen Testcase kann man nun abspeichern und eigentlich immer wieder ausführen. Ein Klick reicht und Firefox rasselt alles schön durch. Den Namen leicht verändern kann man durch Editieren des Skripts…

Für den zweiten Schritt sollte man das Testscript als PHP exportieren.

Zweiter Schritt: Selenium RC

Die Selenium RC Komponente kann den Browser fernsteuern und so ferngesteuert am Wettbewerb teilnehmen. Um sie unter Linux Debian zum Laufen zu kriegen, war ein Bisschen Gemurkse notwendig.

Weiterlesen

Ein Ajax Baum der sich sein Zustand merkt

Man gönnt sich ja sonst nichts… Ich wollte für eine mit dem Symfony Framework erstellte Webanwendung eine Baumansicht. Da leichtes implemenieren langweilig ist, musste es schon ein Ajaxbaum mit Kontextmenü und allem Schnickschnack sein. Nach längerem Suchen fiel die Wahl auf Dojo, einem Toolkit mit vielen Widgets und sonstigen Spassmachern. Der Baum besitz viele Nodes, darum sollten diese dynamisch nachgeladen werden (ok, etwas Masochismus war auch dabei). Die Anwendung besteht eigentlich aus herkömmlichen Seitenwechseln, und so musste der Baum auch seinen Zustand halten können. Nundenn, nach laaaaaanger Probierephase (die Dokumentation ist nicht gerade erschlagend) und der Hilfe vom Web wurde es dennoch Realität *freu*.

Hilfreiche Websites:

Tipps generell:

  • var djConfig = {isDebug: true }; aktiviert die Debugeingaben, dojo.debug(‚Blah‘); kann für Debugausgaben benutzt werden.
  • saveExpandedIndices und restoreExpandedIndices sind methoden des erweiterten tree-Controllers! Er muss also require’d und gemixint werden (siehe unten)
  • Der Baumzustand wird in einem Cookie gespeichert, damit hat man in der Applikation nichts mehr damit zu tun.

Wie das Ganze in Symfony integriert wurde kann gerne nachgefragt werden.

Nundenn, gimme Code:

<script type="text/javascript" src="/js/dojo.js"></script>
<script type="text/javascript">
var djConfig = {isDebug: true }; // Comment if debugguing
dojo.require("dojo.widget.Tree");
dojo.require("dojo.widget.TreeSelector");
dojo.require("dojo.widget.TreeNode");
dojo.require("dojo.widget.TreeContextMenu");
dojo.require("dojo.widget.TreeLoadingController");
dojo.require("dojo.widget.TreeControllerExtension");
// Do something if a node is clicked
function modulTreeSelectFired() {
    var treeSelector = dojo.widget.manager.getWidgetById('modulTreeSelector');
    var treeNode = treeSelector.selectedNode;

    < !get a reference to the songDisplay div –>
    //var hostDiv = document.getElementById("songDisplay");

    var isFolder = treeNode['isFolder'];
    if ( !isFolder) {
       //var song = treeNode['title']
       //hostDiv.innerHTML = "You clicked on "+song;
    } else {
       //hostDiv.innerHTHML = "";
    }
    //hostDiv.style.display = "";
}

// Set up Dojo and the tree
function init() {
    var treeSelector = dojo.widget.manager.getWidgetById('modulTreeSelector');
    dojo.event.connect(treeSelector,'select','modulTreeSelectFired');

    var modulTree = dojo.widget.manager.getWidgetById('modulTree');
    dojo.event.topic.subscribe(modulTree.eventNames.collapse, "saveExpandedIndices");
    dojo.event.topic.subscribe(modulTree.eventNames.expand, "saveExpandedIndices");

    // add extensions to controller
    dojo.lang.mixin(dojo.widget.byId('modulTreeController'), dojo.widget.TreeControllerExtension.prototype);

    // Restore old state
    restoreExpandedIndices();
 }

// Save the tree state
function saveExpandedIndices() {
        // You can save this object as tree persistent state
        indices = dojo.widget.byId('modulTreeController').saveExpandedIndices(
            dojo.widget.byId('modulTree')
        );
        var flatIndices = dojo.json.serialize(indices);
        //dojo.debug(flatIndices);
        dojo.io.cookie.setCookie('modulTree/saveindices',flatIndices, 365, null, null, null);
    }

// Restore the tree state
function restoreExpandedIndices() {
        flatIndices = dojo.io.cookie.getCookie('modulTree/saveindices');
        indices = dojo.json.evalJson(flatIndices);
        //dojo.debug(flatIndices)
        if(indices) {
            dojo.widget.byId('modulTreeController').restoreExpandedIndices(dojo.widget.byId('modulTree'), indices
            );
        }
    }

// Initialize
dojo.addOnLoad(init);</script>

Der HTML-Code sollte eigentlich klar sein: Controller, Kontextmenü und Tree aufbauen (nur die erste Hierarchiestufe) und der PHP-Teil ist nach Vorgabe. Sind mehr Details gewünscht, liefere ich diese gerne nach.

Stichworte: persistent tree state, remembering tree state

Ergänzung, Juli 2008: Oky, Ihr habt recht: Gimme Code! Schwatzen kann jeder!

Hier mal den Quellcode (eher das Gerüst, bzw proof-of-concept 🙂 ) des Moduls und den Inhalt des web/js Verzeichnisses. Hilft das was? Braucht es mehr, damit es sinnvoll ist?

Geschrieben wurde es mit Symfony 1.0 und Dojo 0.4 Rev: 6258.

In der Zwischenzeit bin ich aber von dem Dojo-Ajax Zeux wieder etwas weggekommen. Ich progge für Intra- und Extranetapplikationen, und da sind zu viele Browser mit zu vielen Konfigurationen im Einsatz. Für „öffentliche“ Websites ist es sicher schön, auch wenn es (wie bei Symfony üblich) nicht voll Ajax ist, sondern halt nur Komponentenweise, was ich nicht als schön empfinde…

Ich bin zurück zum guten alten PHP-Layersmenü (Beitrag dazu folgt noch). Das fallbackt sehr schön ohne Java Script.

Unicode-Hölle: PHP, Apache, Mysql und Symfony in UTF-8

Ich will mein altes ASCII wieder!!! Unicode ist die Hölle. Vielleicht helfen folgende Dinge dem, der damit rumkämpfen will/muss:

  • Im ganzen Pfad muss die Codierung stimmen bzw. gezielt umgewandelt werden. In unserem Beispiel also Dateisystem, Datenbank, (PHP), Applikation und Browser.
  • Glaube nicht was Du siehst! Was angezeigt wird ist wieder aus einer Applikation die irgendwie encodiert.
  • Firefox: View -> Character Encoding ist Dein Freund. Damit sieht man, was der Browser denkt er erhalte und das Umschalten auf 8859-1 kann zeigen, dass man es noch nicht geschafft hat und immernoch das alte Encoding verwendet wird.
  • utf8_encode() und utf8_decode() sind böse! Wenn man die braucht stimmt etwas nicht.

Hier ein paar Hilfen und Tools.

Dateisystem:

locale -a zeigt die Codierungen. Auf Debian kann man mittels dpkg-reconfigure locales einiges umstellen. Hier mal Unicode zu haben schadet nicht.

Konvertierung mit recode:

Folgendes Kommando wandelt eine Datei in ISO88591 in Unicode um:

recode ISO-8859-1..UTF-8 DATEI

Mysql:
Um Daten in UTF-8 abzulegen, muss:

  • das charset stimmen
  • die Verbindung vom Client zum Server stimmen
  • die Datenbank eine Collation „utf8_unicode_ci“ besitzen
  • jede Tabelle eine Collation „utf8_unicode_ci“ besitzen
  • die Daten in UTF-8 abgefüllt sein

Für das Abfüllen eignet sich mysql-query-browser, da dieser schon mit utf-8 arbeiten kann, was bei der Konsole nicht immer ganz sicher ist.

  • Tools: SHOW VARIABLES LIKE ‚character%‘,damit sieht man, wie man unterwegs ist
  • set names utf8 zwingt die Verbindung zu utf8
  • select _utf8’Trällöällä‘ wandelt den String inUTF-8

Apache:

Irgendwo im Conf sollte AddDefaultCharset UTF-8 stehen. Das vereint das Charset über HTTP.

PHP/Browser:

Wenn der Browser kein UTF-8 anzeigen will, kann man das im php.ini umstellen: default_charset = „UTF-8“.

Links

http://www.phpwact.org/php/i18n/charsets ist eine wirklich gute, praktische Einführung in das Problem im Zusammenhang mit PHP.

Symfony/Creole:

Um da mit UTF-8 zu arbeiten muss databases.yml gefüllt werden. KEIN dsn benutzen, denn dann funzts nicht:

all:
  modul:
    class:      sfPropelDatabase
    param:
      phptype:  mysql
      username: dbuser
      password: dbuserpass
      database: modul
      host:     localhost
      encoding: utf8

Ein ähnliches, hässliches Problem bei der Datenbankmigration wird bei Orthogonal Thought beschrieben.

MySQL Views verwenden mit Propel und Symfony

Symfony, das PHP-Framework ist genial. Will man allerdings die Datenbank-Views verwenden, geht das nicht so direkt.

Damit die Propel orm-Klassen generiert werden, muss im config/schema.yml die Struktur so definiert werden, dass sie nicht direkt in die Datenbank abgespitzt wird:

m_my_view:
  _attributes: { phpName: ModulCompleteView, skipSql: true}
  post_id:        {type: integer}
  post_title:     {type: varchar, size: 255}
  post_content:   {type: longvarchar}
  comment_id:     {type: integer}
  comment_content:{type: longvarchar}

Damit die View bei symfony propel-insert-sql erstellt wird, sollte eine Datei im gleichen Verzeichnis erstellt werden in dem auch lib.model.schema.swl ist (normalerweise /data/sql). Wir nennen sie my-view.sql:

create or replace view m_my_view as
select
  post.id as post_id,
  post.title as post_title,
  post.content as post_content,
  comment.id as comment_id,
  comment.content as comment_content
from
  post, comment
where
  comment.post_id=post.id

Und nun nur noch die Datei sqldb.map ergänzen:

my-view.sql=DB-VERBINDUNG

und nun sollts funzen.