AJAX-API-Extension

Dieser Bereich dient dazu, eure Tricks und Erweiterungen vorzustellen, damit diese auch andere Anwender nutzen können. // This area can be used to publish your tricks and extensions to the APF to be used by other developers.
Antworten
TipTop
Beiträge: 193
Registriert: 25.08.2011, 22:37:08
Wohnort: Klagenfurt, Österreich
Kontaktdaten:

AJAX-API-Extension

Beitrag von TipTop » 24.03.2012, 12:18:25

Hallo zusammen,

in einem Wiki-Artikel wird erklärt, wie AJAX mittels APF möglich ist. Dabei wird einfach eine Front-Controller-Action aufgerufen, die bspw. Daten in JSON ausgibt und anschließend den Prozess mittels exit stoppt. Ich bin ein großer Anhänger von der AJAX-Technologie und würde sie auch gerne in dem Admin Control Panel meines CMS einsetzten. Allerdings benötige ich da zu 80% das First-Template-Prinzip - ich brauch meistens als Rückgabe nicht JSON oder XML, sondern einfach ein (X)HTML-Template - die Front-Controller-Action-Variante ist daher eher der falsche bzw. umständlichere Weg. Ich dachte an eine AJAX-API-Extension. Um diese zu nutzen, sollte die Bootstrap-Datei folgend aufgebaut sein:

Code: Alles auswählen

<?php
// include the pagecontroller
require_once '../apps/core/pagecontroller/pagecontroller.php';

import('tools::request', 'RequestHandler');
import('extensions::ajax::biz', 'AJAXManager');
$ajaxManager = new AJAXManager;
$ajaxManager->setLanguage($lang);
echo $ajaxManager->run(
    array('namespace' => 'mysite::pres::templates', 'template' => RequestHandler::getValue('mainview')),
    array('namespace' => 'mysite::pres::templates', 'template' => 'main'),
); 
Der AJAXManager erbt vom FrontController. Dessen Methode, run(), erwartet zwei Parameter. Der erste wird verwendet, falls im Browser des Clients JavaScript aktiviert ist. Falls nicht, wird die start()-Methode des Front-Controllers aufgerufen und übergeben werden Ihr die Daten des 2. Parameters.

Betrete ich zum ersten mal eine Website, dann weiß der AJAXManager natürlich noch nicht, ob JavaScript aktiviert ist. Das heißt, der AJAXManager ruft beim ersten Betreten einer Seite aufjedenfall die start()-Methode des Front-Controllers auf - so also wie es bei jeder APF-Applikation der Fall sein dürfte (sofern der FC genutzt wird). Es wird also das main.html-Template aufgerufen, wo wahrscheinlich das HTML-Grundgerüst definiert ist. Beim nächsten Seitenaufruf - einem AJAX-Request - soll nun aber nicht das main.html-Template, sondern das im ersten Parameter definierte Template als Bootstrap-Template geparst werden (sonst erhält man ja nochmal das HTML-Grundgerüst). Dies geschieht aber nur, wenn dem AJAXManager bekannt ist, dass JS auch aktiviert ist. Um Ihn das bekannt zu machen, muss man in der main.html eine, von der AJAX-Extension mitgelieferte TagLib verwenden. Das main.html-Template sollte folgend aufgebaut sein:

Code: Alles auswählen

<!DOCTYPE html>
<html>
    <head>
        <script type="text/javascript" src="pfad_zu_jquery_framework/jquery.js"></script>
        <core:addtaglib namespace="extension::ajax::pres::taglib" prefix="html" class="ajax" />
        <html:ajax ajaxanchorclass="ajax-anchor" resultcontainer="content" switchcontenteffect="fade" />
    </head>
    <body>
        <a href="index.php?mainview=home" class="ajax-anchor">Home</a>
        <a href="index.php?mainview=contact" class="ajax-anchor">Kontakt</a>
        <div id="content"></div>
    </body>
</html>
Durch <html:ajax /> wird JS-Code in den head des Templates reingeladen. Durch diesen JS-Code wird ein AJAX-Request abgesendet, sobald das Dokument vom Browser fertig geladen wurde. Aufgerufen wird eine Front-Controller-Action, welche ebenfalls in der AJAX-Extension zu finden ist. Diese FC-Action speichert im Prinzip nur in der User-Session, dass JS aktiviert ist, dass wars auch schon.
Der Parameter ajaxanchorclass des <html:ajax />-Tags gibt an, welche Anchors "deaktiviert" werden sollen. Der ins Template geladene JS-Code hängt an jedem Anchor mit der übergebenen class (in meinem Beispiel: ajax-anchor) einfach # an. Dadurch findet logischerweise kein neuladen der Seite mehr statt, wenn ich so einen Anchor anklicke. Sobald ich einen Anchor mit der übergebenen class anklicke, holt sich der JS-Code den Inhalt der href-Attributes, entfernt die # und erzeugt anhand dieser URL einen AJAX-Request. Nun, wo in der User-Session bekannt gemacht wurde, das JS aktiv ist, ladet der AJAXManager das im ersten Parameter übergebene Template. Klick ich den Anchor "Home" an, dann ist das home.html-Templater sozusagen das Bootstrap-Template und nicht die main.html. Die Server-Response und der damit zurückgelieferte (X)HTML-Code wird nun in einen Container hineingeladen. Diesen Container gebe ich ebenfalls beim <html:ajax />-Tag mit dem Attribut resultcontainer an - in meinem Beispiel würde der (X)HTML-Code also im <div id="content"> reingesetzt werden. Mit dem Attribut switchcontenteffect kann man entscheiden, wie der Inhalt im Result-Container ausgewechselt wird - dabei gibt es folgene Möglichkeiten:
-> none (inhalt wird ohne Effekt ausgewechselt)
-> fade (alter Inhalt wird ausgeblendet (also mit opactiy) und der neue eingeblendet)
-> slide (der Container schiebt sich zusammen; ist der neue Content geladen, schiebt sich der Container wieder auseinander).

Möchte man einen anderen Effekt, dann übergibt man einfach den Namen von diesem Effekt. Anschließend wird nach einem JQuery-Plugin gesucht, welches mit diesem Namen benannt wurde.


Um eine Software komplett auf AJAX umzustellen, müsste man mit dieser Extension gerade mal 5 Zeilen Code hinzufügen - und Benutzer mit deaktiviertem JavaScript werden dabei auch noch berücksichtigt. Hatte vor, dies direkt ins CMS zu implementieren. Aber wenn auch andere so eine AJAX-API gebrauchen könnten, dann würde ich das auch als richtige Extensions entwickeln - also, sagt bitte kurz Bescheid, ob Ihr daraus einen Nutzen ziehen könntet.

Grüße
Nico

Benutzeravatar
Screeze
Beiträge: 1920
Registriert: 05.08.2009, 09:49:04
Kontaktdaten:

Re: AJAX-API-Extension

Beitrag von Screeze » 24.03.2012, 13:10:41

Hi,

dein Ansatz klingt auf jeden Fall interessant, jedoch würde ich die Geschichte mit dem bekanntmachen ob JS aktiviert ist oder nicht anders gestalten:
Der PHP Komponente sollte vollkommen egal sein ob JS aktiviert ist oder nicht.

Du solltest vielmehr einfach anhand der URL unterscheiden, ob es ein ajax request ist, oder nicht.

Beispiel:

1. Seitenaufruf: example.com/
---------> ruft ganz normal das main Template des FC auf.
---------> Ein per Taglib injizierter JS Code wird ausgeführt, und versieht alle als AJAX markierte Links mit einem eigenen onClick handler, und schreibt den Link um (mehr dazu später)

Dieser onClick Handler kümmert sich im Falle eines Anklicken des Links darum, dass die im Link hinterlegte Adresse aufgerufen wird, jedoch noch ein Parameter angefügt wird (z.B.: &ajaxapi=true). Die Methode, wie das angefügt wird, sollte leicht überschreibbar sein, Falls das URL-Rewriting geändert/eingesetzt wird. Außerdem kümmert sich der Handler darum, dass der originale Linkklick nicht mehr ausgeführt wird. Denn:
Der ins Template geladene JS-Code hängt an jedem Anchor mit der übergebenen class (in meinem Beispiel: ajax-anchor) einfach # an. Dadurch findet logischerweise kein neuladen der Seite mehr statt, wenn ich so einen Anchor anklicke.
Das stimmt so nicht.
Befindest du dich auf Seite "example.com/index.php?foo=bar" wird trotzdem die Seite neu geladen, wenn du "example.com/index.php?foo=blubb#" anklickst. (Der Teil hinter # würde lediglich versucht als Sprunganker zu verwenden)
Aber Javascript bietet ja eine Funktion auf seinem Event Objekt, mit der du das Event canceln kannst.


----

Ein weiteres Problem, welches eingeplant werden muss, ist die Verlinkbarkeit von Inhalten.
Webseiten, auf denen alles mit AJAX geladen wird, und man keine Möglichkeit hat, jemandem einen Link zu geben, vom aktuellen Punkt an dem man sich befindet, sind die Hölle.
Gleichzeitig sollte man noch einen SEO-Punkt berücksichtigen: Zwar bietest du die Möglichkeit dass Google deine Webseite crawlt, da es kein JS aktiviert hat. Aber Links, die jemand kopiert und irgendwo einfügt, zählen nichts mehr für die eigentliche Seite, weil Sie nur noch auf deine Startseite verlinken, und Suchmaschinen somit keinen Mehrwert daraus ziehen können.
Google bietet hier ein nettes Feature, welches man da verwenden kann:
http://www.seomoz.org/blog/how-to-allow ... ax-content

Anstatt die Links also einfach zu deaktivieren, sollten diese besser umgeschrieben werden, und zwar auf das von Google vorgeschlagene Format:
example.com/index.php?foo=bar&blubb=bla
sollte umgeschrieben werden zu:
"#!foo=bar&blubb=bla"

Und der Linkklick sollte dann nicht abgefangen werden, denn die Seite wird nicht neu geladen, lediglich der Text hinter dem Hash wird an die URL angehängt. Diesen Link kann man nun kopieren und jemandem schicken.
Nun benötigt es noch ein kleines Javascript, welches in die Seite injiziert wird, und diesen lokalen Anchor auswerten kann. Falls einer vorhanden ist, sollte per ajax direkt die korrekte Seite geladen werden.

Nun könnte man direkt noch die von Google verwendete Schnittstelle anbieten, welche es verwendet, falls es eine solche URL erkennt. Die Seite mit "?_escaped_fragment=..." sollte per 301 wieder auf die statische Seite weiterleiten, welche normalerweise verwendet wird, wenn kein JS aktiviert ist.

------

Außerdem sollte der Code nicht statisch in einen Container injiziert werden, sondern es sollte auch eine Möglichkeit geben diesen stattdessen an ein JS-Callback zu senden, sodass man ihn anderweitig verarbeiten kann.

TipTop
Beiträge: 193
Registriert: 25.08.2011, 22:37:08
Wohnort: Klagenfurt, Österreich
Kontaktdaten:

Re: AJAX-API-Extension

Beitrag von TipTop » 25.03.2012, 11:32:51

Außerdem sollte der Code nicht statisch in einen Container injiziert werden, sondern es sollte auch eine Möglichkeit geben diesen stattdessen an ein JS-Callback zu senden, sodass man ihn anderweitig verarbeiten kann.
Ja, stimmt, das wäre auch n wichtiger Punkt.

Etwas in dieser Form wäre ganz nett gewesen:

Code: Alles auswählen

<a href="#!/param=value" onclick="loadData('index.php?param=value', false, 'myCallbackFunction')" class="ajax-anchor">Link</a>
 
Ist der 2. Parameter auf true, wird der Inhalt einfach in den Container, den man im <html:ajax />-Tag definiert, hineingeladen. Ist der 2. Parameter auf false, wird, sofern eine CallbackFunction angegeben wurde, diese ausgerufen und Ihr das Ergebnis des Ajax-Requests übergeben. Die Frage ist nur, wo definiert man, ob man nun eine CallbackFunction verwenden oder das Ergebnis direkt in den Container injizieren möchte - immerhin wird das onclick-Attribut ja dynamisch von der JS-Komponente aus gesetzt.

Benutzeravatar
Screeze
Beiträge: 1920
Registriert: 05.08.2009, 09:49:04
Kontaktdaten:

Re: AJAX-API-Extension

Beitrag von Screeze » 25.03.2012, 11:47:40

würde ich gar nicht machen.
lass den 2. parameter weg, und gib direkt die callback funktion an.

Code: Alles auswählen

loadData = function(url, callback) {
    
// ....  daten laden ...


    if(typeof(callback) !== 'undefined') {
          callback(data);
          return;
    }
    // hier der statische code, wenn kein callback mitgegeben wurde.

};

TipTop
Beiträge: 193
Registriert: 25.08.2011, 22:37:08
Wohnort: Klagenfurt, Österreich
Kontaktdaten:

Re: AJAX-API-Extension

Beitrag von TipTop » 07.04.2012, 10:32:37

AjaxAPI
  • apps
    • extensions
      • ajaxapi
        • pres
          • task
            • base_task.php
        • data
          • JSONMapper.php
        • biz
          • AjaxAPIManager.php

Um die AjaxAPI nutzen zu können, bedarf es einer folgenden Bootstrap-Datei:

Code: Alles auswählen

<?php
// include the pagecontroller
include_once('./apps/core/pagecontroller/pagecontroller.php');

import('extensions::ajaxapi::biz', 'AjaxAPIManager');
$frontController = &Singleton::getInstance('Frontcontroller');
$ajaxAPIManager = new AjaxAPIManager($frontController);
$ajaxAPIManager->setLanguage('de');
echo $ajaxAPIManager->start(
    array('mysite::pres::templates', 'ajax'), 
    array('mysite::pres::templates', 'main'), 
    'ajaxapi::pres::task'
);
?>
Wie gewohnt, wird anfangs der Page-Controller eingebunden und anschließend der AjaxAPIManager. In der Datei AjaxAPIManager.php wird die Datei Frontcontroller.php bereits importiert, und daher muss man sich darum in der Bootstrap nicht mehr kümmern. Dann sollte man sich über Singleton die Frontcontroller-Insantz besorgen und diese dem AjaxAPIManager-Konstruktor übergeben. Bevor die Methode AjaxAPIManager::start() aufgerufen wird, sollte dem AjaxAPIManager Context und Language übergeben werden - dieser injiziert diese anschließend in den per Konstruktor übergebenen Frontcontroller.

Die Methode AjaxAPIManager::start() erwartet mindestens 2 Parameter vom Datentyp Array. Der 3. Parameter ist optional und zu dem komm ich später noch.

Der AjaxAPIManager entscheidet anhand dem URL-Parameter ajaxapi=true ob eine AJAX-Request vorliegt oder nicht. Sofern es ein AJAX-Request ist, kommt der erste Parameter, welchen man AjaxAPIManager::start() übergibt ins Spiel. Ist es ein normaler Request - sprich wurde ajaxapi=true in der URL nicht gefunden - wird der 2. Parameter bei AjaxAPIManager::start() verwendet. So kann man 2. verschiedene Main-Templates je nach Request adressieren.
In den beiden Main-Templates kann man nun mittels <core:importdesign namespace="" template="[urlparam = foo]" incparam="urlparam" /> wie gewohnt Sub-Templates einbinden.

Manchmal benötigt man bei einem AJAX-Request aber kein (X)HTML und somit auch nicht das First-Template-Prinzip. Der 3. Parameter, den man AjaxAPIManager::start() übergibt, gibt den Namespace von (AJAX-)Tasks an. Um einen Task anzufordern, ist folgende URL notwenig:

http://adresse.tld/index.php?ajaxapi=true&task=my_task

Wird ein (AJAX-)Task übergeben, wird diese Task-Klasse instanziiert und das "AJAX-Template", welches im ersten Parameter bei AjaxAPIManager::start() übergeben wird, wird vom Page-Controller erst gar nicht verarbeitet.
Ein Task stellt eine einzige Aufgabe dar, die möglichst gut erledigt werden sollte. Tasks sind der Präsentationsschicht zuzuordnen. Ähnlich einem Document-Controller kann und soll er anhand von (URL-)Parametern entscheiden, wie's weiter geht. Er kann sich wie ein Document-Controller Daten von einem Manager besorgen und diese dann in JSON oder XML zurückgeben.

Ein Task muss von der Klasse base_task erben. Folgenden Aufbau benötigt ein Task aufjedenfall:

Code: Alles auswählen

<?php
class my_task_task extends base_task {
    public function init($params) {
    }

    public function execute() {
    }
}
?>
Diese Datei muss nun als my_task_task.php abgespeichert werden. Möchte man diesen Task ausführen, sollte die URL folgend aufgebaut sein:
http://adresse.tld/index.php?ajaxapi=true&task=my_task (Wichtig: _task beim Task-Namen weglassen)

Ein Task darf aus den Zeichen a-z und _ bestehen. Ein Task muss jeweils 2 Methoden implementieren, init() und execute(). Der init() Methode werden aber nicht stets Parameter übergeben (später mehr dazu). Möchte ein Task URL-Parameter erhalten, muss er sich diese selbst beschaffen.

Die base_task-Klasse, von der jeder Task erben muss, bietet folgende API an:

setResult($result, $contentType = 'json'): über diese Methode kann ein Task ein Resultat (bspw. XML- oder JSON-Code) an den Bildschirm befördern, welche dann die JavaScript-Applikation einfach verarbeiten kann. Der 2.Parameter ist optional und ist standardmäßig auf JSON gesetzt, alternativ kann man noch xml übergeben.

getJSONMapper(): Liefert ein Objekt (Singleton) der Klasse JSONMapper zurück - dieser Mapper ist in apps::extensions::ajaxapi::data zu finden. Ein (AJAX-)Task soll als Vermittler zwischen Manager (biz-Schicht) und der JavaScript-Applikation fungieren. Die Task-Klasse kann mit dem JSONMapper JSON-Strings, die es von der JavaScript-Applikation erhält, ganz einfach in PHP-GORM-Objekte oder -Arrays mappen und diese anschließend dem Manager übergeben. Die Task-Klasse kann auch Arrays, GORM-Objekte und GORM-Objekt-Listen, welche sie vom Manager erhält, in JSON mappen und diese der JavaScript-Applikation über setResult() zukommen lassen.
Zur Funktionalität des JSONMapper komme ich später noch.

executeTask($namespace, $name, array $params = array()): Diese Methode kann man etwa mit loadDesign() vergleichen - man übergibt Namespace und Name eines Tasks und dieser wird dann ausgeführt. Rückgabewert dieser Methode ist das Resultat des Taks (also wahrscheinlich ein JSON- oder XML-String). Weiter oben habe ich ja erwähnt, dass ein Task die init()-Methode implementieren muss. Führt ein Task über die executeTask()-Methode einen anderen Task aus, kann er diesem Parameter übergeben. executeTask() kennt nämlich noch einen 3. optionalen Parameter, bei dem es sich um ein Array handeln muss - dieses Array übergibt executeTask() dann dem instanziierten Task über die init()-Methode.
Beispiel für executeTask():

Code: Alles auswählen

<?php
class my_task_task extends base_task {
    public function init() {}
  
    public function execute() {
        $resultat = $this->executeTask('mysite::pres::task', 'hello_world'); // wichtig: _task beim Namen wieder weglassen

        // oder alternativ mit der übergabe von parametern
        $resultat = $this->executeTask('mysite::pres::task', 'hello_world', array('foo' => 'bar', 'hello' => 'world'));
    }
}
?>
executeTasks(array $tasks): Vergleichbar mit der zuvor genannten Methode. Einziger Unterschied ist, dass man ein Array mit beliebig vielen Tasks übergeben kann, welche nach der Reihe abgearbeitet werden. Rückgabewert dieser Methode ist ein assoziatives Array mit den Resultaten der Tasks. Beispiel:

Code: Alles auswählen

<?php
class my_task_task extends base_task {
    public function init() {}
  
    public function execute() {
        $resultat = $this->executeTasks(array(
            array(
                'mysite::pres::task',
                'hello_world',
            ),
            array(
                'mysite::pres::task',
                'foo',
            ),
            array(
                'mysite::pres::task',
                'bar',
                array('Dieser 3. Parameter ist optional und wird wieder der init()-Methode des Tasks übergeben')
            )
        ));

        // so kann ich auf den Rückgabewert des 1. Tasks zugreifen
        $resultat['hello_world'];
    }
}
?>



Nun noch kurz was zur API des JSONMappers. Er bietet folgende Funktionalität an:

mapArray2Json(array $array): Man übergibt ein beliebiges Array an diese Methode und erählt einen String in JSON Format.

mapGenericDomainObject2Json(GenericORMapperDataObject $object): Übergeben wird ein Objekt, welches das Interface GenericORMapperDataObject implementieren muss (also ein GenericDomainObject bzw. ein Objekt, welches von diesem abgeleitet wird). Zurückgegeben wird das Objekt in JSON-Format.

mapGenericDomainObjectList2Json(array $objectList): Dieser Methode muss ein Array gefüllt mit mit Objekten übergeben werden. Die Objekte müssen wieder das GenericORMapperDataObject-Interface implementieren. Zurückgegeben wird eine in JSON formatierte Liste aus Objekten.

mapJson2Array($jsonString, $depth = 512): Hier übergibt man einen JSON-String und erählt ein PHP-Array - das genaue Gegenstück zur ersten Methoden.

mapJson2GenericDomainObject($jsonString, GenericORMapperDataObject $domainObject): Dieser Methode muss ein JSON-String und ein GenericDomainObject übergeben werden. Das GenericDOmainObject muss natürlich nur den ObjektNamen definieren - die Objekt-Properties werden aus dem JsonString gefiltert und dem GenericDomainObjekt übergeben. Am Ende wird das gefüllte Domain-Objekt zurückgegeben.

mapJson2GenericDomainObjectList($jsonString, GenericORMapperDataObject $domainObject): Schreitet ähnlich vor wie mapJson2GenericDomainObject(), nur dass es im JSON-String mehrere Objekten erwartet. Rückgabewert dieser Methode ist ein array gefüllt mit GenericDomainObjects vom Typ $domainObject.

Der Code ist hier zu finden: https://svn.code.sf.net/p/apfajaxapi/co ... s/ajaxapi/

Soviel mal zur PHP-API. Mit dem JavaScript-Teil werden ich dann vorraussichtlich nächste Woche fortfahren.

Frohe Ostern! :)

Grüße,
Nico

Benutzeravatar
dr.e.
Administrator
Beiträge: 4533
Registriert: 04.11.2007, 16:13:53

Re: AJAX-API-Extension

Beitrag von dr.e. » 09.04.2012, 17:30:14

Hallo Nico,

klingt nach einem interessanten Ansatz und nach einer Ergänzung zu Ralf's(?) Artikel. Bin auf die Implementierung gespannt.
Viele Grüße,
Christian

Antworten

Wer ist online?

Mitglieder in diesem Forum: 0 Mitglieder und 1 Gast