API Tag Cloud

Erstellung einer einfachen REST-API mit PHP

Von , und am 16.11.2013

Der Trend hin zu nativen Apps und Webapps welche hauptsächlich auf JavaScript basieren hat dazu geführt, dass immer öfter eigene REST APIs entwickelt werden müssen. Für unser kommendes Projekt “Herculess”, welches sowohl als native App als auch als JavaScript-MVC-Framework basierte Webapp entwickelt wird, haben wir deshalb angefangen, eine solche API zu entwickeln.

Die wichtigsten Eckdaten für die erste Version unserer API schauen wie folgt aus:

  • PHP-basiertes Backend
  • Gibt JSON zurück
  • Aktionen: CRUD: Create, Retrieve, Update, Delete
  • CRUD soll passende HTTP-Header verwenden
  • Authentifikation soll später über OAuth2 gehen, erstmal eine simple Pseudo-Implementierung zum Testen

Mit diesen Daten im Kopf haben wir also begonnen unsere API zu entwickeln.

Dafür verwenden wir das PHP-Framework Slim Framework, welches ausschließlich für solche REST-APIs gedacht ist. Nachdem wir das Framework nach dem (sehr einfachen) Tutorial der Webseite eingebunden und initialisiert haben, war hier schon mal ein wesentlicher Grundstein gelegt. Das Slim Framework nimmt uns im wesentlichen das Routing ab. So müssen wir uns nur noch um die eigentliche Datenverarbeitung kümmern.

Für unser Framework verwenden wir Routen, wie sie mittlerweile von den meisten APIs genutzt werden. Sie folgen dem Prinzip:

/projects: Gibt alle Projekte zurück

/projects/1: Gibt das Projekt mit der ID 1 zurück

/projects/1/tasks: Gibt alle Tasks des Projektes mit der ID 1 zurück

/tasks/1: Gibt den Task mit der ID 1 zurück

Alle diesen Routen können grundsätzlich mit verschiedenen HTTP-Headern (GET für Retrieve, POST für Create, PUT für Update, DELETE für Delete) genutzt werden. So wird ein GET Aufruf an /projects alle Projekte zurück geben, während ein POST Aufruf an /projects, zusammen mit mitgesendeten Daten, ein neues Projekt erstellen wird.

Wir werden nun kurz erläutern, wie wir bei der Entwicklung dieser API bisher vorgegangen sind. Die Authentifizierung werden wir in einem späteren Blogbeitrag behandeln, in diesem einfachen Beispiel gehen wir davon aus das alles öffentlich zugänglich ist.

Wir haben folgende Ordnerstruktur:

  • /Slim (Framework-Dateien)
  • /classes
    • ApiObject.class.php
    • ApiResultBuilder.class.php
    • Project.class.php
  • index.php
  • .htaccess

In der .htaccess haben wir folgenden Code, der dafür sorgt dass wir “schöne” URLs wie “http://api.com/tasks/1” statt “http://api.com/index.php/tasks/1”:

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [QSA,L]

Wir werden nun zeigen, wie wir beispielsweise die Routen /projects und /projects/1 implementiert haben. Andere Routen werden im Grunde genauso gehandhabt.
In der index.php binden wir die nötigen Klassen ein und kümmern uns dann in einem späteren Schritt um das Routing:

// Don't forget to load all neccessary classes!

// Initialize Slim Framework
require 'Slim/Slim.php';
\Slim\Slim::registerAutoloader();

$api = new \Slim\Slim();

$api->get('/projects', function ($) {
  // todo
}

$api->get('/projects/:uid', function ($uid) {
  // todo
}

$api->run();

Hier sehen wir, wie das Routing mit Slim funktioniert. Die eigentliche Datenverarbeitung passiert in den entsprechenden Klassen, in diesem Fall in der Project.class.php.
Aber was machen eigentlich die anderen drei Klassen, die wir erstellt haben?

Die Klasse “ApiObject” ist die Vorlage, von der die anderen Api-Klassen wie Project oder Task erben. In der aktuellen Implementation gibt es eigentlich keine besonderen Vorgaben hier, wir haben das nur für zukünftige Erweiterbarkeit so umgesetzt.

Die ApiResultBuilder-Klasse ist für den eigentlichen Aufbau der JSON-Response zuständig. Wir haben das so implementiert, damit es immer eine einheitliche Rückgabe gibt, und bei einer ggf. API-Änderung das nur an einer Stelle geändert werden muss.

Wir wollen eine Rückgabe wie folgt erreichen:

{
    "status": {
        "code": 100,
        "message": "OK"
    },
    "projects": [
        {
            //Objekt1
        },
        {
            //Objekt2
        },
        //...
    ]
}

In einer eigenen Tabelle in der Datenbank haben wir uns die möglichen Statuscodes mit passenden Messages gespeichert. zB. ist 100: OK oder 205: ENTRY NOT FOUND.

Schauen wir uns zuerst die ApiResultBuilder-Klasse an.

Diese verfügt über folgende Methoden: addObject(), setStatus(), setDataType(), getStatusMessage() und out().

Die Methoden setStatus() und setDataType() setzen im ApiResultBuilder-Objekt die Attribute “status” bzw. “dataType” auf einen übergebenen Wert. DataType entspricht dabei im obigen Beispiel “project”, also dem Model-Name. So können wir die JSON-Response theoretisch mit mehreren Objekttypen zurückgeben. Die Methode getStatusMessage() liefert zum Status die entsprechende Status-Message (zB. “OK” oder “ENTRY NOT FOUND”).

Die spannenden Methoden sind addObject() und out(): Ersteres kümmert sich darum, Daten zur Ausgabe hinzuzufügen, und letzteres kümmert sich um den Zusammenbau der Ausgabe. Die Methode addObject() nimmt ein normiertes Objekt als Übergabe-Parameter und speichert es im Objekt ab. Die Methode out() baut dann aus status, statusMessage, dataType und den gespeicherten Objekten die obige JSON-Response zusammen und gibt diese als Text aus.

Die Verarbeitung der Route /projects kann dann wie folgt passieren:

// Load all Projects from Project class
$projects = Project::getAll();
$apiBuilder = new ApiResultBuilder();
$dataType = "projects";

if(count($projects) < 0) {   // No results   $apiBuilder->setStatus(205);
} else {
  foreach($projects as $project) {
    $apiBuilder->addObject($project);
  }
  $apiBuilder->setStatus(100);
}

$apiBuilder->setDataType($dataType);
echo $apiBuilder->out();

Somit kümmert sich die index.php nur um das Routing, die ApiResultBuilder nur um die Ausgabe und die eigentlichen Klassen kümmern sich rein um die Datenverarbeitung. Natürlich ist dieses Beispiel recht vereinfacht und muss um Abfragen und Authentifizierung erweitert werden. Es lässt sich aber ausgezeichnet vergrößern und bietet eine ausreichende Trennung der verschiedenen Codeteile.

Wer sich fragt, wie man zB. mit POST-Anfragen umgehen würde: Auch das lässt sich mit Slim ganz einfach machen:

$api->post('/projects', function () { ...}

In diesem Block kann man dann einfach normal mit $_REQUEST arbeiten, um die mitübergebenen Daten zu verwenden.

In diesem Blogpost haben wir nun einen kleinen Einblick in unseren Ansatz zum API-Design gegeben. Natürlich gibt es tausende Ansätze, Formatierungen und Möglichkeiten, um solch eine API umzusetzen. Wir freuen uns über alternative Ideen in den Kommentaren!

4 Kommentare

  • Manuel Manhart am 26.10.2014 um 18:22

    Ein toller Post zu einem Klasse Framework.

    Wenn man sich im Post nicht mit $_REQUEST drum schlagen möchte, kann man zB mit

    $api->post(‘/datastore/:name’, function ($name) {
    $api = \Slim\Slim::getInstance();
    $body = $api->request->getBody();
    save($name, $body);
    });

    den Http Request Body holen, welcher in meinem Fall nur JSON enthält.

  • Oliver am 07.09.2014 um 00:03

    Warum habt Ihr euch eigene Statuscodes ausgedacht? Wären die Standard-HTTP-Codes nicht logischer gewesen, also 200 für OK, 404 für Not Found, etc.?

  • Francesco Novy am 20.11.2013 um 15:10

    Ja, wir verwenden da eine einheitliche API. Jedoch werden sowohl die Web-App als auch die native App Offline-Fähig sein, dafür werden die Daten von der API geladen und lokal (zB. im LocalStorage) gespeichert. Die App selber greift eigentlich immer nur auf den LocalStorage zu, die Synchronisierung passiert dann im Hintergrund – was aber unabhängig von der API ist 😉

  • Aleks am 17.11.2013 um 14:11

    Das heißt ihr verwendet ein einheitliches PHP backbone für die WebApp als auch die native App? Ist sicher cool wenn man die Daten einheitlich von einer Quelle bezieht aber das würde bedeuten die native App wird auch vom Internet abhängig sein? Oder wird das sowieso nur eine UIWebView mit der WebApp?

The comments are closed.