Erstellen einer REST-API für Legacy-PHP-Projekte

Veröffentlicht: 2022-03-11

Das Erstellen oder Entwerfen einer REST-API ist keine leichte Aufgabe, insbesondere wenn Sie dies für ältere PHP-Projekte tun müssen. Heutzutage gibt es viele Bibliotheken von Drittanbietern, die es einfach machen, eine REST-API zu implementieren, aber die Integration in bestehende Legacy-Codebasen kann ziemlich entmutigend sein. Und Sie haben nicht immer den Luxus, mit modernen Frameworks wie Laravel und Symfony zu arbeiten. Bei Legacy-PHP-Projekten finden Sie sich oft irgendwo zwischen veralteten internen Frameworks wieder, die auf alten PHP-Versionen laufen.

Erstellen einer REST-API für Legacy-PHP-Projekte

Erstellen einer REST-API für Legacy-PHP-Projekte
Twittern

In diesem Artikel werfen wir einen Blick auf einige häufige Herausforderungen beim Versuch, REST-APIs von Grund auf neu zu implementieren, einige Möglichkeiten, diese Probleme zu umgehen, und eine Gesamtstrategie zum Erstellen benutzerdefinierter PHP-basierter API-Server für Legacy-Projekte.

Obwohl der Artikel auf PHP 5.3 und höher basiert, gelten die Kernkonzepte für alle Versionen von PHP über Version 5.0 hinaus und können sogar auf Nicht-PHP-Projekte angewendet werden. Hier wird nicht behandelt, was eine REST-API im Allgemeinen ist. Wenn Sie also nicht damit vertraut sind, sollten Sie sich zuerst darüber informieren.

Um Ihnen das Nachvollziehen zu erleichtern, finden Sie hier eine Liste einiger Begriffe, die in diesem Artikel verwendet werden, und ihre Bedeutung:

  • API-Server: Haupt-REST-Anwendung, die die API bedient, in diesem Fall in PHP geschrieben.
  • API-Endpunkt: eine Backend-„Methode“, mit der der Client kommuniziert, um eine Aktion auszuführen und Ergebnisse zu erzielen.
  • API-Endpunkt-URL: URL, über die das Backend-System weltweit zugänglich ist.
  • API-Token: Ein eindeutiger Identifikator, der über HTTP-Header oder Cookies weitergegeben wird und anhand dessen der Benutzer identifiziert werden kann.
  • App: Clientanwendung, die über API-Endpunkte mit der REST-Anwendung kommuniziert. In diesem Artikel gehen wir davon aus, dass es webbasiert ist (entweder Desktop oder Mobil) und daher in JavaScript geschrieben ist.

Erste Schritte

Pfadmuster

Eines der allerersten Dinge, die wir entscheiden müssen, ist, unter welchem ​​URL-Pfad die API-Endpunkte verfügbar sein werden. Es gibt 2 beliebte Methoden:

  • Erstellen Sie eine neue Subdomain, z. B. api.example.com.
  • Erstellen Sie einen Pfad, z. B. example.com/api.

Auf den ersten Blick scheint die erste Variante beliebter und attraktiver zu sein. In der Realität könnte es jedoch sinnvoller sein, die zweite Variante zu wählen, wenn Sie eine projektspezifische API erstellen.

Einer der wichtigsten Gründe für den zweiten Ansatz ist, dass Cookies als Mittel zur Übertragung von Anmeldeinformationen verwendet werden können. Browserbasierte Clients senden automatisch geeignete Cookies innerhalb von XHR-Anforderungen, sodass kein zusätzlicher Autorisierungsheader erforderlich ist.

Ein weiterer wichtiger Grund ist, dass Sie nichts in Bezug auf Subdomain-Konfigurations- oder Verwaltungsprobleme unternehmen müssen, bei denen benutzerdefinierte Header möglicherweise von einigen Proxyservern entfernt werden. Dies kann bei Legacy-Projekten eine langwierige Tortur sein.

Die Verwendung von Cookies kann als „unRESTful“-Praxis angesehen werden, da REST-Anforderungen zustandslos sein sollten. In diesem Fall können wir einen Kompromiss eingehen und den Token-Wert in einem Cookie übergeben, anstatt ihn über einen benutzerdefinierten Header zu übergeben. Tatsächlich verwenden wir Cookies nur als eine Möglichkeit, den Token-Wert anstelle der session_id direkt zu übergeben. Dieser Ansatz könnte als zustandslos betrachtet werden, aber wir können es Ihren Vorlieben überlassen.

API-Endpunkt-URLs können auch versioniert werden. Außerdem können sie das erwartete Antwortformat als Erweiterung in den Pfadnamen aufnehmen. Obwohl diese nicht kritisch sind, insbesondere in den frühen Phasen der API-Entwicklung, können sich diese Details auf lange Sicht sicherlich auszahlen. Vor allem, wenn Sie neue Funktionen implementieren müssen. Die beste Lösung kann die Prüfung sein, welche Version der Client erwartet, und die Bereitstellung des erforderlichen Formats für die Abwärtskompatibilität.

Die URL-Struktur des API-Endpunkts könnte wie folgt aussehen:

 example.com/api/${version_code}/${actual_request_path}.${format}

Und ein echtes Beispiel:

 example.com/api/v1.0/records.json

Routing

Nachdem wir eine Basis-URL für die API-Endpunkte ausgewählt haben, müssen wir uns als Nächstes Gedanken über unser Routing-System machen. Es könnte in ein bestehendes Framework integriert werden, aber wenn das zu umständlich ist, besteht eine mögliche Problemumgehung darin, einen Ordner namens „api“ im Dokumentenstammverzeichnis zu erstellen. Auf diese Weise kann die API eine vollständig separate Logik haben. Sie können diesen Ansatz erweitern, indem Sie die API-Logik in eigenen Dateien platzieren, wie z. B.:

Sie können sich „www/api/Apis/Users.php“ als separaten „Controller“ für einen bestimmten API-Endpunkt vorstellen. Es wäre großartig, Implementierungen aus der vorhandenen Codebasis wiederzuverwenden, zum Beispiel Modelle wiederzuverwenden, die bereits im Projekt implementiert sind, um mit der Datenbank zu kommunizieren.

Stellen Sie abschließend sicher, dass alle eingehenden Anfragen von „/api/*“ auf „/api/index.php“ verweisen. Dies kann durch Ändern Ihrer Webserverkonfiguration erfolgen.

API-Klasse

Version und Format

Sie sollten immer klar definieren, welche Versionen und Formate Ihre API-Endpunkte akzeptieren und welche die Standardformate sind. Auf diese Weise können Sie in Zukunft neue Funktionen erstellen und gleichzeitig alte Funktionen beibehalten. Die API-Version kann grundsätzlich eine Zeichenfolge sein, aber Sie können numerische Werte zum besseren Verständnis und zur Vergleichbarkeit verwenden. Es ist gut, Ersatzziffern für Nebenversionen zu haben, da dies deutlich darauf hinweisen würde, dass nur wenige Dinge anders sind:

  • v1.0 würde erste Version bedeuten.
  • v1.1 erste Version mit einigen geringfügigen Änderungen.
  • v2.0 wäre eine komplett neue Version.

Das Format kann alles sein, was Ihr Kunde benötigt, einschließlich, aber nicht beschränkt auf JSON, XML und sogar CSV. Durch die Bereitstellung per URL als Dateierweiterung gewährleistet die API-Endpunkt-URL die Lesbarkeit und es wird für den API-Konsumenten zu einem Kinderspiel, zu wissen, welches Format er erwarten kann:

  • „/api/v1.0/records.json“ würde ein JSON-Array von Datensätzen zurückgeben
  • „/api/v1.0/records.xml“ würde eine XML-Datei mit Datensätzen zurückgeben

Es sei darauf hingewiesen, dass Sie für jedes dieser Formate auch einen richtigen Content-Type-Header in der Antwort senden müssen.

Wenn Sie eine eingehende Anfrage erhalten, sollten Sie als Erstes prüfen, ob der API-Server die angeforderte Version und das angeforderte Format unterstützt. Analysieren Sie in Ihrer Hauptmethode, die die eingehende Anfrage verarbeitet, $_SERVER['PATH_INFO'] oder $_SERVER['REQUEST_URI'] , um festzustellen, ob das angeforderte Format und die Version unterstützt werden. Fahren Sie dann entweder fort oder geben Sie eine 4xx-Antwort zurück (z. B. 406 „Not Acceptable“). Der wichtigste Teil hier ist, immer etwas zurückzugeben, was der Kunde erwartet. Eine Alternative dazu wäre, statt der URL-Pfaderweiterung den Request-Header „Accept“ zu prüfen.

Zulässige Routen

Sie könnten alles transparent an Ihre API-Controller weiterleiten, aber es könnte besser sein, einen Satz zulässiger Routen auf der Whitelist zu verwenden. Dies würde die Flexibilität etwas einschränken, bietet aber einen sehr klaren Einblick, wie die API-Endpunkt-URLs aussehen, wenn Sie das nächste Mal zum Code zurückkehren.

 private $public_routes = array( 'system' => array( 'regex' => 'system', ), 'records' => array( 'regex' => 'records(?:/?([0-9]+)?)', ), );

Sie können diese auch in separate Dateien verschieben, um die Dinge sauberer zu machen. Die obige Konfiguration wird verwendet, um Anfragen an diese URLs zu ermöglichen:

 /api/v1.0/system.json /api/v1.0/records.json /api/v1.0/records/7.json

Umgang mit PUT-Daten

PHP verarbeitet eingehende POST-Daten automatisch und platziert sie unter $_POST superglobal. Dies ist jedoch bei PUT-Anforderungen nicht der Fall. Alle Daten werden in php://input „begraben“. Vergessen Sie nicht, es in eine separate Struktur oder ein separates Array zu parsen, bevor Sie die eigentliche API-Methode aufrufen. Ein einfacher parse_str könnte ausreichen, aber wenn der Client eine mehrteilige Anfrage sendet, ist möglicherweise eine zusätzliche Analyse erforderlich, um Formulargrenzen zu handhaben. Ein typischer Anwendungsfall für mehrteilige Anfragen ist das Hochladen von Dateien. Das Erkennen und Verarbeiten von mehrteiligen Anfragen kann wie folgt erfolgen:

 self::$input = file_get_contents('php://input'); // For PUT/DELETE there is input data instead of request variables if (!empty(self::$input)) { preg_match('/boundary=(.*)$/', $_SERVER['CONTENT_TYPE'], $matches); if (isset($matches[1]) && strpos(self::$input, $matches[1]) !== false) { $this->parse_raw_request(self::$input, self::$input_data); } else { parse_str(self::$input, self::$input_data); } }

Hier könnte parse_raw_request wie folgt implementiert werden:

 /** * Helper method to parse raw requests */ private function parse_raw_request($input, &$a_data) { // grab multipart boundary from content type header preg_match('/boundary=(.*)$/', $_SERVER['CONTENT_TYPE'], $matches); $boundary = $matches[1]; // split content by boundary and get rid of last -- element $a_blocks = preg_split("/-+$boundary/", $input); array_pop($a_blocks); // loop data blocks foreach ($a_blocks as $id => $block) { if (empty($block)) { continue; } // parse uploaded files if (strpos($block, 'application/octet-stream') !== false) { // match "name", then everything after "stream" (optional) except for prepending newlines preg_match("/name=\"([^\"]*)\".*stream[\n|\r]+([^\n\r].*)?$/s", $block, $matches); // parse all other fields } else { // match "name" and optional value in between newline sequences preg_match('/name=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $block, $matches); } $a_data[$matches[1]] = $matches[2]; } }

Damit können wir die notwendige Anfragenutzlast bei Api::$input als Roheingabe und Api::$input_data als assoziatives Array haben.

PUT/DELETE vortäuschen

Manchmal können Sie sich in einer Situation sehen, in der der Server außer den standardmäßigen GET/POST-HTTP-Methoden nichts unterstützt. Eine gängige Lösung für dieses Problem besteht darin, PUT/DELETE oder eine andere benutzerdefinierte Anforderungsmethode zu „fälschen“. Dafür können Sie einen „magischen“ Parameter wie „_method“ verwenden. Wenn Sie es in Ihrem $_REQUEST- Array sehen, nehmen Sie einfach an, dass die Anfrage vom angegebenen Typ ist. Moderne Frameworks wie Laravel haben solche Funktionen eingebaut. Es bietet eine hervorragende Kompatibilität, falls Ihr Server oder Client Einschränkungen aufweist (z. B. verwendet eine Person das Wi-Fi-Netzwerk seines Jobs hinter einem Unternehmens-Proxy, der keine PUT-Anforderungen zulässt).

Weiterleitung an bestimmte API

Wenn Sie nicht den Luxus haben, vorhandene Projekt-Autoloader wiederzuverwenden, können Sie mit Hilfe der Funktion spl_autoload_register Ihre eigenen erstellen. Definieren Sie es auf Ihrer Seite „api/index.php“ und rufen Sie Ihre API-Klasse auf, die sich in „api/Api.php“ befindet. Die API-Klasse fungiert als Middleware und ruft die eigentliche Methode auf. Beispielsweise sollte eine Anfrage an „/api/v1.0/records/7.json“ mit dem Aufruf der GET-Methode „Apis/Records.php“ mit Parameter 7 enden. Dies würde die Trennung von Bedenken sicherstellen und eine Möglichkeit bieten, die Logikreiniger. Wenn es möglich ist, dies tiefer in das von Ihnen verwendete Framework zu integrieren und seine spezifischen Controller oder Routen wiederzuverwenden, sollten Sie diese Möglichkeit natürlich auch in Betracht ziehen.

Beispiel „api/index.php“ mit primitivem Autoloader:

 <?php // Let's define very primitive autoloader spl_autoload_register(function($classname){ $classname = str_replace('Api_', 'Apis/', $classname); if (file_exists(__DIR__.'/'.$classname.'.php')) { require __DIR__.'/'.$classname.'.php'; } }); // Our main method to handle request Api::serve();

Dadurch wird unsere API -Klasse geladen und unabhängig vom Hauptprojekt bereitgestellt.

OPTIONEN Anfragen

Wenn ein Client einen benutzerdefinierten Header verwendet, um sein eindeutiges Token weiterzuleiten, muss der Browser zuerst prüfen, ob der Server diesen Header unterstützt. Hier kommt die OPTIONS-Anfrage ins Spiel. Ihr Zweck besteht darin, sicherzustellen, dass sowohl für den Client als auch für den API-Server alles in Ordnung und sicher ist. Die OPTIONS-Anforderung könnte also jedes Mal ausgelöst werden, wenn ein Client versucht, etwas zu tun. Wenn ein Client jedoch Cookies für Anmeldeinformationen verwendet, erspart es dem Browser, diese zusätzliche OPTIONS-Anfrage zu senden.

Wenn ein Client POST /users/8.json mit Cookies anfordert, ist seine Anfrage ziemlich normal:

  • Die App führt eine POST-Anforderung an /users/8.json aus.
  • Der Browser führt die Anfrage aus und erhält eine Antwort.

Aber mit benutzerdefinierter Autorisierung oder Token-Header:

  • Die App führt eine POST-Anforderung an /users/8.json aus.
  • Der Browser beendet die Verarbeitung der Anforderung und initiiert stattdessen eine OPTIONS-Anforderung.
  • Die OPTIONS-Anforderung wird an /users/8.json gesendet.
  • Der Browser erhält eine Antwort mit einer Liste aller verfügbaren Methoden und Header, wie von der API definiert.
  • Der Browser fährt nur dann mit der ursprünglichen POST-Anforderung fort, wenn der benutzerdefinierte Header in der Liste der verfügbaren Header vorhanden ist.

Beachten Sie jedoch, dass Sie mit PUT/DELETE auch bei der Verwendung von Cookies möglicherweise diese zusätzliche OPTIONS-Anfrage erhalten. Seien Sie also bereit, darauf zu reagieren.

Datensätze-API

Grundstruktur

Unser Beispiel für die Records-API ist ziemlich einfach. Es enthält alle Anforderungsmethoden und gibt die Ausgabe an dieselbe Haupt-API-Klasse zurück. Zum Beispiel:

 <?php class Api_Records { public function __construct() { // In here you could initialize some shared logic between this API and rest of the project } /** * Get individual record or records list */ public function get($id = null) { if ($id) { return $this->getRecord(intval($id)); } else { return $this->getRecords(); } } /** * Update record */ public function put($record_id = null) { // In real world there would be call to model with validation and probably token checking // Use Api::$input_data to update return Api::responseOk('OK', array()); } // ...

Das Definieren jeder HTTP-Methode ermöglicht es uns also, APIs einfacher im REST-Stil zu erstellen.

Ausgabe formatieren

Naiv mit allem zu antworten, was aus der Datenbank an den Client zurückgesendet wurde, kann katastrophale Folgen haben. Um eine versehentliche Offenlegung von Daten zu vermeiden, erstellen Sie eine spezielle Formatmethode, die nur Schlüssel auf der weißen Liste zurückgibt.

Ein weiterer Vorteil von Schlüsseln auf der weißen Liste besteht darin, dass Sie auf dieser Grundlage Dokumentation schreiben und alle Typprüfungen durchführen können, um beispielsweise sicherzustellen, dass user_id immer eine Ganzzahl ist, Flag is_banned immer boolesch wahr oder falsch ist und Datums- und Uhrzeitangaben einen Standard haben Antwortformat.

Ergebnisse ausgeben

Überschriften

Separate Methoden für die Ausgabe von Headern stellen sicher, dass alles, was an den Browser gesendet wird, korrekt ist. Diese Methode kann die Vorteile nutzen, die API über dieselbe Domäne zugänglich zu machen, während weiterhin die Möglichkeit besteht, benutzerdefinierte Autorisierungsheader zu erhalten. Die Wahl zwischen der gleichen oder einer 3rd-Party-Domain kann mit Hilfe von HTTP_ORIGIN- und HTTP_REFERER-Server-Headern erfolgen. Wenn die App erkennt, dass der Client die X-Autorisierung (oder einen anderen benutzerdefinierten Header) verwendet, sollte sie den Zugriff von allen Ursprüngen zulassen, den benutzerdefinierten Header zulassen. Es könnte also so aussehen:

 header('Access-Control-Allow-Origin: *'); header('Access-Control-Expose-Headers: x-authorization'); header('Access-Control-Allow-Headers: origin, content-type, accept, x-authorization'); header('X-Authorization: '.YOUR_TOKEN_HERE);

Wenn der Client jedoch Cookie-basierte Anmeldeinformationen verwendet, könnten die Header etwas anders sein und nur angeforderte Host- und Cookie-bezogene Header für Anmeldeinformationen zulassen:

 header('Access-Control-Allow-Origin: '.$origin); header('Access-Control-Expose-Headers: set-cookie, cookie'); header('Access-Control-Allow-Headers: origin, content-type, accept, set-cookie, cookie'); // Allow cookie credentials because we're on the same domain header('Access-Control-Allow-Credentials: true'); if (strtolower($_SERVER['REQUEST_METHOD']) != 'options') { setcookie(TOKEN_COOKIE_NAME, YOUR_TOKEN_HERE, time()+86400*30, '/', '.'.$_SERVER['HTTP_HOST']); }

Denken Sie daran, dass die OPTIONS-Anfrage keine Cookies unterstützt, sodass die App sie nicht mitsendet. Und schließlich ermöglicht dies allen unseren gewünschten HTTP-Methoden, dass die Zugriffskontrolle abläuft:

 header('Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, DELETE'); header('Access-Control-Max-Age: 86400');

Körper

Der Hauptteil selbst sollte die Antwort in einem Format enthalten, das von Ihrem Client angefordert wird, mit einem 2xx-HTTP-Status bei Erfolg, einem 4xx-Status bei einem Fehler aufgrund des Clients und einem 5xx-Status bei einem Fehler aufgrund eines Servers. Die Struktur der Antwort kann variieren, obwohl die Angabe der Felder „Status“ und „Antwort“ ebenfalls von Vorteil sein könnte. Wenn der Client beispielsweise versucht, einen neuen Benutzer zu registrieren, und der Benutzername bereits vergeben ist, könnten Sie eine Antwort mit dem HTTP-Status 200, aber einem JSON im Text senden, der in etwa so aussieht:

 {“status”: “ERROR”, “response”: ”username already taken”}

… statt HTTP 4xx Fehler direkt.

Fazit

Keine zwei Projekte sind genau gleich. Die in diesem Artikel beschriebene Strategie kann für Ihren Fall gut geeignet sein oder auch nicht, aber die Kernkonzepte sollten dennoch ähnlich sein. Es ist erwähnenswert, dass nicht hinter jeder Seite die neuesten Trends oder das aktuellste Framework stehen können, und manchmal kann die Wut darüber, „warum mein REST-Symfony-Bundle hier nicht funktioniert“, in eine Motivation umgewandelt werden, etwas Nützliches zu bauen. etwas, das funktioniert. Das Endergebnis ist vielleicht nicht so glänzend, da es immer eine kundenspezifische und projektspezifische Implementierung sein wird, aber am Ende des Tages wird die Lösung etwas sein, das wirklich funktioniert; und in einem Szenario wie diesem sollte das das Ziel eines jeden API-Entwicklers sein.

Beispielimplementierungen der hier besprochenen Konzepte wurden der Einfachheit halber in ein GitHub-Repository hochgeladen. Möglicherweise möchten Sie diese Beispielcodes nicht direkt in der Produktion verwenden, aber dies könnte leicht als Ausgangspunkt für Ihr nächstes Legacy-PHP-API-Integrationsprojekt dienen.

Mussten Sie kürzlich einen REST-API-Server für ein Legacy-Projekt implementieren? Teilen Sie Ihre Erfahrungen mit uns im Kommentarbereich unten.