Schlagwort-Archive: array

Einstieg in Ruby on Rails

Aufgrund meines Jobs habe ich mich den letzten Monat mit Ruby on Rails beschäftigt und die ersten Funktionen in einer RoR-Anwendung ergänzt. Auch wenn ich seit Jahren PHP-Anwendungen schreibe und bereits Erfahrungen mit PHP-Frameworks gesammelt habe, ist dieses Wissen nur begrenzt für die Ruby on Rails-Entwickung von Nutzen. Warum?

Ruby

Ruby hat eine andere Syntax… Zugegeben: eine schönere und besser lesbare Syntax als PHP. Was ich z.B. sehr schön finde, ist folgender Code:
[code lang=’Ruby‘]puts i if i > 0[/code]
Wenn i größer als 0 ist, wird i ausgegeben. Ganz einfach, in einer Zeile, ohne geschweifte Klammern, auf den Punkt.

Natürlich findet man viele Gemeinsamkeiten in Ruby und PHP. Wäre ja komisch, wenn Ruby ohne Arrays und Objekte auskäme.
Der Ruby-Code ist deutlich besser lesbar, da z.B. bei Code-Blöcken mit Texteinrückungen gearbeitet wird, anstatt geschweifte Klammern zu verwenden, die dadurch obsolet werden.

Natürlich sollte als erstes Ruby gelernt werden und erst dann Rails. Für den Ruby-Einstieg eignet sich dieses interaktive TryRuby-Tutorial.

Rails

Rails-Anwendungen werden auf einer ganz anderen Abstraktionsebene programmiert als PHP-Anwendungen. Die zwei Grundsätze von RoR sind

  • Don’t Repeat Yourself
  • Convention over Configuration

Die Vorteile dieser Grundsätze sind

  1. Weniger Code
  2. Schnellere Entwicklung
  3. Konzentration auf die Webanwendung (anstatt sich mit Konfigurationen oder DB-Design zu beschäftigen)

Es muss also attestiert werden, dass Rails einem sehr viel Arbeit abnimmt. Man kann sich über die Rails-Konsole sogar einfache, funktionierende Grundgerüste (Scaffolds) der Webanwendung erstellen lassen, ohne auch nur eine Zeile Code schreiben zu müssen.

Dank den RubyGems können zusätzliche Erweiterungspakete in die Anwendung integriert werden, die die Funktionalität von Ruby on Rails ergänzen. Nested_form zur einfachen Handhabung mehrerer Modelle in einem Formular.
Oder einfache Suchfunktionalitäten mit meta_search gefällig? Welche Schritte sind in PHP notwendig, um eine Suche zu implementieren: Formular bauen, eingegebene Daten prüfen, säubern, übernehmen und eine SQL-Abfrage daraus bauen. Richtig? Mit meta_search benötigst du in RoR gerade mal neun popelige Zeilen Code, um in Artikeln nach Titeln zu suchen:
Im Controller der Artikel müssen die Suchparameter entgegen genommen werden (4 Zeilen):
[code lang=’ruby‘]def index
@search = Article.search(params[:search])
@articles = @search.all # lade alle Einträge
end[/code]

Und im Formular wird die Suche definiert (5 Zeilen):
[code lang=’ruby‘]<%= form_for @search do |f| %>
<%= f.label :title_contains %>
<%= f.text_field :title_contains %>
<%= f.submit %>
<% end %>[/code]
wobei title das zu durchsuchende Feld in der Datenbank ist und _contains gibt an, dass der Suchparameter als Teilstring verwendet wird. Jeder Programmierer wird angesichts dieser wenigen Code-Zeilen mit der Zunge schnalzen, denn viel produktiver kann wohl nicht programmiert werden.

Aber dieser hohe Abstraktionsgrad hat auch seine Nachteile: Was tun, wenn die Suche nicht das tut, was man erwartet? Welche Optionen stehen einem zur Verfügung? Wie richtet man eigene Suchmethoden ein? Wie sieht’s mit Sicherheitslücken aus? Welche Variablen und Funktionen stehen zur Verfügung? Welche Abhängigkeiten bestehen?
Wie bei allen Frameworks begibt man sich in eine neue Umgebung, die man erst erkunden muss. Man muss sich auf Rails voll einlassen und sollte die Art und Weise von Rails akzeptieren. Die Herangehensweise „in PHP habe ich das so implementiert, wie setze ich das jetzt 1:1 in RoR um„, ist zum Scheitern verurteilt. Dazu hat Rails zu viele Konventionen, die man kennen und benutzen sollte, weil sie einem das Leben leichter machen.

Ich habe für mich festgestellt, dass das pure Einlesen in die Rails-Materie relativ wenig bringt. Besser sind Tutorials, in denen man aktiv werden muss. Ein gutes, witzig gemachtes und kostenloses Tutorial ist Rails for Zombies. Den kostenpflichtigen Fortsetzungsteil namens Rails for Zombies 2 von der Code School kann ich ebenfalls nur empfehlen.

Fazit

Auf der Ruby on Rails-Seite prangt folgendes Zitat von Tim O’Reilly:

Ruby on Rails is a breakthrough in lowering the barriers of entry to programming. Powerful web applications that formerly might have taken weeks or months to develop can be produced in a matter of days.

Ein schönes Schlussfazit. Ich würde allerdings nicht sagen, dass es die Einstiegsbarrieren für die Programmierung senkt. RoR ist in meinen Augen kein Anfänger-Framework. Natürlich kümmert Rails sich um viele Aspekte, aber ich halte es gerade bei Anfängern für wichtig, dass sie die zugrunde liegenden Mechanismen verstehen. Und das ist bei all der „Rails-Magie“ nicht möglich.

PHP: Skripte mit langer Laufzeit, JQuery Ajax-Warteseite, Zwischenspeicherung der Berechnungen als JSON und Lock-Mechanismus

Das Problem

Die Überschrift hört sich etwas tricky an, aber ich denke, wenn wir das Problem auf ein praktisches Beispiel übertragen, wird es klar.

Nehmen wir an, wir haben eine Fußballstatistik-Seite, wo alle Statistiken zur 1. bis 3. Fußballbundesliga abrufbar sein sollen. Jeder Spieler hat eine eigene Seite und zu jedem Spieler müssen täglich aufwändige Statistik-Berechnungen durchgeführt werden, die mehrere Minuten dauern.

Eine Lösung wäre, dass man mit Cronjobs 1x täglich diese aufwändigen Berechnungen nachts durchführt und so die Ergebnisse bereit hält, wenn der Besucher eine Spielerseite auswählt. Das Problem an dieser Vorgehensweise ist allerdings, dass es so viele Spieler gibt, dass die Berechnungen zu lange dauern würden. Zudem werden eh nur 10% der Spielerseiten besucht und 2% der Spielerseiten haben 90% der Zugriffe. Man kann also für diese 2% (High Traffic-Seiten) Cronjobs anlegen, die restlichen Berechnungen aber, sollen quasi On-the-Fly erstellt werden. Das führt zwar zu teilweise langen Wartezeiten für die Besucher, aber die Ergebnisse dieser Berechnungen sind so großartig, dass die Besucher darauf auch bis zu mehreren Minuten warten würden.

Anforderungen an die „On the fly“-Berechnungen

  1. Die Parameter für die Berechnung ändern sich nur 1x täglich
  2. Die Ergebnisse liegen nach der Berechnung in einem Array vor und sollen als JSON zwischengespeichert werden, so dass dann die Anzeige der Ergebnisse schnell erfolgen kann
  3. Wenn der Besucher auf die Ergebnisse warten muss, soll eine Warteseite mit einem Ajax-Loader angezeigt werden. Wenn die Berechnungen beendet sind, wird auf die eigentliche Seite weitergeleitet
  4. Für einen Spieler darf die Statistik-Berechnung nur 1x ausgeführt werden.
    Ein Beispiel: Besucher 1 besucht die Seite von „Arjen Robben“ und stößt die Berechnungen an. Besucher 2 besucht wenige Sekunden danach die Seite von „Arjen Robben“, es liegen keine Daten vor und trotzdem soll das Skript erkennen: „Moment! Die Berechnungen laufen schon, stoße die Berechnungen kein zweites Mal an.“
  5. Sollten veraltete Berechnungen vorliegen, werden diese zwar angezeigt (mit einem Hinweis, dass die Daten veraltet sind), um keine Wartezeit zu haben, im Hintergrund wird allerdings die aufwändige Berechnung angestoßen. Hier gilt wieder: Die Berechnung darf nur 1x angestoßen werden.

Warteseite mit JQuery-Ajax

Die Warteseite wird nur dann ausgegeben, wenn noch nie Daten für den Spieler berechnet wurden.

Damit die Warteseite das Ende der Berechnung mitbekommt, muss diese über Ajax die Berechnungen anstoßen.
[code lang=’js‘]$(document).ready(function() {
$.ajax({
type: ‚GET‘,
url: ’spielerSeite.php‘,
cache: false,
data: ‚do=ajax‘,
success: function(msg){
if (msg == ‚true‘){
location.replace(‚http://localhost/spielerSeite.php‘);
}
else
{
$(‚div#responseAjax‘).empty();
$(‚div#responseAjax‘).addClass(’success‘);
$(‚div#responseAjax‘).html(msg);
}
}
});
});[/code]
Es wird also die Seite spielerSeite.php?do=ajax aufgerufen. Diese Seite führt die Berechnungen durch und gibt am Ende ‚true‘ aus, so dass auf die eigentliche Seite spielerSeite.php weitergeleitet wird. Wenn nicht ‚true‘ zurück gegeben wird, hat bereits ein anderer Benutzer die Berechnungen angestoßen und man gibt eine Meldung zurück.

Zwischenspeicherung der Berechnungen als JSON

Die fertigen Berechnungen, werden als json-Datei im Filesystem gespeichert:
[code lang=’php‘]function setJSON()
{
file_put_contents($this->json_dir.$this->spieler_string.‘.json‘, json_encode($this->berechnung_array));
}

function getJSON()
{
$this->berechnung_array = json_decode(file_get_contents($this->json_dir.$this->spieler_string.‘.json‘), true);
}[/code]
Vorteil von json ist die absolut problemlose Konvertierung vom PHP-Array zu einem json-String, der in einer Datei gespeichert wird. Anhand des Änderungsdatums der Datei mit filemtime kann man zudem einfach feststellen, ob die Daten wieder aktualisiert werden müssen.

Lock-Mechanismus zur einmaligen Ausführung

Wie kann man mit PHP feststellen, ob bereits eine Berechnung für einen Spieler läuft? Gar nicht. Also muss man es sich merken!

In einem Execution Array ($this->execution_array) werden die $this->spieler_string gespeichert, deren Berechnungen zur Zeit ausgeführt werden. Damit alle Skripte, die gleichzeitig laufen, auf dieses Array zugreifen können, wird das Array als JSON gespeichert:
[code lang=’php‘]function setExJSON()
{
file_put_contents(‚execution.json‘, json_encode($this->execution_array));
}

function getExJSON()
{
if (!file_exists(‚execution.json‘))
{
$this->execution_array = array();
}
elseif (file_get_contents(‚execution.json‘) === ’null‘)
{
$this->execution_array = array();
}
else
{
$this->execution_array = json_decode(file_get_contents(‚execution.json‘), true);
}
}[/code]
Nun muss bei jedem potentiellen Start der Berechnungen überprüft werden, ob für den Spieler schon Berechnungen laufen. Dabei ist darauf zu achten, dass $this->execution_array frisch aus der JSON-Datei geladen wurde. Sollte niemand die Berechnung gestartet haben, wird das Execution-Array um den entsprechenden Eintrag erweitert und als JSON gespeichert.
Nachdem die Berechnungen durchgeführt wurden, wird das Array wieder geladen, der entsprechende Eintrag gelöscht und das Array wieder gespeichert:
[code lang=’php‘]$this->getExJSON();
if (in_array($this->spieler_string,$this->execution_array))
{
exit();
}
else
{
$this->execution_array[] = $this->spieler_string;
$this->setExJSON();
$this->fuehreLangeBerechnungenDurch();
$this->getExJSON();
unset($this->execution_array[array_search($this->spieler_string, $this->execution_array)]);
$this->setExJSON();
}[/code]
Das Problem an dieser Implementation ist natürlich, dass Schreiben und Lesen ins Execution-Array kein kritischer Abschnitt ist und z.B. zeitgleich mehrere Berechnungsvorgänge gestartet werden könnten.. Da aber schlimmstenfalls „nur“ mehrere Berechnungen für einen Spieler gleichzeitig durchgeführt würden, ist das Problem vernachlässigbar.

Ablaufkontrolle

Und hier der komplette Ablauf des Skripts:
[code lang=’php‘]$this->execution_array = array();
if (isset($_GET[‚do‘]) AND $_GET[‚do‘] === ‚old‘)
{
//Daten veraltet: Alten Daten laden und anzeigen
$this->getJSON();
$this->toHTML();
}
elseif (isset($_GET[‚do‘]) AND $_GET[‚do‘] === ‚wait‘)
{
//Warteseite mit Ajax anzeigen
$this->doWaitHTML();
}
elseif (isset($_GET[‚do‘]) AND $_GET[‚do‘] === ‚ajax‘)
{
//Durch Ajax von Warteseite aus Berechnungen angestoßen
$this->getExJSON();
if (in_array($this->spieler_string,$this->execution_array))
{
//Anderer User hat Berechnungen schon angestoßen
$this->ajaxFalseResponse();
}
else
{
//Berechnungen durchführen
$this->execution_array[] = $this->spieler_string;
$this->setExJSON();
$this->fuehreLangeBerechnungenDurch();
$this->getExJSON();
unset($this->execution_array[array_search($this->spieler_string, $this->execution_array)]);
$this->setExJSON();
}
}
elseif (!file_exists($this->json_dir.$this->spieler_string.‘.json‘))
{
//Es liegen noch keine Berechnungen vor! Weiterleiten auf Warteseite.
$host = $_SERVER[‚HTTP_HOST‘];
$uri = $_SERVER[‚PHP_SELF‘];
$extra = ‚?do=wait‘;
header(„Location: http://$host$uri$extra“);
exit();
}
else
{
$this->getJSON();
if (filemtime($this->json_dir.$this->spieler_string.‘.json‘) < strtotime(' -1 day')) { //Die Daten sind veraltet. Weiterleiten zum Anzeigen der alten Daten, aber Berechnungen der neuen Daten starten $host = $_SERVER['HTTP_HOST']; $uri = $_SERVER['PHP_SELF']; $extra = '?do=old'; header("Location: http://$host$uri$extra"); $this->getExJSON();
if (in_array($this->spieler_string,$this->execution_array))
{
//Anderer User hat Berechnungen schon angestoßen
exit();
}
else
{
//Berechnungen durchführen
$this->execution_array[] = $this->spieler_string;
$this->setExJSON();
$this->fuehreLangeBerechnungenDurch();
$this->getExJSON();
unset($this->execution_array[array_search($this->spieler_string, $this->execution_array)]);
$this->setExJSON();
}
}
else
{
//Wenn Daten aktuell sind: Anzeigen!
$this->toHTML();
}
}[/code]

Google Calendar Api: GetTimes() leer

Google stellt für alles mögliche APIs bereit, so auch eine Javascript API für den Google Calendar, die wir für unser Uni Projekt benutzen wollten. Hier muss zwischen den verschiedenen Teilnehmern ein freier Termin koodiniert werden. Das interaktive Beispiel Retrieve Events with date query ist sehr nah an dem dran, was wir brauchen.

Doch anstatt des Titels, der in dem Beispiel ausgegeben wird
PRINT
(‚Event title = ‚ + event.getTitle().getText());
interessieren uns das Start- und Enddatum des Events, welches so angesprochen werden können sollte:
event.getTimes()[0].getStartTime().getDate();
event.getTimes()[0].getEndTime().getDate();
Aber es funktioniert nicht, da das Array von event.getTimes() leer ist. Warum ist das Array empty? Funktioniert die Funktion nicht? Nein, es liegt daran, dass der falsche Feed beim Einlesen der Daten verwendet wurde: Es muss ein full-Feed verwendet werden und kein basic-Feed:
var feedUrl = „http://www.google.com/calendar/feeds/liz@gmail.com/private/full“;