Ich habe in der FoxPro-Community, auf Patreon und in der WhatsApp-Gruppe, eine Umfrage mit einer einzigen Frage gemacht: Was ist heute das Schwierigste an Ihrer VFP-Anwendung? Zwei von drei antworteten dasselbe: sie im Web oder als API bereitzustellen. Nicht die schweren Prozesse, nicht die künstliche Intelligenz. Die Daten und die Logik, die sie bereits haben, in etwas zu bringen, das ein Browser, eine mobile App oder ein externer Kunde aufrufen kann.
Dieses Tutorial ist die kurze Antwort auf diese Frage. Am Ende haben Sie eine REST-API, bereitgestellt von FoxServer, die eine DBF-Tabelle als JSON zurückgibt und per POST einen neuen Datensatz annimmt. Der Geschäftscode ist ganz normales VFP 9, und Sie fassen die Anwendung, die Sie bereits haben, nicht an: Sie veröffentlichen sie.
Was Sie brauchen
- Visual FoxPro 9 SP2, installiert. FoxServer kompiliert Ihr Projekt damit.
- FoxServer, installiert. Das Setup legt den Server in
bin\, das Kommandozeilenwerkzeug incli\und die FoxForge-Werkstatt für die IDE inide\ab. Die 30-Tage-Testlizenz, die bei der Registrierung im Portal ausgestellt wird, reicht für alles hier. - Eine PowerShell-Konsole als Administrator für den Kompilierschritt. Nur für diesen. Warum, erkläre ich weiter unten.
curl, das Windows bereits mitbringt, oder ein beliebiger HTTP-Client.
Ich verwende die Kommandozeile, weil sich jeder Schritt einfügen und wiederholen lässt. Wenn Ihnen die VFP-IDE mit ihren Menüs lieber ist: Es gibt dasselbe Tutorial Schritt für Schritt aus der IDE, mit FoxForge und jedem Bildschirm, den Sie sehen werden.
Drei Dinge, die man verstehen muss, bevor man eine Zeile schreibt
Wenn Sie aus diesem Artikel nur einen Kasten lesen, dann diesen.
1. Ihr Code bleibt Ihrer. Er wird nur veröffentlicht. FoxServer übersetzt nichts und zwingt Ihnen kein Framework auf. Eine PROCEDURE einer VFP-Klasse wird zum HTTP-Endpunkt, indem man eine HELP-Klausel deklariert:
PROCEDURE pHallo(req, res) HELP "GET: hallo public"
LOCAL loRes
loRes = THIS.newObject("meldung")
loRes.meldung = "hallo"
RETURN THIS.Ok(loRes)
ENDPROCDas ist eine API. Der Rest des Artikels ist Detail.
2. Die Route existiert erst, wenn Sie kompilieren. Die Route entsteht in dieser HELP-Klausel und erreicht den Server erst, wenn der Build-Hook sie beim Kompilieren ausliest. Das ist Ursache Nummer eins für „ich habe den Endpunkt hinzugefügt und bekomme ein 404“. Wenn Sie eine Route hinzufügen und sie nicht auftaucht: Es fehlt das Kompilieren.
3. Der Aufruf ist synchron. Ein Endpunkt, der zwei Sekunden braucht, braucht zwei Sekunden und belegt so lange seinen Platz. Für Abfragen ist das bequem; bei einem Vierzig-Sekunden-Prozess gibt man eine Kennung zurück und erledigt die Arbeit an anderer Stelle.
Schritt 1. Das Projekt anlegen
Nehmen Sie den Ordner cli\ Ihrer FoxServer-Installation in den PATH auf und öffnen Sie PowerShell. Wenn Sie den PATH lieber nicht anfassen, wechseln Sie in diesen Ordner und schreiben .\foxserver statt foxserver: PowerShell führt ohne dieses .\ nichts aus dem aktuellen Verzeichnis aus, und ohne es antwortet der erste Befehl mit „wird nicht als Name eines Cmdlet erkannt“. Ab hier schreibe ich schlicht foxserver.
foxserver init --name Lager --path C:\projekte\lager --port 8099 --format json{
"created": true,
"name": "Lager",
"path": "C:\\projekte\\lager\\"
}Das legt den Ordnerbaum, die .pjx und das Server-Manifest in build\ an. Beachten Sie, dass das Manifest vor dem ersten Kompilieren existiert: Es sorgt dafür, dass die Abfragebefehle ab sofort funktionieren.
In src\controllers\ finden Sie außerdem einen DemoController.prg mit sieben Beispiel-Endpunkten (hello, users, search…). init legt ihn an, damit das Projekt von der ersten Minute an etwas ausliefert. Er gehört nicht zu Ihrem Code: Wenn Sie mit dem Tutorial fertig sind, löschen Sie ihn und kompilieren neu, dann verschwinden seine Routen.
Schritt 2. Ein Controller und sein erster Endpunkt
foxserver controller-add --project C:\projekte\lager --name Artikel --format json
foxserver endpoint-add --project C:\projekte\lager --controller Artikel `
--method get --route artikel --proc pArtikelListe --public --format jsonDer zweite Befehl schreibt in src\controllers\Artikel.prg eine Prozedur mit ihrer HELP-Klausel und verknüpft sie mit dem Projekt. Öffnen Sie die Datei. Darin steht eine VFP-Klasse, die von ApiController erbt und genau so heißt wie das --name, das Sie übergeben haben, ohne Suffix:
define class Artikel as ApiController olepublic
procedure pArtikelListe(req, res) help "GET: artikel public"
local loRes
loRes = this.newobject("status,data,message")
loRes.status = "ok"
loRes.data = null
loRes.message = "pArtikelListe is alive"
res.status(200).json(this.toJson(loRes))
endproc
enddefineDieser Beispielrumpf zeigt die lange Form: Objekt aufbauen, serialisieren, mit dem gewünschten Statuscode antworten. Im Rest des Artikels verwende ich die kurze Form, THIS.Ok(objekt), die dasselbe in einer Zeile tut. Beide existieren nebeneinander.
Die HELP-Klausel liest sich so: "<VERB>: <route> [public]". Mit public verlangt der Endpunkt kein Token. Ohne, und wenn Authentifizierung konfiguriert ist, wird eines nötig. Und ein Hinweis, der einen Nachmittag spart: Die HELP-Klausel darf 255 Zeichen nicht überschreiten. Bei 256 kompiliert VFP nicht, und die Fehlermeldung erwähnt die Länge mit keinem Wort.
Schritt 3. Eine ganze Tabelle zurückgeben
Vor dem Code die Tabelle, denn hier bleiben alle hängen, und nicht dort, wo man es erwarten würde.
Wo sie liegt. Der Server löst ein USE artikel ohne Pfad gegen den Ordner build\ des Projekts auf, in dem die DLL liegt. Liegt die Tabelle nicht dort, liefert die erste Anfrage ein 500, das wenigstens die genaue Datei nennt, nach der gesucht wurde (der Wortlaut stammt von VFP selbst und erscheint daher in der Sprache Ihres VFP; meines spricht Spanisch):
{
"type": "about:blank",
"title": "Internal Server Error",
"status": 500,
"detail": "Internal server error: Error: .beforeendpoint c:\\projekte\\lager\\build\\src\\controllers\\artikel.prg Error en la línea 9 El archivo 'c:\\projekte\\lager\\build\\artikel.dbf' no existe. 1, Error Code: 1",
"instance": "/artikel"
}Ist es die Tabelle Ihrer Anwendung, die schon existiert und irgendwo liegt, öffnen Sie sie mit vollem Pfad oder setzen ein SET PATH in das BeforeEndpoint weiter unten.
Und wenn Sie eine zum Ausprobieren anlegen, legen Sie sie in einer Datenbank an, nicht als freie Tabelle: bezeichnung hat elf Buchstaben, und eine freie Tabelle akzeptiert keine Feldnamen mit mehr als zehn. Sie antwortet mit Fehler 1712, der von doppelten Namen spricht und die Länge nicht erwähnt. Und code muss die 32 Zeichen fassen, die NewGuid() in Schritt 5 liefert: Mit weniger speichert der Insert einen abgeschnittenen Code und gibt Ihnen den vollständigen zurück, sodass die Kennung, die der Client erhält, in der Tabelle nicht existiert.
CD C:\projekte\lager\build
CREATE DATABASE lagerdaten
CREATE TABLE artikel (code C(32), bezeichnung C(60), preis N(10,2), bestand N(10,0), aktiv L)
INSERT INTO artikel VALUES ("A001", "Schraube M6", 0.42, 1200, .T.)
INSERT INTO artikel VALUES ("A002", "Unterlegscheibe M6", 0.11, 3400, .T.)Jetzt kommt Ihre Anwendung ins Spiel. Ersetzen Sie den Rumpf von pArtikelListe durch eine Abfrage auf Ihre Tabelle und einen einzigen Aufruf, der den Cursor in JSON verwandelt:
PROCEDURE pArtikelListe(req, res) HELP "GET: artikel public as artikel.liste"
LOCAL lcJson
SELECT code, bezeichnung, preis, bestand ;
FROM artikel ;
WHERE aktiv ;
ORDER BY bezeichnung ;
INTO CURSOR art
lcJson = THIS.TableToJson("art")
USE IN SELECT("art")
RETURN THIS.OkRaw(lcJson)
ENDPROCTableToJson() ist die Funktion, die neunzig Prozent der Arbeit einer Abfrage-API erspart: Ein Cursor geht hinein, ein JSON-Array kommt heraus. Zwei Details, die nicht offensichtlich sind:
OkRaw, nichtOk.TableToJson()liefert bereits JSON. Geben Sie es anOk(), wird es ein zweites Mal serialisiert und kommt als escapte Zeichenkette an.Ok()mit einer Zeichenkette serialisiert nichts: Es liefert{}. Übergeben Sie immer ein Objekt, oder verwenden SieOkRaw(), wenn das JSON schon fertig ist.
Die Tabelle artikel öffnen Sie so, wie Sie sie schon immer geöffnet haben. Hat Ihre Anwendung eine Startroutine, die Pfade und SETs einstellt, ist BeforeEndpoint der Ort, sie aufzurufen; es läuft vor jeder Anfrage, und AfterEndpoint räumt auf. Zum Beispiel:
FUNCTION BeforeEndpoint(req, res)
SET DELETED ON
SET EXCLUSIVE OFF
USE artikel IN 0 SHARED AGAIN
ENDFUNC
PROCEDURE AfterEndpoint(req, res)
CLOSE DATABASES ALL
ENDPROCSchritt 4. Lesen, was hereinkommt: ein Artikel nach seinem Code
Ein zweiter Endpunkt mit einem Parameter in der Route:
PROCEDURE pArtikel(req, res) HELP "GET: artikel/{code} public as artikel.eins"
LOCAL lcCode, loArt
lcCode = req.GetParam("code", "")
IF EMPTY(lcCode)
RETURN THIS.BadRequest("Der Artikelcode fehlt")
ENDIF
SELECT artikel
LOCATE FOR ALLTRIM(artikel.code) == ALLTRIM(lcCode)
IF !FOUND()
RETURN THIS.NotFound("Es gibt keinen Artikel " + lcCode)
ENDIF
loArt = THIS.NewObject("code,bezeichnung,preis,bestand")
loArt.code = ALLTRIM(artikel.code)
loArt.bezeichnung = ALLTRIM(artikel.bezeichnung)
loArt.preis = artikel.preis
loArt.bestand = artikel.bestand
RETURN THIS.Ok(loArt)
ENDPROCAchten Sie auf den Alias: Gesucht wird in artikel, der Tabelle, die BeforeEndpoint öffnet, und nicht im Cursor art aus dem vorigen Schritt. Dieser Cursor entsteht und stirbt innerhalb von pArtikelListe; ihn hier zu verwenden, liefert ein 500 mit „No se encuentra el alias ‘ART’“, einer dieser Fehler, die eine Weile kosten, weil der Code sich einwandfrei liest.
{code} in der Route kommt über req.GetParam() an. Das zweite Argument ist der Standardwert: Ohne ihn liefert eine Anfrage, der dieses Stück fehlt, eine leere Zeichenkette, und Sie merken es spät. Was req sonst noch mitbringt: req.GetQuery("q", "") für den Query-String, req.GetHeader() für Header, req.json für den Body und req.AuthenticatedUser für den angemeldeten Benutzer.
Es gibt neun Ergebnisse, die Sie zurückgeben können, und sie heißen, wie man es erwartet: Ok, OkRaw, Created, NoContent, BadRequest, Unauthorized, Forbidden, NotFound und Fail(status, meldung, code) für alles andere.
Created nimmt außerdem ein zweites Argument an, die URI der gerade angelegten Ressource, und dann geht das 201 mit einem Location-Header hinaus: RETURN THIS.Created(loNeu, "/artikel/" + loNeu.code). Mit nur einem Argument, wie in Schritt 5, geht das 201 ohne ihn hinaus.
Schritt 5. Einen neuen Datensatz annehmen
PROCEDURE pAnlegen(req, res) HELP "POST: artikel public as artikel.anlegen"
LOCAL loDaten, loNeu, lcBez
loDaten = req.json
IF ISNULL(loDaten) OR VARTYPE(loDaten) != "O"
RETURN THIS.BadRequest("Der Body muss ein JSON-Objekt sein", "ungueltiger_body")
ENDIF
lcBez = ""
TRY
lcBez = ALLTRIM(TRANSFORM(loDaten.bezeichnung))
CATCH
lcBez = ""
ENDTRY
IF EMPTY(lcBez)
RETURN THIS.BadRequest("Die Bezeichnung fehlt", "bezeichnung_fehlt")
ENDIF
loNeu = THIS.NewObject("code,bezeichnung")
loNeu.code = THIS.NewGuid()
loNeu.bezeichnung = lcBez
INSERT INTO artikel (code, bezeichnung, aktiv) ;
VALUES (loNeu.code, loNeu.bezeichnung, .T.)
RETURN THIS.Created(loNeu)
ENDPROCreq.json bringt den Body bereits geparst. Validieren Sie immer: Was über HTTP hereinkommt, hat nicht Ihr Formular geschrieben. Und eine konkrete Falle: Verwenden Sie PEMSTATUS() auf req.json nicht, um zu prüfen, ob ein Feld vorhanden ist. Es funktioniert in der Produktion und scheitert auf dem Prüfstand, wo das Objekt über COM reist. Der direkte Zugriff in einem TRY, wie oben, funktioniert in beiden Welten.
Schritt 6. Kompilieren. Die einzige Hürde
foxserver build --project C:\projekte\lager --format jsonAus einer normalen Konsole wird das nicht funktionieren, und das ist Absicht. Sie sehen Folgendes (die Meldungen der CLI sind derzeit auf Spanisch; diese besagt, dass zum Kompilieren eine Konsole mit erhöhten Rechten nötig ist, weil BUILD MTDLL am Ende die COM-Klasse registriert und ohne Rechte mit Fehler 1640 scheitert, was eine DLL ohne Typbibliothek hinterlässt, und dass nichts verändert wurde):
foxserver: hace falta una consola ELEVADA para compilar.
BUILD MTDLL registra la coclase en COM al terminar, y sin
permisos falla con el error 1640 dejando una DLL sin type
library. Compila, pesa una decima parte y el motor no puede
activar nada de ella.
No se ha tocado nada. Abre una consola como administrador y
repite el comando.Öffnen Sie PowerShell als Administrator und wiederholen Sie den Befehl. Ohne --format json erscheint am Ende womöglich eine Warnung build.register: Das ist kein Fehler, die DLL ist kompiliert. Jetzt existieren die Endpunkte für den Server:
foxserver endpoint-list --project C:\projekte\lagerSie sehen zehn Routen, nicht drei: Die sieben zusätzlichen gehören zum DemoController, den init angelegt hat.
Sehen Sie statt der Liste einen Kompilierfehler, wird er mit Datei und Zeile ausgegeben, entnommen aus der .ERR-Datei von VFP. Korrigieren und wiederholen.
Schritt 7. Bereitstellen und testen
foxserver serve --project C:\projekte\lager --format jsonDer Server startet, gibt Ihnen die pid zurück und kehrt zur Konsole zurück. Und jetzt der Test.
Eine Sache vor dem ersten curl: In PowerShell ist curl ein Alias für Invoke-WebRequest, das weder -X noch -d versteht. Schreiben Sie curl.exe, mit Endung, dann rufen Sie das Programm curl auf. Aus cmd oder bash ist schlichtes curl bereits das richtige.
curl.exe http://127.0.0.1:8099/artikel[{
"code": "A001",
"bezeichnung": "Schraube M6",
"preis": 0.42,
"bestand": 1200
},{
"code": "A002",
"bezeichnung": "Unterlegscheibe M6",
"preis": 0.11,
"bestand": 3400
}]Die Ausgabe kommt mit Zeilenumbrüchen und nach bezeichnung sortiert, wie es die Abfrage verlangt: „Schraube“ vor „Unterlegscheibe“.
curl.exe http://127.0.0.1:8099/artikel/A001{
"code": "A001",
"bezeichnung": "Schraube M6",
"preis": 0.42,
"bestand": 1200
}Und wenn Sie einen anfordern, den es nicht gibt, sieht das NotFound aus Schritt 4 so aus:
curl.exe -i http://127.0.0.1:8099/artikel/ZZZZHTTP/1.1 404 Not Found
Content-Type: application/problem+json; charset=utf-8
{"type":"about:blank","title":"Not Found","status":404,
"detail":"Es gibt keinen Artikel ZZZZ","instance":"/artikel/ZZZZ",
"code":"not_found"}Das ist Problem Details, RFC 9457: das Format, mit dem ASP.NET und Co. einen Fehler beschreiben und das Postman oder ein Browser bereits lesen können. Das detail ist Ihre Meldung, genau so, wie Sie sie in THIS.NotFound(...) geschrieben haben; der code ist Ihrer, falls Sie einen übergeben haben. Ein 200 trägt keinerlei Umschlag: Es kommt so heraus, wie Sie es oben gesehen haben.
Und der neue Datensatz. Das --% ist keine Dekoration: Ohne es verschluckt PowerShell die Anführungszeichen innerhalb von -d, der Body kommt in losen Stücken an, und curl versucht, sie als URLs zu öffnen.
curl.exe --% -X POST http://127.0.0.1:8099/artikel -H "Content-Type: application/json" -d "{\"bezeichnung\":\"Mutter M6\"}"{
"code": "6572b2099f784487a23c37e5089595e9",
"bezeichnung": "Mutter M6"
}Aus cmd oder bash dieselbe Zeile ohne das --%.
Ein Detail für den Moment, in dem andere Rechner Sie aufrufen sollen. Das Projekt entsteht mit "host": "127.0.0.1" im Manifest, von außen kommt also nicht einmal eine Verbindung zustande: Das ist nicht die Lizenz, das ist das Manifest. Und wenn Sie es auf die IP des Rechners ändern, sehen Sie das Zweite: Bekommen Sie statt 200 ein 403, liegt es nicht an Ihrem Endpunkt, sondern an der Lizenz. Eine Developer-Lizenz nimmt nur Anfragen vom eigenen Rechner an. Um andere zu bedienen, braucht es eine Runtime-Lizenz, die auf dem Server des Kunden eingesetzt wird. In der Entwicklung brauchen Sie sie nicht.
Was Sie getan haben, und was nicht
Sie haben eine Tabelle Ihrer Anwendung als REST-API mit drei Endpunkten veröffentlicht, ohne die Logik neu zu schreiben, ohne die Sprache zu wechseln und ohne ein Framework dazwischen. Was Sie noch nicht getan haben, ist, sie in Produktion zu bringen: HTTPS-Zertifikat, Windows-Dienst, Logs und wie man eine neue Version ausrollt, ohne den Server herunterzufahren. Das ist das nächste Tutorial, denn dort bleiben die Leute, die bereits Systeme in Produktion betreiben, am häufigsten hängen.
Bis dahin zwei Befehle, die ohne laufenden Server arbeiten und die man vom ersten Tag an kennen sollte:
foxserver doctor --project C:\projekte\lager # was fehlt und wo es angreifbar ist
foxserver openapi --project C:\projekte\lager # Ihr API-Vertrag, bereit für Swagger oder PostmanWenn hier etwas nicht so funktioniert, wie es geschrieben steht, schreiben Sie an soporte@irwinrodriguez.dev, mit dem Befehl und der Ausgabe. Jede Ausgabe in diesem Artikel ist aus einem echten Lauf kopiert, und wenn Ihre anders aussieht, möchte ich es wissen.