Crearea API-ului REST pentru proiectele PHP vechi

Publicat: 2022-03-11

Construirea sau arhitectura unui API REST nu este o sarcină ușoară, mai ales atunci când trebuie să o faceți pentru proiecte PHP vechi. În prezent, există o mulțime de biblioteci terță parte care facilitează implementarea unui API REST, dar integrarea lor în bazele de cod vechi existente poate fi destul de descurajantă. Și nu aveți întotdeauna luxul de a lucra cu cadre moderne, cum ar fi Laravel și Symfony. Cu proiectele PHP vechi, vă puteți găsi adesea undeva în mijlocul cadrelor interne depreciate, rulând peste versiunile vechi de PHP.

Crearea API-ului REST pentru proiectele PHP vechi

Crearea API-ului REST pentru proiectele PHP vechi
Tweet

În acest articol, vom arunca o privire asupra unor provocări comune ale încercării de a implementa API-urile REST de la zero, câteva modalități de a rezolva aceste probleme și o strategie generală pentru construirea de servere API personalizate bazate pe PHP pentru proiectele vechi.

Deși articolul se bazează pe PHP 5.3 și mai sus, conceptele de bază sunt valabile pentru toate versiunile de PHP dincolo de versiunea 5.0 și pot fi aplicate chiar și proiectelor non-PHP. Aici, nu vom acoperi ce este un API REST în general, așa că dacă nu sunteți familiarizat cu el, asigurați-vă că citiți mai întâi despre el.

Pentru a vă facilita urmărirea, iată o listă cu câțiva termeni folosiți în acest articol și semnificațiile acestora:

  • Server API: aplicația REST principală care deservește API-ul, în acest caz, scrisă în PHP.
  • Punct final API: o „metodă” backend cu care clientul comunică pentru a efectua o acțiune și a produce rezultate.
  • Adresa URL a punctului final API: URL prin care sistemul de backend este accesibil lumii.
  • Token API: un identificator unic transmis prin anteturi HTTP sau cookie-uri din care poate fi identificat utilizatorul.
  • Aplicație: aplicație client care va comunica cu aplicația REST prin punctele finale API. În acest articol vom presupune că este bazat pe web (fie desktop, fie mobil), și deci este scris în JavaScript.

Pași inițiali

Modele de cale

Unul dintre primele lucruri pe care trebuie să le decidem este în ce cale URL vor fi disponibile punctele finale API. Există 2 moduri populare:

  • Creați un nou subdomeniu, cum ar fi api.example.com.
  • Creați o cale, cum ar fi example.com/api.

La o privire, poate părea că prima variantă este mai populară și mai atractivă. În realitate, totuși, dacă construiți un API specific pentru proiect, ar putea fi mai potrivit să alegeți a doua variantă.

Unul dintre cele mai importante motive din spatele adoptării celei de-a doua abordări este că aceasta permite utilizarea cookie-urilor ca mijloc de transfer de acreditări. Clienții bazați pe browser vor trimite automat cookie-uri adecvate în cadrul solicitărilor XHR, eliminând necesitatea unui antet de autorizare suplimentar.

Un alt motiv important este că nu trebuie să faceți nimic în ceea ce privește configurarea subdomeniului sau problemele de gestionare în care anteturile personalizate pot fi eliminate de unele servere proxy. Aceasta poate fi o încercare obositoare în proiectele moștenite.

Utilizarea cookie-urilor poate fi considerată o practică „ne REST”, deoarece solicitările REST ar trebui să fie apatride. În acest caz, putem face un compromis și putem trece valoarea token-ului într-un cookie în loc să o transmitem printr-un antet personalizat. În mod efectiv, folosim cookie-uri doar ca o modalitate de a transmite valoarea token-ului în loc de session_id direct. Această abordare ar putea fi considerată apatridă, dar o putem lăsa la latitudinea preferințelor dumneavoastră.

Adresele URL ale punctelor finale API pot fi, de asemenea, versionate. În plus, ele pot include formatul de răspuns așteptat ca extensie în numele căii. Deși acestea nu sunt esențiale, mai ales în primele etape ale dezvoltării API-ului, pe termen lung, aceste detalii cu siguranță pot da roade. Mai ales când trebuie să implementați noi funcții. Verificarea versiunii pe care clientul se așteaptă și furnizarea formatului necesar pentru compatibilitatea cu versiunea inversă poate fi cea mai bună soluție.

Structura URL a punctului final API ar putea arăta după cum urmează:

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

Și, un exemplu real:

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

Dirijare

După ce am ales o adresă URL de bază pentru punctele finale API, următorul lucru pe care trebuie să-l facem este să ne gândim la sistemul nostru de rutare. Ar putea fi integrat într-un cadru existent, dar dacă acest lucru este prea greoi, o posibilă soluție este să creați un folder numit „api” în rădăcina documentului. În acest fel, API-ul poate avea o logică complet separată. Puteți extinde această abordare plasând logica API în propriile fișiere, cum ar fi acesta:

Vă puteți gândi la „www/api/Apis/Users.php” ca la un „controller” separat pentru un anumit punct final API. Ar fi grozav să reutilizați implementările din baza de cod existentă, de exemplu reutilizarea modelelor care sunt deja implementate în proiect pentru a comunica cu baza de date.

În cele din urmă, asigurați-vă că direcționați toate solicitările primite de la „/api/*” la „/api/index.php”. Acest lucru se poate face prin modificarea configurației serverului dvs. web.

Clasa API

Versiune și format

Ar trebui să definiți întotdeauna clar ce versiuni și formate acceptă punctele finale API și care sunt cele implicite. Acest lucru vă va permite să construiți noi funcții în viitor, păstrând în același timp funcționalitățile vechi. Versiunea API poate fi practic un șir, dar puteți utiliza valori numerice pentru o mai bună înțelegere și comparabilitate. Este bine să aveți cifre de rezervă pentru versiunile minore, deoarece ar indica în mod clar că doar câteva lucruri sunt diferite:

  • v1.0 ar însemna prima versiune.
  • v1.1 prima versiune cu câteva modificări minore.
  • v2.0 ar fi o versiune complet nouă.

Formatul poate fi orice are nevoie clientul dvs., inclusiv dar fără a se limita la JSON, XML și chiar CSV. Prin furnizarea acestuia prin intermediul URL-ului ca extensie de fișier, adresa URL a punctului final API asigură lizibilitatea și devine o idee simplă pentru consumatorul API să știe la ce format se poate aștepta:

  • „/api/v1.0/records.json” ar returna o matrice JSON de înregistrări
  • „/api/v1.0/records.xml” ar returna fișierul XML de înregistrări

Merită să subliniați că va trebui, de asemenea, să trimiteți un antet Content-Type în răspunsul pentru fiecare dintre aceste formate.

La primirea unei solicitări, unul dintre primele lucruri pe care ar trebui să le faceți este să verificați dacă serverul API acceptă versiunea și formatul solicitate. În metoda dvs. principală, care se ocupă de cererea de intrare, analizați $_SERVER['PATH_INFO'] sau $_SERVER['REQUEST_URI'] pentru a determina dacă formatul și versiunea solicitate sunt acceptate. Apoi, fie continuați, fie returnați un răspuns 4xx (de ex. 406 „Inacceptabil”). Partea cea mai critică aici este să returnezi întotdeauna ceva la care clientul se așteaptă. O alternativă la aceasta ar fi să verificați antetul cererii „Accept” în loc de extensia căii URL.

Rute permise

Puteți redirecționa totul în mod transparent către controlerele dvs. API, dar ar putea fi mai bine să utilizați un set de rute permise pe lista albă. Acest lucru ar reduce puțin flexibilitatea, dar va oferi o perspectivă foarte clară despre cum arată adresele URL ale punctelor finale API data viitoare când reveniți la cod.

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

De asemenea, le puteți muta în fișiere separate pentru a face lucrurile mai curate. Configurația de mai sus va fi utilizată pentru a activa solicitările către aceste adrese URL:

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

Manipularea datelor PUT

PHP gestionează automat datele POST primite și le plasează sub $_POST superglobal. Cu toate acestea, acesta nu este cazul cererilor PUT. Toate datele sunt „îngropate” în php://input . Nu uitați să îl analizați într-o structură sau o matrice separată înainte de a invoca metoda API-ului propriu-zis. Un simplu parse_str ar putea fi suficient, dar dacă clientul trimite o cerere în mai multe părți, poate fi necesară o analiză suplimentară pentru a gestiona limitele formularelor. Cazurile de utilizare tipice ale cererilor cu mai multe părți includ încărcările de fișiere. Detectarea și gestionarea cererilor din mai multe părți se poate face după cum urmează:

 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); } }

Aici, parse_raw_request ar putea fi implementat ca:

 /** * 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]; } }

Cu aceasta, putem avea sarcina utilă de solicitare necesară la Api::$input ca intrare brută și Api::$input_data ca o matrice asociativă.

Falsificare PUT/DELETE

Uneori vă puteți vedea într-o situație în care serverul nu acceptă nimic în afară de metodele standard GET/POST HTTP. O soluție comună la această problemă este „falsarea” PUT/DELETE sau orice altă metodă de solicitare personalizată. Pentru asta puteți folosi un parametru „magic”, cum ar fi „_method”. Dacă îl vedeți în matricea dvs. $_REQUEST , presupuneți pur și simplu că cererea este de tipul specificat. Cadrele moderne precum Laravel au astfel de funcționalități încorporate în ele. Oferă o compatibilitate excelentă în cazul în care serverul sau clientul dvs. are limitări (de exemplu, o persoană utilizează rețeaua Wi-Fi a postului său în spatele proxy-ului corporativ care nu permite solicitări PUT.)

Redirecționare către API-ul specific

Dacă nu aveți luxul de a reutiliza încărcările automate de proiecte existente, vă puteți crea propriile cu ajutorul funcției spl_autoload_register . Definiți-l în pagina dvs. „api/index.php” și apelați-vă clasa API aflată în „api/Api.php”. Clasa API acționează ca un middleware și apelează metoda reală. De exemplu, o solicitare către „/api/v1.0/records/7.json” ar trebui să ajungă să invoce metoda GET „Apis/Records.php” cu parametrul 7. Acest lucru ar asigura separarea preocupărilor și ar oferi o modalitate de a păstra curățător de logică. Desigur, dacă este posibil să integrați acest lucru mai profund în cadrul pe care îl utilizați și să reutilizați controlerele sau rutele sale specifice, ar trebui să luați în considerare și această posibilitate.

Exemplu „api/index.php” cu încărcător automat primitiv:

 <?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();

Aceasta va încărca clasa noastră Api și va începe să o difuzeze independent de proiectul principal.

OPȚIUNI Cereri

Când un client folosește antetul personalizat pentru a redirecționa simbolul său unic, browserul trebuie mai întâi să verifice ori de câte ori serverul acceptă acel antet. Aici intervine cererea OPȚIUNI. Scopul ei este să se asigure că totul este în regulă și în siguranță atât pentru client, cât și pentru serverul API. Deci, cererea OPȚIUNI poate fi declanșată de fiecare dată când un client încearcă să facă ceva. Cu toate acestea, atunci când un client folosește cookie-uri pentru acreditări, acesta scutește browserul de a trimite această solicitare suplimentară de OPȚIUNI.

Dacă un client solicită POST /users/8.json cu module cookie, cererea sa va fi destul de standard:

  • Aplicația efectuează o solicitare POST către /users/8.json.
  • Browserul execută cererea și primește un răspuns.

Dar cu autorizare personalizată sau antet token:

  • Aplicația efectuează o solicitare POST către /users/8.json.
  • Browserul oprește procesarea cererii și inițiază în schimb o solicitare OPȚIUNI.
  • Solicitarea OPȚIUNI este trimisă la /users/8.json.
  • Browserul primește răspuns cu o listă cu toate metodele și anteturile disponibile, așa cum sunt definite de API.
  • Browserul continuă cu cererea POST originală numai dacă antetul personalizat este prezent în lista antetelor disponibile.

Cu toate acestea, rețineți că, chiar și atunci când utilizați cookie-uri, cu PUT/DELETE este posibil să primiți în continuare acea solicitare de OPȚIUNI suplimentare. Așa că fiți pregătiți să răspundeți.

API Records

Structură de bază

Exemplul nostru de API Records este destul de simplu. Acesta va conține toate metodele de solicitare și va returna ieșirea înapoi la aceeași clasă principală API. De exemplu:

 <?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()); } // ...

Deci, definirea fiecărei metode HTTP ne va permite să construim mai ușor API în stil REST.

Formatarea ieșirii

Răspunsul naiv cu tot ce a primit din baza de date înapoi către client poate avea consecințe catastrofale. Pentru a evita orice expunere accidentală a datelor, creați o metodă specifică de format care ar returna doar cheile din lista albă.

Un alt avantaj al cheilor incluse în lista albă este că puteți scrie documentație pe baza acestora și puteți face toate verificările de tip, asigurându-vă, de exemplu, că user_id va fi întotdeauna un număr întreg, flag is_banned va fi întotdeauna boolean adevărat sau fals, iar data ora va avea un standard format de răspuns.

Rezultatele de ieșire

Anteturi

Metode separate pentru ieșirea antetelor se vor asigura că totul trimis către browser este corect. Această metodă poate folosi avantajele de a face API-ul accesibil prin același domeniu, păstrând în același timp posibilitatea de a primi antet de autorizare personalizat. Alegerea între același domeniu sau domeniu terță parte se poate face cu ajutorul antetelor de server HTTP_ORIGIN și HTTP_REFERER. Dacă aplicația detectează că clientul folosește autorizarea x (sau orice alt antet personalizat), ar trebui să permită accesul din toate originile, permiteți antetul personalizat. Deci ar putea arăta așa:

 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);

Cu toate acestea, dacă clientul folosește acreditări bazate pe cookie-uri, anteturile ar putea fi puțin diferite, permițând doar anteturile de gazdă solicitate și legate de cookie-uri pentru acreditări:

 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']); }

Rețineți că solicitarea OPȚIUNI nu acceptă cookie-uri, așa că aplicația nu le va trimite împreună cu ea. Și, în cele din urmă, acest lucru permite tuturor metodelor HTTP dorite să aibă expirarea controlului accesului:

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

Corp

Corpul însuși ar trebui să conțină răspunsul într-un format solicitat de clientul dvs. cu o stare HTTP 2xx la succes, starea 4xx la eșec din cauza clientului și starea 5xx la eșec din cauza serverului. Structura răspunsului poate varia, deși specificarea câmpurilor „stare” și „răspuns” ar putea fi de asemenea benefică. De exemplu, dacă clientul încearcă să înregistreze un utilizator nou și numele de utilizator este deja luat, puteți trimite un răspuns cu starea HTTP 200, dar un JSON în organism care arată ceva de genul:

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

… în loc de eroare HTTP 4xx direct.

Concluzie

Nu există două proiecte exact la fel. Strategia prezentată în acest articol poate fi sau nu potrivită pentru cazul dvs., dar conceptele de bază ar trebui să fie totuși similare. Este de remarcat faptul că nu fiecare pagină poate avea în spate cel mai recent cadru de tendință sau actualizat, iar uneori furia cu privire la „de ce pachetul meu REST Symfony nu funcționează aici” poate fi transformată într-o motivație pentru a construi ceva util, ceva care funcționează. Rezultatul final poate să nu fie la fel de strălucitor, deoarece va fi întotdeauna o implementare personalizată și specifică proiectului, dar la sfârșitul zilei, soluția va fi ceva care funcționează cu adevărat; și într-un scenariu ca acesta, acesta ar trebui să fie scopul fiecărui dezvoltator de API.

Exemple de implementări ale conceptelor discutate aici au fost încărcate într-un depozit GitHub pentru comoditate. Este posibil să nu doriți să utilizați aceste coduri exemplu direct în producție așa cum sunt, dar acest lucru ar putea funcționa cu ușurință ca punct de plecare pentru următorul proiect de integrare PHP API.

A trebuit să implementeze recent un server API REST pentru un proiect moștenit? Împărtășiți-vă experiența cu noi în secțiunea de comentarii de mai jos.