Budowanie REST API dla starszych projektów PHP

Opublikowany: 2022-03-11

Budowanie lub projektowanie REST API nie jest łatwym zadaniem, zwłaszcza gdy musisz to zrobić dla starszych projektów PHP. Obecnie istnieje wiele bibliotek innych firm, które ułatwiają implementację interfejsu API REST, ale integracja ich z istniejącymi starszymi bazami kodu może być dość zniechęcająca. I nie zawsze masz luksus pracy z nowoczesnymi frameworkami, takimi jak Laravel i Symfony. W przypadku starszych projektów PHP często możesz znaleźć się gdzieś w środku przestarzałych, wewnętrznych frameworków, działających na starych wersjach PHP.

Budowanie REST API dla starszych projektów PHP

Budowanie REST API dla starszych projektów PHP
Ćwierkać

W tym artykule przyjrzymy się niektórym typowym wyzwaniom związanym z implementacją interfejsów API REST od podstaw, kilku sposobom obejścia tych problemów oraz ogólnej strategii budowania niestandardowych serwerów API opartych na PHP dla starszych projektów.

Chociaż artykuł jest oparty na PHP 5.3 i nowszych, podstawowe koncepcje są ważne dla wszystkich wersji PHP poza wersją 5.0 i mogą być stosowane nawet w projektach innych niż PHP. Tutaj nie będziemy omawiać ogólnie, czym jest REST API, więc jeśli nie jesteś z nim zaznajomiony, najpierw go przeczytaj.

Aby ułatwić Ci śledzenie, poniżej znajduje się lista niektórych terminów używanych w tym artykule oraz ich znaczenia:

  • Serwer API: główna aplikacja REST obsługująca API, w tym przypadku napisana w PHP.
  • Punkt końcowy API: backendowa „metoda”, z którą klient komunikuje się w celu wykonania akcji i uzyskania wyników.
  • URL punktu końcowego API: adres URL, przez który system zaplecza jest dostępny dla świata.
  • Token API: unikalny identyfikator przekazywany za pośrednictwem nagłówków HTTP lub plików cookie, na podstawie którego można zidentyfikować użytkownika.
  • Aplikacja: aplikacja kliencka, która będzie komunikować się z aplikacją REST za pośrednictwem punktów końcowych API. W tym artykule przyjmiemy, że jest on oparty na sieci (komputer stacjonarny lub mobilny), a więc jest napisany w JavaScript.

Kroki wstępne

Wzory ścieżek

Jedną z pierwszych rzeczy, które musimy zdecydować, jest to, na jakiej ścieżce adresu URL będą dostępne punkty końcowe interfejsu API. Istnieją 2 popularne sposoby:

  • Utwórz nową subdomenę, taką jak api.example.com.
  • Utwórz ścieżkę, na przykład example.com/api.

Na pierwszy rzut oka może się wydawać, że ten pierwszy wariant jest bardziej popularny i atrakcyjny. W rzeczywistości jednak, jeśli budujesz API specyficzne dla projektu, bardziej odpowiednie może być wybranie drugiego wariantu.

Jednym z najważniejszych powodów przyjęcia drugiego podejścia jest to, że pozwala to na wykorzystanie plików cookie jako środka do przesyłania danych uwierzytelniających. Klienci korzystający z przeglądarki będą automatycznie wysyłać odpowiednie pliki cookie w ramach żądań XHR, eliminując potrzebę dodatkowego nagłówka autoryzacji.

Innym ważnym powodem jest to, że nie musisz nic robić w związku z konfiguracją subdomeny lub problemami z zarządzaniem, gdzie niestandardowe nagłówki mogą być usuwane przez niektóre serwery proxy. Może to być żmudna próba w starszych projektach.

Używanie plików cookie można uznać za praktykę „nieRESTful”, ponieważ żądania REST powinny być bezstanowe. W takim przypadku możemy dokonać kompromisu i przekazać wartość tokena w pliku cookie zamiast przekazywać go przez niestandardowy nagłówek. W rzeczywistości używamy plików cookie jako sposobu na przekazanie wartości tokena zamiast bezpośrednio session_id. Takie podejście można uznać za bezpaństwowe, ale możemy to pozostawić Twoim preferencjom.

Adresy URL punktów końcowych interfejsu API mogą być również wersjonowane. Ponadto mogą zawierać oczekiwany format odpowiedzi jako rozszerzenie w nazwie ścieżki. Chociaż nie są one krytyczne, zwłaszcza na wczesnych etapach rozwoju API, na dłuższą metę te szczegóły z pewnością mogą się opłacić. Zwłaszcza, gdy potrzebujesz wdrożyć nowe funkcje. Najlepszym rozwiązaniem może być sprawdzenie, jakiej wersji oczekuje klient i zapewnienie wymaganego formatu dla kompatybilności wstecznej.

Struktura adresu URL punktu końcowego interfejsu API może wyglądać następująco:

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

I prawdziwy przykład:

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

Rozgromienie

Po wybraniu bazowego adresu URL dla punktów końcowych API, następną rzeczą, którą musimy zrobić, jest zastanowienie się nad naszym systemem routingu. Można go zintegrować z istniejącym frameworkiem, ale jeśli jest to zbyt kłopotliwe, potencjalnym obejściem jest utworzenie folderu o nazwie „api” w katalogu głównym dokumentu. W ten sposób API może mieć całkowicie oddzielną logikę. Możesz rozszerzyć to podejście, umieszczając logikę API we własnych plikach, na przykład:

Możesz myśleć o „www/api/Apis/Users.php” jako o oddzielnym „kontrolerze” dla konkretnego punktu końcowego API. Przydałoby się ponowne wykorzystanie implementacji z istniejącej bazy kodu, na przykład ponowne wykorzystanie modeli, które są już zaimplementowane w projekcie do komunikacji z bazą danych.

Na koniec upewnij się, że wszystkie przychodzące żądania z „/api/*” są wskazywane na „/api/index.php”. Można to zrobić, zmieniając konfigurację serwera WWW.

Klasa API

Wersja i format

Zawsze powinieneś jasno określić, jakie wersje i formaty akceptują Twoje punkty końcowe API, a jakie są domyślne. Umożliwi to budowanie nowych funkcji w przyszłości przy zachowaniu starych funkcjonalności. Wersja API może być w zasadzie ciągiem, ale możesz użyć wartości liczbowych, aby lepiej zrozumieć i porównać. Dobrze jest mieć zapasowe cyfry do wersji drugorzędnych, ponieważ wyraźnie wskazywałoby to, że niewiele rzeczy się różni:

  • v1.0 oznaczałaby pierwszą wersję.
  • v1.1 pierwsza wersja z kilkoma drobnymi zmianami.
  • v2.0 byłaby zupełnie nową wersją.

Format może być wszystkim, czego potrzebuje Twój klient, w tym między innymi JSON, XML, a nawet CSV. Dostarczając go za pośrednictwem adresu URL jako rozszerzenia pliku, adres URL punktu końcowego interfejsu API zapewnia czytelność i staje się oczywistym dla konsumenta interfejsu API, aby wiedzieć, jakiego formatu może się spodziewać:

  • „/api/v1.0/records.json” zwróci tablicę rekordów JSON
  • „/api/v1.0/records.xml” zwróci plik XML z rekordami

Warto zaznaczyć, że będziesz musiał również wysłać odpowiedni nagłówek Content-Type w odpowiedzi dla każdego z tych formatów.

Po otrzymaniu przychodzącego żądania jedną z pierwszych rzeczy, które należy zrobić, jest sprawdzenie, czy serwer API obsługuje żądaną wersję i format. W głównej metodzie, która obsługuje przychodzące żądanie, przeanalizuj $_SERVER['PATH_INFO'] lub $_SERVER['REQUEST_URI'], aby określić, czy żądany format i wersja są obsługiwane. Następnie kontynuuj lub zwróć odpowiedź 4xx (np. 406 „Nie do przyjęcia”). Najważniejszą częścią jest tutaj zawsze zwracanie czegoś, czego oczekuje klient. Alternatywą do tego byłoby sprawdzenie nagłówka żądania „Akceptuj” zamiast rozszerzenia ścieżki adresu URL.

Dozwolone trasy

Możesz przesłać wszystko w sposób przezroczysty do swoich kontrolerów API, ale może lepiej użyć zestawu dozwolonych tras z białej listy. Zmniejszyłoby to nieco elastyczność, ale zapewni bardzo jasny wgląd w wygląd adresów URL punktów końcowych interfejsu API przy następnym powrocie do kodu.

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

Możesz także przenieść je do oddzielnych plików, aby wszystko było czystsze. Powyższa konfiguracja będzie używana do włączania żądań do tych adresów URL:

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

Obsługa danych PUT

PHP automatycznie obsługuje przychodzące dane POST i umieszcza je w superglobalne $_POST . Jednak tak nie jest w przypadku żądań PUT. Wszystkie dane są „pochowane” w php://input . Nie zapomnij przeanalizować go do osobnej struktury lub tablicy przed wywołaniem właściwej metody API. Prosty parse_str może wystarczyć, ale jeśli klient wysyła wieloczęściowe żądanie, może być potrzebne dodatkowe parsowanie, aby obsłużyć granice formularza. Typowy przypadek użycia żądań wieloczęściowych obejmuje przesyłanie plików. Wykrywanie i obsługę żądań wieloczęściowych można wykonać w następujący sposób:

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

Tutaj parse_raw_request można zaimplementować jako:

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

Dzięki temu możemy mieć niezbędny ładunek żądania w Api::$input jako surowe dane wejściowe i Api::$input_data jako tablicę asocjacyjną.

Udawanie PUT/DELETE

Czasami możesz zobaczyć się w sytuacji, gdy serwer nie obsługuje niczego poza standardowymi metodami GET/POST HTTP. Typowym rozwiązaniem tego problemu jest „fałszywe” PUT/DELETE lub jakakolwiek inna niestandardowa metoda żądania. W tym celu możesz użyć parametru „magicznego”, takiego jak „_method”. Jeśli widzisz to w swojej tablicy $_REQUEST , po prostu załóż, że żądanie jest określonego typu. Nowoczesne frameworki, takie jak Laravel, mają wbudowaną taką funkcjonalność. Zapewnia doskonałą kompatybilność w przypadku, gdy Twój serwer lub klient ma ograniczenia (na przykład dana osoba korzysta z sieci Wi-Fi w swojej pracy za firmowym serwerem proxy, która nie zezwala na żądania PUT).

Przekazywanie do określonego API

Jeśli nie masz luksusu ponownego wykorzystania istniejących programów do automatycznego ładowania projektów, możesz stworzyć własne za pomocą funkcji spl_autoload_register . Zdefiniuj go na swojej stronie „api/index.php” i wywołaj klasę API znajdującą się w „api/Api.php”. Klasa API działa jako oprogramowanie pośredniczące i wywołuje właściwą metodę. Na przykład żądanie do „/api/v1.0/records/7.json” powinno zakończyć się wywołaniem metody GET „Apis/Records.php” z parametrem 7. Zapewniłoby to oddzielenie obaw i zapewniłoby sposób na zachowanie środek do czyszczenia logiki. Oczywiście, jeśli możliwe jest głębsze zintegrowanie tego z frameworkiem, którego używasz i ponowne wykorzystanie jego określonych kontrolerów lub tras, powinieneś również rozważyć tę możliwość.

Przykład „api/index.php” z prymitywnym autoloaderem:

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

To załaduje naszą klasę Api i zacznie ją obsługiwać niezależnie od głównego projektu.

OPCJE Żądania

Gdy klient używa niestandardowego nagłówka do przekazania swojego unikalnego tokena, przeglądarka musi najpierw sprawdzić, czy serwer obsługuje ten nagłówek. Tutaj pojawia się żądanie OPTIONS. Jego celem jest zapewnienie, że wszystko jest w porządku i bezpieczne zarówno dla klienta, jak i serwera API. Tak więc żądanie OPTIONS może być uruchamiane za każdym razem, gdy klient próbuje coś zrobić. Jednak gdy klient używa plików cookie do poświadczeń, przeglądarka nie musi wysyłać tego dodatkowego żądania OPTIONS.

Jeśli klient żąda POST /users/8.json za pomocą plików cookie, jego żądanie będzie dość standardowe:

  • Aplikacja wykonuje żądanie POST do /users/8.json.
  • Przeglądarka realizuje żądanie i otrzymuje odpowiedź.

Ale z autoryzacją niestandardową lub nagłówkiem tokena:

  • Aplikacja wykonuje żądanie POST do /users/8.json.
  • Przeglądarka przestaje przetwarzać żądanie i zamiast tego inicjuje żądanie OPTIONS.
  • Żądanie OPTIONS jest wysyłane do /users/8.json.
  • Przeglądarka otrzymuje odpowiedź z listą wszystkich dostępnych metod i nagłówków zdefiniowanych przez API.
  • Przeglądarka kontynuuje oryginalne żądanie POST tylko wtedy, gdy niestandardowy nagłówek znajduje się na liście dostępnych nagłówków.

Należy jednak pamiętać, że nawet w przypadku korzystania z plików cookie przy użyciu funkcji PUT/DELETE możesz nadal otrzymywać dodatkowe żądanie OPTIONS. Przygotuj się więc na odpowiedź.

Rekordy API

Podstawowa struktura

Nasze przykładowe API Records jest dość proste. Będzie zawierał wszystkie metody żądania i zwracał dane wyjściowe z powrotem do tej samej głównej klasy API. Na przykład:

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

Tak więc zdefiniowanie każdej metody HTTP pozwoli nam łatwiej zbudować API w stylu REST.

Formatowanie wyjścia

Naiwne reagowanie na wszystko, co otrzymaliśmy z bazy danych z powrotem do klienta, może mieć katastrofalne konsekwencje. Aby uniknąć przypadkowego ujawnienia danych, utwórz określoną metodę formatowania, która zwróci tylko klucze z białej listy.

Kolejną zaletą kluczy umieszczonych na białej liście jest to, że można na ich podstawie pisać dokumentację i przeprowadzać wszystkie kontrole typu, upewniając się, że na przykład user_id zawsze będzie liczbą całkowitą, flaga is_banned zawsze będzie miała wartość logiczną prawda lub fałsz, a czasy będą miały jeden standard format odpowiedzi.

Wyprowadzanie wyników

Nagłówki

Oddzielne metody dla wyjścia nagłówków zapewnią, że wszystko, co zostanie wysłane do przeglądarki, będzie poprawne. Ta metoda może wykorzystać zalety udostępnienia API przez tę samą domenę, przy jednoczesnym zachowaniu możliwości otrzymywania niestandardowego nagłówka autoryzacji. Wybór między tą samą domeną lub domeną innej firmy może nastąpić za pomocą nagłówków serwera HTTP_ORIGIN i HTTP_REFERER. Jeśli aplikacja wykryje, że klient korzysta z autoryzacji x (lub dowolnego innego niestandardowego nagłówka), powinna zezwolić na dostęp ze wszystkich źródeł, zezwól na niestandardowy nagłówek. Więc może to wyglądać tak:

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

Jeśli jednak klient używa poświadczeń opartych na plikach cookie, nagłówki mogą być nieco inne, zezwalając na poświadczenia tylko żądanych nagłówków związanych z hostem i plikami cookie:

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

Pamiętaj, że żądanie OPTIONS nie obsługuje plików cookie, więc aplikacja ich nie wyśle. I wreszcie pozwala to wszystkim naszym poszukiwanym metodom HTTP na wygaśnięcie kontroli dostępu:

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

Ciało

Sama treść powinna zawierać odpowiedź w formacie żądanym przez klienta ze statusem HTTP 2xx w przypadku powodzenia, 4xx w przypadku niepowodzenia z powodu klienta i 5xx w przypadku niepowodzenia z powodu serwera. Struktura odpowiedzi może być różna, chociaż określenie pól „status” i „odpowiedź” również może być korzystne. Na przykład, jeśli klient próbuje zarejestrować nowego użytkownika, a nazwa użytkownika jest już zajęta, możesz wysłać odpowiedź ze statusem HTTP 200, ale JSON w treści wygląda mniej więcej tak:

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

…zamiast błędu HTTP 4xx bezpośrednio.

Wniosek

Żadne dwa projekty nie są dokładnie takie same. Strategia opisana w tym artykule może, ale nie musi być odpowiednia dla twojego przypadku, ale podstawowe koncepcje powinny być jednak podobne. Warto zauważyć, że nie każda strona może mieć za sobą najnowsze trendy lub aktualne frameworki, a czasami złość na „dlaczego mój pakiet REST Symfony tutaj nie działa” może przerodzić się w motywację do zbudowania czegoś pożytecznego, coś, co działa. Efekt końcowy może nie być tak błyszczący, ponieważ zawsze będzie to jakieś niestandardowe i specyficzne dla projektu wdrożenie, ale ostatecznie rozwiązanie będzie czymś, co naprawdę działa; i w takim scenariuszu powinien to być cel każdego programisty API.

Przykładowe implementacje omawianych tutaj koncepcji zostały przesłane do repozytorium GitHub dla wygody. Możesz nie chcieć używać tych przykładowych kodów bezpośrednio w środowisku produkcyjnym, ale może to z łatwością posłużyć jako punkt wyjścia dla Twojego kolejnego starszego projektu integracji API PHP.

Musiałeś ostatnio zaimplementować serwer REST API dla jakiegoś starszego projektu? Podziel się z nami swoim doświadczeniem w sekcji komentarzy poniżej.